calibre
Author: Kovid Goyal
Date: 2024-05-03T03:02:17+00:00
Description: Comprehensive documentation for calibre

导航

- 模块
- 下一页 |
- 开始 »
- calibre 用户手册

calibre 用户手册
calibre 是一个电子书库管理器。它能够查看、转换、分类大部分主要的电子书格式。它还能够连接多种电子书阅读设备。calibre 可以连接互联网，并为您的电子书籍抓取元数据。它能够下载报纸并将其转换电子书以便阅读。它是跨平台的，可以在 Linux、Windows 和 macOS 上运行。

您已经启动 calibre 了，现在做什么呢？在 calibre 可以开始处理你的电子书之前，它首先要了解您的电子书。拖放一些电子书文件至 calibre，或者点击“添加书籍”按钮并浏览你想要处理的电子书。一旦你添加了书籍，它们就会显示在主窗口中，如图所示：

当你欣赏刚刚添加进你的目录的书籍列表时，你可能想要阅读其中的书籍。为了做到这一点，你需要将书籍转换成你的电子书阅读器可以理解的格式。当第一次运行 calibre 时，“欢迎向导”会显示，并将为你的电子书阅读设备对 calibre 进行设置。转换格式是轻而易举的。只要选择你想要转换的书籍，然后点击“转换书籍”按钮。现在先忽略所有的选项，点击“确定”。右下角的小图标将开始旋转。一旦它停止旋转，转换后的书籍就准备就绪了。点击“阅读”按钮可以阅读书籍。

如果你想在你的阅读器上阅读书籍，那么请连接阅读器与电脑，待 calibre 检测到阅读器后（约10-20秒），点击“发送到设备”按钮。图标停止旋转后，即可断开阅读器，开始阅读！如果你未在之前步骤中转换书籍格式，calibre 会自动将书籍转换为阅读器支持的格式。

想要学会更进阶的使用方法，您可以阅读“gui”。为了使用更强大、更灵活的功能，可以学习“generated/zh_CN/cli-index”。你会发现“faq”列表也很有用。

如果你有更多问题，或想与其他用户讨论 calibre，或为特定的问题寻求帮助，可以访问”论坛和其他可用的帮助资源<help>”。

节

- 图形用户界面
- 添加你喜欢的新闻站点
- 电子书阅读器
- 电子书转换
- 编辑书籍
- calibre 内容服务器
- 对比电子书
- 编辑电子书元数据
- 常见问题
- 教程
- Calibre:// URL方案
- 自定义Calibre
- 命令行界面
- 设置Calibre开发环境
- 数字版权管理(DRM)
- 词汇表

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 图形用户界面

图形用户界面

通过图形用户界面*（GUI）*可访问所有的书库管理和电子书格式转换功能。使用 calibre 的基本工作流程是：首先从你的硬盘中将书籍添加进书库。calibre 将自动尝试从书籍中读取元数据，并将其添加进 calibre 的内部数据库。一旦元数据在数据库中，你可以对它们进行不同的“操作”，包括转换格式、传输到电子书阅读器、在电脑上阅读、编辑元数据等。编辑元数据包括修改封面、描述和标签及其他详细信息。注意 calibre 复制了你添加的文件，你的原始文件不会被修改。

calibre 的界面分为几个部分：

- 操作

- 首选项

- 书目

- 搜索和排序

- 搜索界面

- 保存搜索

- 搜索所有书籍的全文

- 虚拟书库

- 临时标记书本

- 从文件名猜测元数据

- 书籍详情

- 标签浏览器

- 封面网格

- 封面浏览器

- 为作者、系列等添加注释。

- 快速过滤

- 任务

- 键盘快捷键

操作

操作栏提供了常用操作的快捷入口。如果你右键单击这些按钮，你可以在默认操作的基础上进行变化。请注意，你的电子书阅读器连接电脑前后，操作栏的样子会稍有不同。

- 添加书籍

- 编辑元数据

- 转换书籍

- 阅读

- 发送到设备

- 抓取新闻

- 书库

- 设备

- 保存到磁盘

- 连接/共享

- 移除书籍

添加书籍

通过右键单击“Add books”按钮，可以执行七种变化操作。

1. 从单一目录添加书籍：本操作将打开一个文件选择窗口，让你指定一个目录来从中添加书籍。这个操作*与操作发生的环境相关*：它具体干什么取决于你选择的”书目<catalogs>”。如果你选择了”书库”，则添加书籍至书库；如果你选择了电子书阅读设备，则上传书籍至设备；类似这样。

2. 从目录及子目录添加图书：让您选择一个目录，然后递归扫描该目录及其所有子目录，并将找到的所有电子书添加到书库。您可以选择让 calibre 将某个目录中存在的所有文件添加到同一个还是好几个书籍记录。calibre 假设每个目录只含一本书——同一目录中的所有电子书文件都代表同一本书，只是格式不同。此操作是“保存到磁盘 <save_to_disk_multiple>”的反操作，换句话说，您可以先“保存到磁盘”，然后删除书籍，接着重新用每个目录单个书籍模式添加回来，除日期外不会丢失任何信息（假设您没有更改保存到磁盘操作的任何设置）。

3. 从存档 (ZIP/RAR) 添加多本书：允许您添加存储在所选 ZIP 或 RAR 文件中的多本电子书。 这是一个方便的快捷方式，可以避免必须先解压缩存档，然后通过上述两个选项之一添加书籍。

4. 添加空白书籍（无格式的书籍条目）：这一操作允许你创建一条空白的书籍记录，可以用于手动填写你还没有的书的信息。

5. 使用 ISBN 号添加书籍：这一操作允许你通过输入书籍的 ISBN 号添加一本或多本书籍。

6. 添加文件到选中的书籍记录中：允许你添加或更新书库中与现有书籍关联的文件。

7. 将数据文件添加到所选图书记录: 允许您添加任意数量的额外文件，这些文件将存储在图书目录中的“data”子目录中。有关 详细信息，请参阅“data_files”。

8. 添加一个空文件到选中的书籍记录中：允许您将指定格式的空文件添加到选定的书籍记录。

“添加书籍”功能可以从许多种类的电子书格式中读取元数据。此外，它还尝试从文件名猜测元数据。参见“config_filename_metadata”一节以学习如何设置这一功能。

要为现有书籍添加附加格式，您可以执行以下三项操作之一：

1. 将文件拖放至主窗口右侧的书籍详情面板。

2. 右键单击“添加书籍”按钮，选择“添加文件到选中的书籍记录中”。

3. 单击“编辑元数据”对话框右上角区域中的“添加书籍”按钮，可通过“编辑元数据信息”操作来访问。

编辑元数据

：“编辑元数据”操作有四种变化操作，可以通过右键单击该按钮进行访问。

1. 逐个修改原数据：这一操作允许你一本一本地编辑书籍的元数据，有从互联网获取元数据的选项，包括封面。这一操作还允许你添加或移除一本书籍的特定的电子书格式。

2. 批量修改元数据：这一操作允许你同时编辑多本书籍的相同元数据字段，它将修改你在“Library view<search_sort>”中选中的所有书籍。

3. **下载元数据和封面&&：下载在书籍列表中选中的书籍的元数据和封面（如果可用）。

4. 合并书籍记录：向你提供合并两个或多个书籍记录的元数据和格式的功能。你可以选择删除或保留不是第一次点击的书籍记录。

5. 管理数据文件: 管理与所选书籍关联的额外数据文件。

有关更多 详细信息，请参阅：“metadata”。

转换书籍

电子书可以从许多种格式转换成你的电子书阅读器偏好的格式。很多可购买电子书被`数字权利管理 <drm>` *（DRM）*技术所保护。calibre 不会转换这些电子书。很多格式电子书的 DRM 很容易移除，但是由于这样做可能违法，你必须自己找到工具解除电子书的限制，然后再使用 calibre 转换。

对大多数人来说，转换应该是简单的一键完成。如果你想要知道更多关于转换过程的信息，参见“conversion”。

“转换书籍”操作有三种变化操作，可以通过右键单击该按钮进行访问。

1. 逐个转换：这一操作允许你指定转换选项，以自定义每一本选中的电子书的转换过程。

2. 批量转换：这一操作允许你一次性指定转换选项，以批量转换多本电子书。

3. 为你的 calibre 书库中的书籍编制书目：这一操作允许你生成书库中书籍的完整列表，包括所有的元数据，格式包括XML、CSV、BiBTeX、EPUB 和 MOBI。书目将包括当前显示在书库视图中的所有书。这一操作允许你使用搜索特性限制被编目的书籍。此外，如果你使用鼠标选中数本书籍，则只有这些书籍会被添加进书目中。如果你生成的书目是电子书格式，例如EPUB、MOBI或AZW3，下次你连接电子书阅读器时，书目将自动发送至设备。关于书目如何运作的更多信息，参见“catalog_tut”。

阅读

：“阅读”操作可在电子书阅读器程序中阅读书籍。calibre 有一个内置的阅读器，支持多种电子书格式。对于其他格式，calibre 将使用默认的操作系统应用程序。你可以通过“首选项->界面->界面行为”设置何种格式应以内置阅读器打开。如果一本书籍有多种格式，你可以右键单击该按钮来阅读其中一种格式。

发送到设备

：“发送到设备”操作有八种变化操作，可以通过右键单击该按钮进行访问。

1. 传送到内部储存空间：选中的书籍将被传送到电子书阅读器的内部储存空间。

2. 发送到储存卡（A）：选中的书籍将被发送到电子书阅读器的储存卡（A）。

3. 发送到储存卡（B）：选中的书籍将被发送到电子书阅读器的储存卡（B）。

4. 发送特定格式至：选中的书籍将以你指定的格式被发送到设备的选中的存储位置。

5. 弹出设备：断开设备与 calibre 的连接。

6. 设置“发送到设备”的默认操作：这一操作允许你指定单击“发送到设备”的主按钮时，上面1-5或下面的7这些选项中哪一个会成为默认操作。

7. 发送并从书库中删除：选中的书籍将被传送至设备的选中的存储位置，然后从书库中**删除**。

8. 抓取注解（实验功能）：将你在设备上做出的电子书中的注解传送到在 calibre 书库中该书籍的评论元数据。

你可以在“首选项->导入/导出->发送书籍到设备”设置一个模板，以控制发送至设备的文件的文件名和目录结构。

抓取新闻

“抓取新闻”操作从不同的网站下载新闻，并将其转换成你的电子书阅读器可以读取的电子书格式。通常，新创建的电子书会添加进你的电子书书库，但是如果下载完成时电子书阅读器连接到电脑，则新闻会自动上传到阅读器。

“抓取新闻”操作对每个新闻网站使用简单的新闻源（10-15行的代码）。要学习如何为你自己的新闻源创建新闻新闻，参见“news”。

“抓取新闻”操作有三种变化操作，可以通过右键单击该按钮进行访问。

1. 定期新闻下载：这一操作允许你定期从数百个可用的新闻源中选择并定期下载。你选择的每个新闻源的下载日程可以单独设置，下载日程是灵活的，允许你选择一周中特定的数天或每隔数天进行下载。

2. 添加自定义新闻来源：允许您创建一个简单的新闻源，用于从您希望访问的自定义新闻网站下载新闻。创建自定义新闻源可以像指定RSS新闻提要URL一样简单，也可以通过为任务创建基于Python的代码来更具说明性。详情请参考：“新闻”。

3. 下载所有设置了定期下载的新闻源：要求 calibre 立即开始下载你设置了日程的所有新闻源。

书库

“书库”操作允许你创建、切换、重命名或移除书库。calibre 允许你创建任意数量的书库。例如，你可以创建一个虚构书库、一个非虚构书库、一个外语书库、一个项目书库，或者任何满足你要求的结构。书库是 calibre 中最高级别的组织结构。每个书库有自己的一系列书籍、标签、分类和基础存储位置。

1. 切换/创建书库…：这一操作允许你：a）连接至位于其他位置的已经存在的 calibre 书库；b）在新位置创建一个空白书库；c）移动当前书库至指定的新位置。

2. 快速切换：这一操作允许你在 calibre 注册或创建的书库之间切换。

3. 重命名书库：这一操作允许你重命名一个书库。

4. 随机挑一本书：从书库中随机选择一本书

5. 删除书库：允许你将一个书库从 calibre 注销。

6. 导出/导入所有calibre数据：允许您导出calibre数据以迁移到新计算机或导入以前导出的数据。

7. <library name>：操作7、8等等……允许您在已创建或附加到的多个书库之间即时切换访问。这一列表只包括5个最常使用的书库。完整的列表请使用“快速切换”菜单。

8. 书库维护：这一操作允许你检查当前书库的数据一致性问题，以及从备份中恢复当前书库的数据库。

备注

你的电子书的元数据，即标题、作者、标签等存储在你的 calibre 书库文件夹中的 metadata.db 文件。如果这个文件被损坏（非常罕见），你将会丢失元数据。幸运的是，calibre 会自动为每本书籍备份其元数据，并放在该书籍的文件夹下面，作为一个 OPF 文件。通过使用上面提到的“书库维护”下的“恢复数据库”操作，你可以让 calibre 为你从单独的 OPF 文件重新构造 metadata.db 文件。

通过右键单击书籍并选择“复制到书库”，你可以在不同书库之间复制或移动书籍（当你设置了超过一个书库）。

设备

：“设备”操作允许你查看你的设备的主内存或储存卡中的书籍，或弹出设备（将其与 calibre 断开连接）。这个图标在你连接支持的设备时将自动出现在 calibre 的主工具栏上。你可以点击它以查看你的设备中的书籍。你还可以从你的 calibre 书库中拖放书籍至该图标，来将书籍传送至你的设备。反过来，你可以拖放你的设备中的书籍到工具栏上的书库图标，来将书籍从你的设备传送到你的 calibre 书库。

保存到磁盘

: “保存到磁盘”操作有五个变化操作，可以通过右键单击该按钮进行访问。

1. 保存到磁盘：将选中的书籍以目录的形式保存至磁盘。目录结构为：

Author_(sort)
Title
Book Files

通过在“首选项->导入/导出->保存书籍至磁盘”中设置一个模板，你可以控制所保存到磁盘的文件的文件名和目录结构。参见“templatelangcalibre”。

2. 保存到磁盘的单个目录：将选中的书籍保存到磁盘的单个目录中。

对于操作 1. 和 2. ，选中书籍的所有可用的格式和元数据一样会被保存到磁盘。元数据以 OPF 文件格式存储。保存的书籍可以通过“添加书籍<add_books>”操作重新导入书库，不会有任何信息损失。

3. 仅保存*<your preferred>*格式到磁盘：类似操作(1.)，以目录形式保存选中的书籍到磁盘，但仅以你偏好的电子书格式。你可以在“首选项->界面->界面行为->操作方式->首选输出格式”中设置你偏好的格式。

4. 仅保存*<your preferred>*格式到磁盘中的单个目录：保存选中的书籍到磁盘中的单个目录，但仅以你偏好的电子书格式。你可以在“首选项->界面->界面行为->操作方式->首选输出格式”中设置你偏好的格式。

5. 以单一格式保存到磁盘…：类似操作(1.)，以目录形式保存选中的书籍到磁盘，但仅以你从弹出的列表中选择的格式。

连接/共享

：“连接/共享”操作允许你手动连接到一个设备或你的电脑中的一个文件夹。它还允许你设置你的 calibre 书库以便通过浏览器或电子邮件访问。

“连接/共享”操作有四个变化操作，可以通过右键单击该按钮来进行访问。

1. 连接到文件夹：这一操作允许你连接到你的电脑中的任意文件夹，如同该文件夹是一个设备一样，并对该文件夹使用 calibre 对设备拥有的所有功能。如果你的设备不能被 calibre 支持，但可以当做 USB 磁盘使用，这一功能会有用。

2. 启动内容服务器：启动 calibre 的内置 web 服务器。启动后，你的 calibre 书库将可以通过浏览器从 Internet 访问（如果你选择）。通过在“首选项->分享->通过网络分享”，你可以设置 web 服务器被访问的方式。

3. 设置基于电子邮件的书籍共享：这一操作允许通过电子邮件共享书籍和新闻源。为该选项设置了邮件地址后，calibre 将发送新闻更新和书籍更新至输入的邮件地址。你可以在“首选项->分享->通过邮件分享”中设置 calibre 发送邮件的方式。一旦你设置了一个或多个邮件地址，这个菜单项将会被发送书籍到设置的邮件地址的菜单项所替代。

移除书籍

：“移除书籍”操作会**永久删除书籍**，因此需要小心使用。这个功能是*内容敏感的*，也就是说它取决于你选择的是哪个“书目<catalogs>”。如果你选择的是“书库”，书籍会从书库中移除。如果你选择了电子书阅读设备中的书籍，书籍将从设备中移除。如果要删除某一书籍的某一特定格式，使用“edit_meta_information”操作。移除图书也有五种变化操作，可以通过右键单击该按钮来访问。

1. 移除选定书籍：**永久**移除书籍列表中选中的所有书籍。

2. 移除选定书籍中指定格式的电子书文件…：**永久**移除书籍列表中选定的书籍的特定格式的电子书文件。

3. 移除选定书籍中指定格式以外的电子书文件…：**永久**移除书籍列表中选定的书籍的特定格式以外的电子书文件。

4. 移除选定书籍中所有格式的电子书：**永久**移除书籍列表中选中的书籍的所有电子书文件，仅留下元数据。

5. 移除选定书籍的封面：**永久**移除书籍列表中选中的书籍的封面图片文件。

6. 从设备中移除对应的书籍：从连接的设备中移除与书籍列表中选中的书籍对应的电子书文件。

7. 恢复最近删除的内容: 允许您撤消书本或格式的删除。

备注

请注意，当您使用界面中`删除图书`从您的Calibre书库中删除图书时，图书记录会被删除，但图书会临时存储在垃圾桶文件夹中几天。您可以通过右键单击界面中`删除图书`按钮并选择`恢复最近删除的`图书来撤消删除。

首选项

：“首选项”操作允许你改变 calibre 不同方面的工作方式。有四个变化操作，可以通过右键单击该按钮来访问。

1. 首选项：改变 calibre 不同方面的工作方式。点击按钮也会执行该操作。

2. 运行欢迎向导：启动你第一次启动 calibre 时出现的“欢迎向导”。

3. 获取用于增强 calibre 功能的插件：打开新的窗口显示 calibre 插件。这些插件由第三方开发，用于扩展 calibre 的功能。

4. 以调试模式重启：开启调试模式，协助 calibre 开发者解决你遇到的该程序的问题。对大部分用户而言，这一功能应该保持禁用，除非受到开发者指导而启用。

书目

书目*是书籍的集合。calibre 可以管理两种不同类型的*书目：

1. 书库：存储在你电脑上的 calibre 书库的书籍集合。

2. 设备：存储在你的电子书阅读器的书籍集合，在你连接阅读器至电脑时可用。

许多操作，例如添加书籍、删除、阅读等，是环境敏感的。因此，例如如果你在**设备**书目被选中时点击“阅读”按钮，calibre 将打开设备中的文件以供阅读。如果**书库**书目被选中，则你的 calibre 书库中的文件将会被打开。

搜索和排序

“搜索和排序”部分允许你对你的书籍集合执行强大的操作。

- 你可以通过单击列标题，依据标题、作者、日期、评分等对它们进行排序。你还可以进行子排队，也就是对几列进行排序。例如，如果你点击标题列，然后点击作者列，书籍将按照作者排序，然后所有作者相同的条目将按标题排序。

- 你可以使用搜索栏搜索某本特定的书籍或一组书籍。更多信息参见下文。

- 你可以通过在列表中选择要编辑的条目并按:kbd:`E`键，快速方便地编辑元数据。

- 你可以对一组书籍执行“actions”。以下方法可以选中多本书籍：

- 按住Ctrl键不放，点击你想要选择的书籍。

- 按住Shift键不放，点击你想要选择的书籍条目范围的第一本和最后一本。

- 你可以使用“configuartion”窗口设置显示的字段。

- 要执行复杂的基于多列的子排序，请通过“首选项-> 工具栏和菜单 ”将“排序依据”工具添加到工具栏中。

搜索界面

您可以通过在搜索栏中输入搜索词来搜索所有书籍元数据。例如：

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

搜索种类

有四种搜索类型：包含、相等、`正则表达式`(参见`正则表达式<https://en.wikipedia.org/wiki/Regular_expression>`_)，和`字符变量`。您可以选择带有前缀字符的搜索类型。

‘包含’ 搜索

没有前缀字符的搜索是`包含`，并且默认情况下不区分大小写。如果搜索字符串出现在所指示的元数据中的任何位置，则项匹配。您可以通过勾选“首选项/搜索”中的`区分大小写搜索`选项来设置`包含`搜索区分大小写。如果选中搜索选项`非重音字符匹配重音字符且忽略标点符号‘，则字符将匹配其所有变体(例如，e`匹配`é、è、é`和`ë)，并且忽略所有标点符号和空格。例如，如果选中`无重音字符匹配…‘选项，则给定两个书名：

1. Big, Bothéred, and Bad

2. Big Bummer

然后，这些搜索会发现：

- 名称:"er" 同时匹配（’e’同时匹配’e’和’e’）。

- 名称:"g " 两者都匹配，因为空格被忽略。

- 名称:"g," 两者都匹配，因为逗号被忽略。

- 名称:"gb" 两者都匹配，因为第1册中忽略了‘，’，第2册中忽略了空格。

- 名称:"g b" 两者都匹配，因为逗号和空格被忽略。

- 名称:"db" 匹配#1，因为忽略了‘and Bad’中的空格。

- 名称:"," 两者都匹配(它实际上匹配所有图书)，因为逗号被忽略。

如果“无重音字符匹配…”选项，则字符变体、标点符号和空格都是有效的。

您只能设置搜索选项“区分大小写搜索”和“无重音字符与重音字符匹配，标点符号被忽略”的其中一个。

“等式”搜索

相等搜索是通过在搜索字符串前面加一个等号（=）来表示的。例如，查询“tag:”=science“”将匹配“science”，但不匹配“scientificy”或“hard science”。字符变体很重要：“é”与“e”不匹配。

Two variants of equality searches are used for hierarchical items (e.g., A.B.C): hierarchical prefix searches and hierarchical component searches. The first, indicated by a single period after the equals (=.) matches the initial parts of a hierarchical item. The second, indicated by two periods after the equals (=..) matches an internal name in the hierarchical item. Examples, using the tag History.Military.WWII as the value:

- 标签:"=.History" : True. ``历史``是标签的前缀。

- 标签:"=.History.Military" : True. ``History ory.Military``是标签的前缀。

- 标签:"=.History.Military.WWII" : True. `‘History ory.Military.WWII``是标签的前缀，尽管这个前缀并不恰当。

- 标签:"=.Military" : False. ``Military``不是标签的前缀。

- 标签:"=.WWII" : False. ``Military``不是标签的前缀。

- 标签:"=..History" : True. 该层次结构包含值``历史记录``。

- 标签:"=..Military" : True. 该层次结构包含值``Military``。

- 标签:"=..WWII" : True. 该层次结构包含值``WWII``。

- 标签:"=..Military.WWII" : False. ``..`搜索查找单一值。

‘正则表达式’ 搜索

正则表达式搜索是通过在搜索字符串前面加波浪号（~）来表示的。任何`Python兼容的正则表达式<https://docs.python.org/library/re.html>`__可以使用。用于转义正则表达式中特殊字符的反斜杠必须加倍，因为在查询分析过程中将删除单个反斜杠。例如，要匹配文字括号，必须输入“\（”或使用“超级引号”（见下文）。正则表达式搜索是“包含”搜索，除非该表达式被锚定。字符变体很重要：“~e”与“é”不匹配。

“字符变体”搜索

字符变体搜索是通过在搜索字符串前面加一个插入符号（^）来指示的。此搜索类似于“包含”搜索（如上），不同之处在于：

- 字母大小写总是被忽略。

- 字符变体总是相互匹配。

- 标点符号和空格总是很重要。

搜索选项“无重音字符与重音字符匹配，标点符号被忽略”和“区分大小写搜索”被忽略。它们对搜索的行为没有影响。

下面将此搜索与包含搜索进行比较，假设在给定相同的两本书名的情况下选中了“非重音字符匹配…”选项（见上文）：

1. Big, Bothéred, and Bad

2. Big Bummer

然后，这些字符变体搜索会找到：

- 名称:"^er" 两者都匹配(‘e’与‘é’和‘e’都匹配)

- 名称:"^g" 两者都匹配

- ``名称:”^g “``匹配#2，因为空格是有效的

- 名称:"^g," 匹配#1，因为逗号是有效的

- 名称:"^gb" 不匹配，因为空格和逗号都是有效的

- 名称:"^g b" 匹配#2，因为逗号是有效的

- 名称:"^db" 不匹配

- 名称:"^," 匹配#1（而不是所有书籍），因为逗号是有效的

搜索正则表达式

“搜索表达式”是由运算符“和”以及“或”选择性地分隔的“搜索项”序列。如果出现两个没有分隔运算符的搜索项，则假定为“”和“”。“and”运算符的优先级高于“or”运算符；例如，表达式“a或b和c”与“a或（b和c）”相同。您可以使用括号来更改优先级；例如“（a或b）和c”，使“或”在“和”之前求值。您可以使用运算符“not”来否定（反转）搜索表达式的求值结果。示例：

- Not Tag：foo 查找不包含标签 ``foo`的所有图书

- not (作者：阿西莫夫或作者：韦伯) 发现所有的书都不是阿西莫v或韦伯写的。

以上示例显示了“搜索项”的示例。基本的“搜索项”是不包括空格的字符序列，引号（“）、反斜杠（\）或圆括号（（））。它前面可以有列名说明符：列的 “查找名称”后面跟着冒号（：），例如`author:Asimo``。如果搜索项必须包含空格，则整个搜索项必须用引号括起来，如``title:“the Ring“``。如果搜索项必须包含引号，则必须用反斜杠对其进行转义。例如，要搜索名为“Ball”和“Chain”的系列，请使用：

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

如果您需要一个实际的反斜杠，这是`正则表达式`搜索中经常发生的事情，请使用其中两个反斜杠(\\)。

有时很难正确执行所有转义，因此结果就是您想要的，尤其是在“正则表达式”和“模板”搜索中。在这些情况下，使用“超级引号”：“”“”字符序列“”“``。超级引号字符使用不变：不进行转义处理。

更多信息

要搜索以等号、波浪号或脱字符开头的字符串，请在字符串前面加上反斜杠。

如果搜索字符串包含括号或空格，请用引号（“）将其括起来。例如，要查找标记为“科幻小说”的书籍，必须搜索 “tag:”=科幻小说“”。如果搜索 “tag:=科幻小说”，则在任何元数据中都会找到标记为“科学”和单词“小说”的所有书籍。

通过单击搜索框左侧的按钮|sbi|，可以使用“高级搜索对话框”轻松构建高级搜索查询。

可用的搜索字段为：tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl``和自定义列。如果设备插入，搜索 calibre 书库视图时 ``ondevice 字段也可以使用。悬停鼠标光标在书库视图中自定义列的列标题上，可以找到自定义列的搜索名称（实际上叫作`查询名称`）。

日期

日期的搜索规则是：

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

如果日期不明确，则使用当前区域设置进行日期比较。例如，在mm/dd/yyyy区域设置中，2009年1月2日被解释为2009年2月1日。在dd/mm/yyyy区域设置中，它被解释为2009年1月2日。一些特殊的日期字符串是可用的。字符串“today”翻译成今天的日期，不管它是什么。字符串“昨日”和“本月”（或当前语言中的翻译等价物）也可以使用。此外，字符串“daysago”（也可翻译）可用于与几天前的日期进行比较。例如：

date:>10daysago
date:<=45daysago

为了避免使用非英语版本的 calibre 时翻译的字符串的潜在问题，字符串``_today``、_yesterday、``_thismonth``和``_daysago``永远可用。它们不会被翻译。

使用关系比较搜索日期和数值

日期型和数值型字段支持关系运算符``=`` （等于）、 >``（大于）、>=``（大于或等于）、<``（小于）、``<=``（小于或等于）和!=``（不等于）。评分字段被视为数值型。例如，搜索``rating:>=3``将找到所有评分高于或等于3的书籍。

您可以搜索具有特定大小格式的书籍，如下所示：

- ``size:>1.1M“”将查找格式大于1.1MB的书籍

- ``size:<=1K“”将查找格式小于或等于1KB的书籍

您可以使用字符“#”搜索多值字段中的项数，如标签，然后使用与数字字段相同的语法。例如，要查找包含4个以上标签的所有书籍，请使用`tags:#>4``。要查找所有正好有10个标签的书，请使用`tags:#=10``。

系列指数

系列索引是可搜索的。对于标准系列，搜索名称为`series_index``。对于自定义系列列，请使用列搜索名称，后跟_index。例如，要搜索名为“#my_series”的自定义系列列的索引，可以使用搜索名称“#my_series_index”。序列索引是数字，因此可以使用上面描述的关系运算符。

保存的搜索

特殊字段``search``用于`保存的搜索 <saved_searches>`。如果您保存的搜索名为 “My spouse’s books” ，则可以在搜索栏中输入”My spouse’s books”``以重复使用保存的搜索。下面是有关保存搜索的更多信息。

虚拟书库

特殊字段“vl”用于在虚拟书库中搜索书籍。例如，“vl:Read”将查找*Read*虚拟书库中的所有书籍。搜索“vl:Read和vl:“Science Fiction”将找到*Read和*Science Fiction虚拟书库中的所有书籍。“vl:`”后面的值必须是虚拟书库的名称。如果虚拟书库名称包含空格，则用引号将其括起来。

字段是否有值

您可以使用”true”和”false”搜索某个字段的值是否存在。例如：

- cover:false 查找所有没有封面的书籍

- series:true 查找系列中的所有图书

- series:false 查找不在系列中的所有书籍

- comments:false 查找所有评论为空的图书

- ``formats:false``查找没有图书文件的所有图书(空记录)

是/否自定义列

在“是/否”自定义列中搜索“false”、“empty”或“blank”，将在该列中找到所有具有未定义值的书籍。搜索“true”将找到该列中没有未定义值的所有书籍。搜索“是”或“已检查”将找到列中所有带有“是”的书籍。搜索“否”或“未检查”将找到列中所有带有“否”的书籍。请注意，单词“是”、“否”、“空白”、“空”、“已检查”和“未检查”是翻译的；您可以使用当前语言的对等单词或英语单词。单词“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`或单击“标签浏览器”中的配置按钮并选择“仅显示具有可见类别的图书”条目来轻松运行此搜索。

使用模板搜索

您可以使用：`templatelangcalible`中的模板而不是元数据字段进行搜索。要执行此操作，请输入模板、搜索类型和要搜索的值。语法为：

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

“模板”是任何有效的口径模板语言模板。“搜索类型”必须是``t``（文本搜索）、``d``（日期搜索）、``n``（数字搜索）或``b``（设置/不设置（布尔值））之一。“value”是您想要的任何值，并且可以对各种搜索类型使用上面描述的：search kinds <search_kinds> 。如果搜索字符串中的任何位置都有空格，则必须引用整个搜索字符串。

例如：

- template:"program: connected_device_name('main')#@#:t:kindle" – –当连接`kindle``设备时为true。

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – 查找EPUB文件大于1MB的书籍。

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – 查找EPUB文件10天内更新的书籍。

- template:"""program：book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" – 查找标签中包含系列名称的所有图书。此示例使用超级引号，因为模板在构造搜索表达式时同时使用单引号 (') 和双引号 (") 。

您可以通过点击按钮 进入的`高级搜索对话框`轻松构建模板搜索查询。您可以使用口径`模板测试器`在特定图书上测试模板，可以通过`首选项->工具栏和菜单`将其添加到工具栏或菜单中。也可以通过`首选项->快捷键`为其分配键盘快捷键。

高级搜索对话框

“高级搜索对话框”

保存搜索
calibre 支持将经常使用的搜索保存为特殊名称并一键复用该搜索。你可以通过在搜索栏中搜索或使用标签浏览器创建搜索，然后在搜索栏旁边的“保存搜索”中输入搜索的名称。点击“保存搜索”旁边的加号图标保存搜索。

现在，您可以在标签浏览器中的“已保存搜索”下访问保存的搜索。只需单击一下，您就可以轻松重复使用任何任意复杂的搜索，而无需重新创建。

搜索所有书籍的全文
calibre可以*选择性地*索引书库中的书籍全文，以允许即时搜索任何书籍中的单词。要使用此功能，请单击搜索栏左侧的“FT”按钮。

然后，为当前书库启用索引。索引完成后，您可以搜索整个书库中的所有文本。当您将新书添加到书库时，它们将在后台自动编入索引。通过此搜索，您可以快速查找包含一个单词或单词组合的所有书籍。您甚至可以搜索出现在其他单词附近的单词，如搜索弹出窗口中的示例所示。请注意，该搜索工具将在特定书籍中只找到搜索查询的一次出现，而不是列出每一次出现的情况，因为最好使用“电子书阅读器”在书籍内部进行搜索。

您可以通过在Calibre中右键单击界面`图书 详细信息面板`并选择`为这本图书重新编制索引以进行全文搜索`来对单个图书重新编制索引。

虚拟书库

一个“虚拟书库”是一种模仿你的 calibre 书库只有几本书而不是全部藏书的的方式。这是将您的大量藏书划分为较小的、可管理的块的绝佳方式。要了解如何创建和使用虚拟库，请参阅教程：“virtual_libraries”。

临时标记书本

您可以临时标记任意套书籍。有标记的图书上会有别针，可以通过搜索``mark：true`找到它们。要标记图书，请按`Ctrl+M`或进入“首选项->工具栏和菜单”，然后在主工具栏上添加`标记图书`按钮。

您可以使用特定的文本标签来标记图书，方法是右键单击操作界面`标记图书`按钮，然后选择`用文本标签标记图书`。以后可以通过搜索``marked：“=the-Text-You-Ented”``找到标有文本标签的图书。

从文件名猜测元数据

通常，calibre 从电子书文件内读取元数据。然而，它可以通过“首选项->导入/导出->添加书籍->从文件内容而不是文件名读取元数据”，设置为从文件名读取元数据。

您还可以使用正则表达式控制从文件名读取元数据的方式(参见文档`regexp`)。在配置对话框的操作界面“添加图书”部分，您可以指定一个正则表达式，Calibre将使用该正则表达式来尝试并猜测您添加到书库的电子书文件的名称中的元数据。默认正则表达式为：

title - author

即，假设第一个``-``之前的所有字符为书籍的标题，之后的字符为书籍的作者。例如：文件名

Foundation and Earth - Isaac Asimov.txt

将被解读为：标题为“Foundation and Earth”，作者为“Isaac Asimov”。

小技巧

如果文件名不包含连字符，上面的正则表达式将失效。

书籍详情

书籍详情面板显示了当前选中书籍的封面和所有元数据，可以通过 calibre 主窗口右下角的“布局”按钮隐藏。书籍详情面板显示的作者名字可以点击，默认将打开作者的维基百科页面。这个功能可以通过右键单击作者名字并选择管理该作者进行自定义。

类似地，如果你下载书籍的元数据，书籍详情面板将自动显示元数据下载源中该书籍的网页链接，例如亚马逊、worldcat等。

你可以在书籍详情面板中右键单击某种电子书格式来删除该格式电子书、与原始版本对比、保存到磁盘、以外部程序打开灯。

你可以通过简单地拖放图片到书籍详情面板改变书籍的封面。如果你希望在外部程序中编辑封面图片，只要右键单击并选择“封面的打开方式”。

你还可以通过拖放电子书文件到书籍详情面板向当前书籍添加电子书文件。

双击书籍详情面板将让其以分离的弹出窗口打开。

最后，你可以通过“首选项->界面->界面外观->书籍详情”自定义要显示的信息。

标签浏览器

标签浏览器让你可以容易地根据作者、标签、丛书等浏览你的书集。如果点击标签管理器中的任意项目，例如作者名称 Issac Asimov，则右边的书籍列表将显示该作者的书籍。你还可以点击分类的名称。例如，点击“丛书”将显示所有在某一丛书中的书籍。

第一次点击某个项目会将图书列表限制在包含或匹配该项目的图书列表中。继续上面的例子，点击Isaac Asimov将显示该作者的书籍。再次单击该项目将更改显示内容，具体取决于该项目是否有子项目(请参阅下面的子类别和分层项目)。继续艾萨克·阿西莫夫的例子，再次点击艾萨克·阿西莫夫将把图书列表限制在那些不是艾萨克·阿西莫夫写的书上。第三次点击将取消限制，显示所有书籍。如果按住“Ctrl”或“Shift”键并单击多个项目，则会创建基于多个项目的限制。例如，您可以按住键盘“Ctrl”，然后单击标签“历史”和“欧洲”以查找有关欧洲历史的书籍。“标签浏览器”的工作原理是构造自动输入到搜索栏中的搜索表达式。查看“标签浏览器”生成的内容是学习如何构建基本搜索表达式的好方法。

标签浏览器中的项目的图标部分被上色。颜色的数量取决于该类书籍的平均评分。因此，例如如果 Isaac Asimov 的书籍平均评分为四星，则标签浏览器中 Isaac Asimov 的图标将上 4/5 的颜色。你可以在图标上悬停鼠标光标查看平均评分。

“标记浏览器”中的外层条目，如“作者”和“系列”，称为类别。您可以创建自己的类别，操作界面称为“用户类别”，这对组织项目很有用。例如，您可以使用“用户类别编辑器”（单击操作界面“标记浏览器”左下角的“配置”按钮，然后选择“管理作者、标记等->用户类别”）创建一个名为“收藏夹作者”的用户类别，然后将收藏夹的项目放入该类别中。用户类别可以有子类别。例如，用户类别“收藏夹”。“作者”是“收藏夹”的一个子类别。您可能还有“收藏夹”。系列“”，在这种情况下，“收藏夹”下将有两个子类别。右键单击用户类别，选择“将子类别添加到…”，然后输入子类别名称，即可创建子类别；或者使用 “用户类别编辑器”，输入类似上面“收藏夹”示例的名称。

你可以像内置分类一样通过点击搜索用户分类。通过循环点击分类，有四种不同的搜索结果：

1. “所有匹配此分类中的一个项目的书籍”，以一个绿色加号标识。

2. “所有匹配此分类或其子分类中的一个项目的书籍”，以两个绿色加号标识。

3. “所有不匹配此分类中的一个项目的书籍”，以一个红色减号标识。

4. “所有不匹配此分类或其子分类中的一个项目的书籍”，以两个红色减号标识。

在某些文本型分类（例如标签、丛书和自定义列）中创建层次项也是可能的。这些层次项以小三角形显示，允许子项目被隐藏。要在一个分类中使用项目的层次项，你必须首先打开“首选项->界面->界面外观”，然后在“具有层次项的分类”中输入分类名称。完成之后，分类中包含英文句号的项目将使用小三角形展示。例如，假设你创建了一个 自定义列名为“Genre”，并显示其包含层次项。完成之后，类似于 Mystery.Thriller 和 Mystery.English 的项目将显示为 Mystery，旁边有小三角形。点击三角形后将显示 Thriller 和 English 为子项。更多信息参见 “管理书籍的子组织，例如“Genre”<subgroups-tutorial>”。

层次项（有子项的项目）使用与用户分类相同的四次“点击”搜索。没有子项的项目使用两次点击搜索：“所有匹配的书籍”和“所有不匹配的书籍”。

您可以将“标签浏览器”中的项目拖放到用户类别中，以将其添加到该类别中。如果源是“用户类别”，在拖动的同时按住：`Shift`键可将项目移至新类别。您还可以将图书从图书列表拖放到标签浏览器”中的项目上；将图书拖放到项目上会使该项目自动应用于放置的图书。例如，将一本书拖到Isaac Asimov上会将该书的作者设置为Isaac Asimov。将其放到标签历史记录上会将该标签历史记录添加到图书的标签中。

单击右下角的搜索按钮，您可以在标签浏览器中轻松找到任何项目。此外，您可以右键单击任何项目，然后从几个操作中选择一个。有些示例是隐藏它、重命名它，或者打开一个“Manage x”对话框来管理这类项目。例如，操作界面“管理作者”对话框允许您重命名作者并控制其名称的排序方式。

你可以通过标签浏览器左下角的“配置”按钮控制标签浏览器中项目排序的方式。你可以选择以名称、平均评分或受欢迎程度（受欢迎程度是书库中某一项目的书籍的数量；例如，Isaac Asimov 的受欢迎程度是你的书库中 Isaac Asimov的书籍的数量）。

封面网格

如果你更喜欢通过封面来浏览你的书集，你可以让 calibri 显示书籍封面的网格，而不是书籍的列表。点击 calibre 主窗口右下角的“布局”可以激活“封面网格”。通过“首选项->界面->界面外观->封面网格”，你可以自定义封面的大小和背景。你甚至可以让 calibre 在封面下方显示任何指定字段，例如标题、作者、评分或你指定的自定义列。

封面浏览器

除了上文描述的“封面网格”之外，你还可以让 calibre 在一行中展示封面。这个功能可以通过主窗口右下角的“布局”按钮激活。在“首选项->界面->界面外观->封面浏览器”中，你可以改变展示的封面数量，甚至可以让“封面浏览器”展示为一个分离的弹出窗口。

为作者、系列等添加注释。

您可以为作者/系列/标签/出版商等添加注释。到您的calibre书库。为此，右键单击操作界面左侧“标签浏览器”或右侧操作界面“图书 详细信息”面板中的作者姓名，然后选择操作界面“创建笔记”或“编辑笔记”。

一个简单的弹出窗口将允许您使用基本格式和支持的链接和图像输入笔记。一旦为作者创建了笔记，就可以通过点击作者姓名旁边的小铅笔图标，从操作界面的“书籍 详细信息”面板轻松查看。

通过按下组合键“Ctrl+Shift+N”或通过图形用户界面“首选项->工具栏和菜单”将其添加到工具栏，您可以使用图形用户界面“浏览笔记”工具搜索您的库中的所有笔记。

快速过滤

有时，您希望选择一本书，并快速获得某个类别（作者、标签、出版商、系列等）中与当前所选书籍具有相同值的书籍列表，但不更改书库的当前视图。您可以使用“快速预览”执行此操作。“快速预览”打开图书列表中的第二个窗口或面板，显示与感兴趣的值匹配的图书列表。例如，假设您想查看当前所选书籍的一个或多个作者的所有书籍的列表。在您感兴趣的作者单元格中单击，然后按“Q”键，或者单击calibre窗口的界面“布局”部分中的“快速预览”图标。将打开一个窗口或面板，左边是该书的所有作者，右边是选定作者的所有书籍。

一些快速过滤的用途的例子：快速看到：

- 将一些标签应用于当前选定的书籍，

- 与当前书籍在相同丛书的其他书籍；

- 与当前书籍在某自定义列有相同值的其他书籍；

- 与当前书籍有一位相同作者的书籍；

- 在某一自定义列共享值的其他书籍。

快速过滤的信息显示的位置可以有两种选择：

1. 可以选择“不停靠”：在 calibre 窗口之上，将保持开启直到你关闭它。

2. 可以选择“停靠”：作为 calibre 主窗口书籍列表部分的一个面板。

你可以使用“停靠/取消停靠”切换窗口至停靠或取消停靠。

快速过滤面板可以一直保持开启，这种情况下它会跟随书籍列表的变动。例如，如果你在 calibre 书库视图中对一本书籍点击了一个分类列（标签、丛书、出版社、作者等），快速过滤窗口的内容将改变，以显示选中书籍在该分类中的所有值（即该书籍的标签）。选中列表中的第一个项目，快速过滤窗口就会在右边的面板显示你的书库中所有使用此值的书籍。在左面板点击不同值以查看有不同值的书籍。

双击“快速查看”窗口中的一本书，即可在图书馆视图中选择该书。 这还将更改“快速查看”窗口（左侧面板）中显示的项目，以显示新选择的书中的项目。

“Shift- ” 或“Ctrl- ”双击“快速浏览”窗口中的一本书，在calibre窗口中打开该书的编辑元数据对话框。当您关闭编辑元数据对话框时，编辑后的书籍将被快速查看。

你可以通过悬停在某一列的标题并查看该标题的工具提示，看到该列是否可以被快速过滤。你还可以通过右键点击列标题查看“快速过滤”选项是否出现在菜单来得知，这种情况下选择那个“快速过滤”选项与在当前单元格按“Q”的效果相同。

选项（在“首选项->界面外观->快速过滤”）：

- 限于(或不限于)当前的虚拟书库。如果勾选，则快速查看仅显示当前虚拟图书馆中的图书。默认值：限于虚拟书库

- 使用光标键更改图书列表上的列时，更改“快速查看”窗口的内容。默认值：不跟随光标键所做的更改

- 双击“快速查看”窗口中的单元格时，更改“快速查看”列。否则，本书将被更改，而被检查的栏目则不会被更改。默认值：更改列

- 在“快速查看”面板中按回车键时，将“快速查看”列更改为当前列。否则，本书将被更改，而被检查的栏目则不会被更改。默认值：更改列

- 选择在“快速查看”窗口/面板中显示的列。

任务

“任务”面板显示当前正在运行的工作数。任务是在单独进程中运行的活动。它们包括转换电子书和与你的阅读设备对话。您可以单击“任务”面板以访问作业列表。任务完成后，您可以通过双击列表中的该工作查看详细日志。这对于调试可能未成功完成的任务非常有用。

键盘快捷键

Calibre有几个键盘快捷键，可以节省您的时间和鼠标移动。这些快捷方式在图书列表视图中处于活动状态(当您不在编辑特定图书的详细信息时)，并且它们中的大多数都会影响您所选择的书目。Calibre电子书阅读器“有自己的快捷键<viewer_shortcuts>” ，可在阅读器 “首选项”中自定义。

备注

注：除非特别注明，否则calibre键盘快捷键不需要修改键（命令、选项、控制等）。您只需按字母键，例如e即可编辑。

Calibre主程序的键盘快捷键

键盘快捷键

操作

:键盘:`F2(MacOS下按Enter键)`

编辑图书列表中当前选定字段的元数据。

“A”

添加书籍

“Shift+A”

添加格式到选定书籍

“C”

转换选定书籍

“D”

发送到设备

“Del”

移除选定书籍

“E”

编辑选定书籍的元数据

“G”

获取书籍

“I”

显示书籍细节

“K”

编辑目录(TOC)

“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+”

重启 Calibre

“Ctrl+Shift+R”

以调试模式重启calibre

“Shift+Ctrl+E”

添加空白书籍到 Calibre

“Ctrl+M”

切换所选图书的标记/未标记状态

“Ctrl+/` 或“Ctrl+Alt+F”

打开弹出窗口以搜索书库中所有图书的全文

“Q”

通过打开“快速查看”弹出窗口，查看相关系列/标签等中的书籍。

“Shift+Q”

聚焦打开的快速查看面板

“Shift+S”

在“快速查看”面板执行搜索

“F5”

重新应用当前排序

“Ctrl+Q”

退出 Calibre

“X”

切换图书列表的自动滚动

“Ctrl+Alt+Shift+F”

将显示的图书限制为当前在“标签浏览器”中显示的图书类别中的图书

“B”

浏览在calibre阅读器中为书库中的所有图书制作的标注（突出显示和书签）

“Ctrl+Shift+N”

浏览与作者/系列/标签/等相关的备注。

“Alt+Shift+L”

在宽大和紧凑视图之间切换布局

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 添加你喜欢的新闻站点

添加你喜欢的新闻站点
caliber有一个强大，灵活且易于使用的框架，用于从Internet下载新闻并将其转换为电子书。 以下将通过示例向您展示如何从各种网站获取新闻。

要了解如何使用框架，请按照下面列出的顺序执行示例：

- 完全自动获取

- calibre博客

- bbc.co.uk

- 自定义获取过程

- 使用 bbc.co.uk 的印刷版

- 替换文章样式

- 数据交叉分析

- 现实生活中的例子

- 开发新新闻源的技巧

- 以后阅读

- API文档

完全自动获取

如果你的新闻来源足够简单，Calibre很可能能够完全自动获取它，你所需要做的就是提供URL。Calibre收集下载新闻源到`Receipe`所需的所有信息。为了告诉Calibre关于一个新闻源的信息，你必须为它创建一个“Receipe”。让我们来看一些例子：

calibre博客
calibre 博客是一个帖子博客，以简单易懂的方式为新 calibre 用户描述许多有用的 calibre 功能。 为了将此博客下载成电子书，我们依赖博客的“RSS”提要：

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

我通过查看博客页面底部的“订阅”并选择“Posts->Atom”获得了 RSS URL。 要使 calibre 下载提要并将其转换为电子书，您应该右键单击“获取新闻”按钮，然后单击“添加自定义新闻源”菜单项，然后单击“新新闻源”按钮。 应该会打开一个类似于下图所示的对话框。

首先在“新闻源标题”字段中输入“Calibre Blog”。 这将是根据上述提要中的文章创建的电子书的标题。

接下来的两个字段（“最旧的文章”和“最大文章数”）允许您控制应该从每个提要下载多少文章，并且它们非常不言自明。

要将提要添加到新闻源中，请输入提要标题和提要 URL，然后单击“添加提要”按钮。 添加提要后，只需单击“保存”按钮即可完成！ 关闭对话框。

要测试新的“新闻源”，请单击“获取新闻”按钮，然后在“自定义新闻源”子菜单中单击“calibre Blog”。 几分钟后，新下载的博客文章电子书将出现在主书库视图中（如果您连接了阅读器，它将被放入阅读器而不是书库中）。 选择它并点击“查看”按钮即可阅读！

这种方法效果如此之好、花费如此之少的原因是博客提供了“完整内容”“RSS”提要，即文章内容嵌入在提要本身中。 对于大多数以这种方式提供新闻的新闻源，通过“完整内容”提要，您不需要更多的努力将它们转换为电子书。 现在我们将看看不提供完整内容提要的新闻源。 在此类提要中，完整的文章是一个网页，并且提要仅包含指向该网页的链接以及文章的简短摘要。

bbc.co.uk

让我们尝试一下来自 The 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_blog”中概述的过程为 The BBC 创建新闻源（使用上面的提要）。 查看下载的电子书，我们发现 calibre 做得工作非常出色，它只从每篇文章的网页中提取您关心的内容。 然而，提取过程并不完美。 有时，它会留下不需要的内容，例如菜单和导航辅助工具，或者删除本应保留的内容，例如文章标题。 为了实现完美的内容提取，我们需要自定义获取过程，如下一节所述。

自定义获取过程

当您想要完善下载过程或从特别复杂的网站下载内容时，您可以利用“recipe”框架的所有功能和灵活性。 为此，在“添加自定义新闻源”对话框中，只需单击“切换到高级模式”按钮即可。

最简单且通常最有效的定制是使用在线文章的印刷版。 印刷版通常没有那么繁琐，并且可以更顺利地转换为电子书。 让我们尝试使用 The BBC 文章的印刷版。

使用 bbc.co.uk 的印刷版

第一步是查看我们之前从“bbc”下载的电子书。 在电子书中每篇文章的末尾都有一个小简介，告诉您该文章是从哪里下载的。 将该 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

所以看起来要获取打印版本，我们需要在每篇文章的 URL 前加上以下前缀：

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

现在，在“自定义新闻源”对话框的“高级模式”中，您应该看到类似的内容（请记住在切换到高级模式之前选择 The BBC 菜谱）：

您可以看到“基本模式”中的字段已以简单的方式转换为 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>”_，请参阅“教程 <https://docs.python.org/tutorial/>”_。

现在，单击“添加/更新新闻源”按钮，您的更改将被保存。 重新下载电子书。 你应该有一本改进了很多的电子书。 新版本的问题之一是印刷版网页上的字体太小。 当转换为电子书时，此问题会自动修复，但即使在修复过程之后，菜单和导航栏的字体大小相对于文章文本也变得太大。 为了解决这个问题，我们将在下一节中进行更多自定义。

替换文章样式

在上一节中，我们看到 The BBC 印刷版文章的字体太小。 在大多数网站包括 BBC 中，此字体大小是通过“CSS”样式表设置的。 我们可以通过添加以下行来禁用此类样式表的获取：

no_stylesheets = True

新闻源现在看起来像：

新版本看起来还不错。 如果您是完美主义者，您将需要阅读下一节，其中涉及实际修改下载的内容。

数据交叉分析

在操作下载内容方面，calibre 包含非常强大且灵活的功能。 为了展示其中的一些，让我们再次看看我们的老朋友“BBC <bbc1>”新闻源。 查看几篇文章（打印版）的源代码（“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 }'

通过这些添加，我们的配方已成为“生产质量”。

当谈到calibre的能力时，这个“秘诀”只探索了冰山一角。 为了探索 calibre 的更多功能，我们将在下一节中研究一个更复杂的现实生活示例。

现实生活中的例子

一个相当复杂的现实生活示例，公开了“BasicNewsRecipe”的更多“API”，是“纽约时报”的“recipe”

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'))

我们在这个“新闻源”中看到了几个新功能。 首先，我们有:

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

这会将创建的电子书首页上的显示时间设置为“日，日_数字月，年”格式。 请参阅“timefmt <calibre.web.feeds.news.BasicNewsRecipe.timefmt>”。

然后我们看到一组指令来清理下载的“HTML”：

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

这些会删除第一个“<h1>”标签之前的所有内容以及第一个 id 为“footer”的标签之后的所有内容。 请参阅 remove_tags <calibre.web.feeds.news.BasicNewsRecipe.remove_tags>、remove_tags_before <calibre.web.feeds.news.BasicNewsRecipe.remove_tags_before>、`remove_tags_after <calibre.web.feeds.news.BasicNewsRecipe.remove_tags_after> `。

下一个有趣的功能是：

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

needs_subscription = True 告诉 calibre 该菜谱需要用户名和密码才能访问内容。 这会导致每当您尝试使用此配方时，calibre 都会要求输入用户名和密码。calibre.web.feeds.news.BasicNewsRecipe.get_browser 中的代码实际上登录了《纽约时报》网站。 登录后，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 并获取 todays 报纸中出现的文章列表。 虽然比简单地使用“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 页面执行任意转换。 这里它用于绕过纽约时报在每篇文章之前向您展示的广告。

开发新新闻源的技巧

开发新新闻源的最佳方法是使用命令行界面。 使用您最喜欢的 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”选项中指定的文件夹。

下载完成后，您可以通过在浏览器中打开文件 `debug/input/index.html`来查看下载的“HTML”。 一旦您对下载和预处理正确进行感到满意，您就可以生成不同格式的电子书，如下所示：

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

如果您对自己的新闻源感到满意，并且您认为有足够的需求来证明将其包含在内置新闻源集中，请将您的新闻源发布在“calibre 新闻源论坛”<https://www.mobileread.com/forums/forumdisplay.php?f=228>`_ 与其他 calibre 用户分享。

备注

在 macOS 上，命令行工具位于 calibre 包内，例如，如果您在“/Applications”中安装了 calibre，则命令行工具位于“/Applications/calibre.app/Contents/MacOS/”中。

参见

“生成/en/电子书转换”

所有电子书转换的命令行界面。

以后阅读

要了解有关使用“BasicNewsRecipe”中提供的某些工具编写高级食谱的更多信息，您应该查阅以下来源：

“API文档<news_recipe>”

“BasicNewsRecipe” 类及其所有重要方法和字段的文档。

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

``基本新闻源``的源代码

内置新闻源 [https://github.com/kovidgoyal/calibre/tree/master/recipes]

calibre自带的内置新闻源的源代码

calibre 新闻源论坛 [https://www.mobileread.com/forums/forumdisplay.php?f=228]

许多知识渊博的新闻源作家都在这里闲逛。

API文档
清单的API文档BasicNewsRecipe

BasicNewsRecipe.abort_article()
BasicNewsRecipe.abort_recipe_processing()
BasicNewsRecipe.add_toc_thumbnail()
BasicNewsRecipe.adeify_images()
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.image_url_processor()
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.print_version()
BasicNewsRecipe.publication_date()
BasicNewsRecipe.skip_ad_pages()
BasicNewsRecipe.sort_index_by()
BasicNewsRecipe.tag_to_string()
BasicNewsRecipe.articles_are_obfuscated
BasicNewsRecipe.auto_cleanup
BasicNewsRecipe.auto_cleanup_keep
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.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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 添加你喜欢的新闻站点 »
- 清单的API文档

清单的API文档

编写清单的API由”BasicNewsRecipe”定义
class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[源代码]

包含所有新闻源所需逻辑的基类。 通过逐渐覆盖此类中的更多功能，您可以逐渐制作更加定制/强大的新闻源。 有关创建新闻源的教程介绍，请参阅“新闻”。

abort_article(msg=None)[源代码]

在任何预处理方法中调用此方法可中止当前文章的下载。 有助于跳过包含不当内容的文章，例如纯视频文章。

abort_recipe_processing(msg)[源代码]

导致新闻源下载系统中止此新闻源的下载，并向用户显示一条简单的反馈消息。

add_toc_thumbnail(article, src)[源代码]

从 populate_article_metadata 调用此方法，并使用文章中 <img>标记的 src 属性，该标记适合用作代表目录中文章的缩略图。 缩略图是否实际使用取决于设备（目前仅由 Kindle 使用）。 注意，引用的图片必须是已成功下载的图片，否则会被忽略。

classmethod adeify_images(soup)[源代码]

如果您的新闻在转换为 EPUB 后在 Adobe Digital Editions 中查看时出现图像问题，请从“postprocess_html”中调用此方法。

canonicalize_internal_url(url, is_link=True)[源代码]

返回一组“url”的规范表示。 默认实现仅使用服务器主机名和 URL 路径，忽略任何查询参数、片段等。规范表示在该新闻源的所有 URL 中必须是唯一的。 如果不是，则内部链接可能无法正确解析。

参数:

is_link – 如果 URL 来自 HTML 文件中的内部链接，则为 True。 如果 URL 是用于下载文章的 URL，则为 False。

cleanup()[源代码]

下载完所有文章后调用。 用它来进行任何清理，例如注销订阅网站等。

clone_browser(br)[源代码]

克隆浏览器br。 克隆浏览器用于多线程下载，因为 mechanize 不是线程安全的。 默认克隆例程应该捕获大多数浏览器自定义，但如果您在新闻源中做了一些奇怪的事情，则应该在新闻源中覆盖此方法并手动克隆。

默认情况下，克隆的浏览器实例使用相同的线程安全 CookieJar，除非您自定义了 cookie 处理。

default_cover(cover_file)[源代码]

为没有封面的新闻源创建通用封面

download()[源代码]

下载并预处理本新闻源中提要中的所有文章。 该方法只能在特定的 Recipe 实例上调用一次。 多次调用它会导致未定义的行为。 index.html 的路径

extract_readable_article(html, url)[源代码]

从“html”中提取主要文章内容，进行清理并以（article_html，extracted_title）元组形式返回。 基于Arc90原创的可读性算法。

get_article_url(article)[源代码]

在子类中重写以自定义指向每篇文章内容的“URL”的提取。 返回文章网址。 它通过“article”来调用，“article”是一个表示来自提要的已解析文章的对象。 请参阅“feedparser <https://pythonhosted.org/feedparser/>”_。 默认情况下，它会查找原始链接（通过 FeedBurner 或 Pheedo 等服务联合供稿），如果找到，则返回该链接，否则返回“https://pythonhosted.org/feedparser/reference-entry-link.html>`_。

get_browser(*args, **kwargs)[源代码]

返回用于从网络获取文档的浏览器实例。 默认情况下，它返回一个 mechanize [https://mechanize.readthedocs.io/en/latest/] 浏览器实例，该实例支持 cookie、忽略 robots.txt、处理刷新并具有 mozilla firefox 用户代理。

如果您的新闻源要求您先登录，请在您的子类中重写此方法。 例如，《纽约时报》新闻源中使用以下代码来登录以获得完全访问权限：

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

get_cover_url()[源代码]

返回此问题封面图片的“URL”或“None”。 默认情况下，它返回成员“self.cover_url”的值，通常为“None”。 如果您希望您的新闻源下载电子书的封面，请在子类中重写此方法，或者在调用此方法之前设置成员变量“self.cover_url”。

get_extra_css()[源代码]

默认情况下返回“self.extra_css”。 如果您想以编程方式生成 extra_css，请覆盖。

get_feeds()[源代码]

返回要为此配置文件获取的“RSS”提要列表。 列表中的每个元素必须是 (title, url) 形式的 2 元素元组。 如果 title 为 None 或空字符串，则使用提要中的标题。 如果您的新闻源需要进行一些处理来找出要下载的提要列表，则此方法非常有用。 如果是这样，请在您的子类中重写。

get_masthead_title()[源代码]

在子类中重写以使用新闻源标题以外的内容

get_masthead_url()[源代码]

返回此问题的标头图片的“URL”或“无”。 默认情况下，它返回成员“self.masthead_url”的值，通常为“None”。 如果您希望您的新闻源下载电子书的标头，请在子类中重写此方法，或者在调用此方法之前设置成员变量 self.masthead_url 。 标头图像用于 Kindle MOBI 文件中。

get_obfuscated_article(url)[源代码]

如果您设置了“articles_are_obfuscated”，则每个文章 URL 都会调用此方法。 它应该返回文件系统上包含文章 HTML 的文件的路径。 该文件由递归 HTML 获取引擎处理，因此它可以包含指向 Web 上的页面/图像的链接。 或者，您可以返回以下形式的字典：{‘data’: 1, ‘url’: 2}。 这避免了需要创建临时文件。 如果文章的有效 URL 与传递到此方法的 URL 不同，例如由于重定向，则字典中的“url”键很有用。 如果 URL 不变，则可以省略。

此方法通常适用于试图阻止自动访问文章内容的网站。

get_url_specific_delay(url)[源代码]

返回下载此 URL 之前的延迟，以秒为单位。 如果您想以编程方式确定指定 URL 的延迟，请在子类中重写此方法，默认情况下为您不想影响的 URL 返回 self.delay。

返回:

浮点数，以秒为单位的延迟。

classmethod image_url_processor(baseurl, url)[源代码]

对图像 url 执行一些处理（可能消除动态生成的图像的大小限制等）并返回处理后的 URL。 返回 None 或空字符串以跳过获取图像。

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”使用。

它必须返回一个列表。 列表中的每个元素必须是形式为``(‘提要标题’，文章列表)`` 的 2 元素元组。

每个文章列表必须包含以下形式的字典：

{
'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”的方法。 此外，您可以为文章的作者添加“作者”。

如果您想出于某种原因中止处理并让 calibre 向用户显示一条简单的消息而不是错误，请调用“abort_recipe_processing”。

populate_article_metadata(article, soup, first)[源代码]

下载属于文章的每个 HTML 页面时调用。 旨在用于获取文章元数据，如作者/摘要/等。 来自解析的 HTML（汤）。

参数:

- article – calibre.web.feeds.Article 类的对象。 如果更改摘要，请记住同时更改 text_summary

- soup – 属于本文的已解析 HTML

- first – 当且仅当解析的 HTML 是文章的第一页时为 true。

postprocess_book(oeb, opts, log)[源代码]

对已解析的下载电子书运行任何所需的后处理。

参数:

- oeb – 一个 OEBBook 对象

- opts – 转换选项

postprocess_html(soup, first_fetch)[源代码]

在解析链接和图像后，使用每个下载的 HTML 文件的源调用此方法。 它可用于对 HTML 进行任意强大的后处理。 处理后它应该返回`soup`。

参数:

- soup – 一个 BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] 实例，包含下载的 HTML。

- first_fetch – 如果这是文章的第一页，则为 true。

preprocess_html(soup)[源代码]

在解析链接和图像之前，使用每个下载的“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 是一个 unicode 字符串，表示从网络下载的原始 HTML。 url 是下载 HTML 的 URL。

请注意，此方法在 preprocess_regexps *之前*起作用。

此方法必须将处理后的 raw_html 作为 unicode 对象返回。

classmethod print_version(url)[源代码]

获取指向包含文章内容的网页的“url”，并返回指向文章打印版本的“URL”。 默认情况下不执行任何操作。 例如：

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

publication_date()[源代码]

使用此方法设置此问题的发布日期。 默认为下载时刻。 必须返回一个“datetime.datetime”对象。

skip_ad_pages(soup)[源代码]

在应用任何清理属性（如remove_tags、keep_only_tags）之前，使用每个下载的 HTML 文件的源调用此方法。 请注意，preprocess_regexps 已被应用。 它的目的是允许菜谱跳过广告页面。 如果汤代表广告页面，则返回真实页面的 HTML。 否则返回 None。

soup：一个 BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] 实例，包含下载的 HTML。

sort_index_by(index, weights)[源代码]

根据“权重”对“索引”中的标题进行排序的便捷方法。 index 已就地排序。 返回“索引”。

index: 标题列表。

weights：将权重映射到标题的字典。 如果索引中的任何标题不在权重中，则假定它们的权重为 0。

classmethod tag_to_string(tag, use_alt=True, normalize_whitespace=True)[源代码]

获取 BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] Tag 并从中递归提取文本的便捷方法，包括任何 CDATA 部分和 alt 标签属性。 返回一个可能为空的 Unicode 字符串。

use_alt：如果为`True`，则尝试对没有任何文本内容的标签使用 alt 属性
tag: BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] 标签

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"

center_navbar = True

如果 True 则导航栏居中对齐，否则左对齐

compress_news_images = False

将其设置为 False 可忽略所有缩放和压缩参数并传递图像而不进行修改。 如果 True 和其他压缩参数保留默认值，则 JPEG 图像将缩放以适合输出配置文件设置的屏幕尺寸，并压缩至最大尺寸 (w * h)/16，其中 w x h 是缩放后的图像尺寸 。

compress_news_images_auto_size = 16

自动压缩 JPEG 图像时使用的系数。 如果设置为 None，则禁用自动压缩。 否则，如果可能的话，通过降低质量级别，图像的大小将减小到 (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

连续下载之间的默认延迟，以秒为单位。 该参数可以是浮点数以指示更精确的时间。 请参阅“get_url_specific_delay”来实现每个 URL 的延迟。

description = ''

几行描述了此新闻源下载的内容。 这将主要用在显示新闻源列表的 GUI 中。

encoding = None

为字符集规范不正确的站点指定覆盖编码。 最常见的是指定“latin1”并使用“cp1252”。 如果没有，请尝试检测编码。 如果它是可调用的，则使用两个参数调用可调用的：新闻源对象和要解码的源。 它必须返回解码后的源。

extra_css = None

指定应添加到下载的“HTML”文件中的任何额外“CSS”。 它将被插入到“<style>”标签中，就在结束“</head>”标签之前，从而覆盖所有“CSS”，除了在各个“HTML”标签上使用 style 属性声明的内容之外。 请注意，如果您想以编程方式生成 extra_css，请重写“get_extra_css()”方法。 例如：

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

feeds = None

要下载的提要列表。 可以是 [url1, url2, ...] 或 [('title1', url1), ('title2', url2),...]

filter_regexps = []

确定要忽略哪些链接的正则表达式列表。 如果为空则被忽略。 仅当未实现 is_link_wanted 时使用。 例如：

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

将删除其中包含“ads.doubleclick.net”的所有网址。

仅应定义“BasicNewsRecipe.match_regexps”或“BasicNewsRecipe.filter_regexps”之一。

handle_gzip = True

如果您不想使用 gzip 传输，请设置为 False。 请注意，一些旧服务器在使用 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”属性为“内容”或“标题”的标签。

language = 'und'

新闻所使用的语言。必须是 ISO-639 代码，长度为两个或三个字符

masthead_url = None

默认情况下，calibre 将使用默认图像作为标头（仅限 Kindle）。 在您的配方中覆盖此设置以提供用作标头的 URL。

match_regexps = []

确定要跟踪哪些链接的正则表达式列表。 如果为空，则被忽略。 仅当未实现 is_link_wanted 时使用。 例如：

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

将匹配所有包含“page=some number”的 URL。

仅应定义“BasicNewsRecipe.match_regexps”或“BasicNewsRecipe.filter_regexps”之一。

max_articles_per_feed = 100

从每个提要下载的最大文章数。 这主要对于没有文章日期的提要有用。 对于大多数提要，您应该使用“BasicNewsRecipe.oldest_article”

needs_subscription = False

如果为 True，GUI 将要求用户提供下载时使用的用户名和密码。 如果设置为“可选”，则用户名和密码的使用将变为可选

no_stylesheets = False

方便的标志，用于禁用样式表加载的网站，这些网站的样式表过于复杂，不适合转换为电子书格式。 如果未下载和处理 True 样式表

oldest_article = 7.0

从此新闻源下载最旧的文章。 几天之内。

preprocess_regexps = []

要在下载的“HTML”上运行的“regexp”替换规则列表。 列表的每个元素应该是一个二元素元组。 元组的第一个元素应该是已编译的正则表达式，第二个元素应该是可调用的，它接受单个匹配对象并返回一个字符串来替换匹配项。 例如：

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

将删除从 <!–Articleendshere–> 到 </body> 的所有内容。

publication_type = 'unknown'

出版物类型 设置为报纸、杂志或博客。 如果设置为 None，则不会将任何发布类型元数据写入 opf 文件。

recipe_disabled = None

设置为非空字符串以禁用此新闻源。 该字符串将用作禁用消息

recursions = 0

文章网页上可跟踪的链接级别数

remove_attributes = []

要从所有标签中删除的属性列表。 例如：

remove_attributes = ['style', 'font']

remove_empty_feeds = False

如果为 True，则从输出中删除空提要。 如果在子类中重写了 parse_index，则此选项无效。 它仅适用于使用“feeds”或“get_feeds”返回提要列表的配方。 如果您使用ignore_duplicate_articles选项，它也会被使用。

remove_javascript = True

方便的标记，用于从下载的 HTML 中去除所有 JavaScript 标签

remove_tags = []

要删除的标签列表。 指定的标签将从下载的 HTML 中删除。 标签被指定为以下形式的字典：

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

所有键都是可选的。 有关搜索条件的完整说明，请参阅 Beautiful Soup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree] 一个常见示例：

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

这将从下载的“HTML”中删除所有“<div class=”advert”>”标签及其所有子标签。

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，则下载文章中指向其他下载文章的链接将更改为指向该文章的下载副本，而不是其原始 Web URL。 如果将此设置为 True，您可能还需要实现“canonicalize_internal_url”以使用特定网站的 URL 方案。

reverse_article_order = False

颠倒每个提要中文章的顺序

scale_news_images = None

图像缩放到的最大尺寸（宽，高）。 如果scale_news_images_to_device为True，则设置为由输出配置文件设置的设备屏幕尺寸，除非没有配置文件集，在这种情况下，它将保留为已分配的任何值（默认为“无”）。

scale_news_images_to_device = True

重新缩放图像以适合输出配置文件设置的设备屏幕尺寸。 如果未设置输出配置文件，则忽略。

simultaneous_downloads = 5

同时下载的数量。 如果服务器挑剔，则设置为 1。 如果 BasicNewsRecipe.delay > 0，则自动减少到 1

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    '

用于设置模板样式的 CSS，即导航栏和目录。 您应该在新闻源中使用“extra_css”来自定义外观，而不是覆盖此变量。

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

第一页上显示的日期的格式字符串。 默认情况下：Day_Name、Day_Number Month_Name Year

timeout = 120.0

从服务器获取文件的超时时间，以秒为单位

title = '未知新闻来源'

用于电子书的标题

use_embedded_content = None

通常，我们会尝试根据嵌入内容的长度来猜测提要是否嵌入了完整的文章。 如果“无”，则使用默认猜测。 如果为“True”，则我们始终假设提要具有嵌入内容，如果为“False”，我们始终假设提要没有嵌入内容。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 电子书阅读器

电子书阅读器
calibre包含内置的电子书阅读器，即可阅读所有主流格式。其阅读器高度定制化，包含许多高级功能。

- 启动电子书阅读器

- 浏览电子书

- 突出显示文本

- 朗读

- 搜索文本

- 使用键盘快捷键跟踪链接

- 自定义阅读视图的外观

- 字典查询

- 文字和图像复制中

- 放大图像

- 键盘快捷键

- 不可重排的内容

- 设计您的图书，使其与calibre阅读器配合使用

启动电子书阅读器

您可以通过选择图书并按“查看”按钮来查看calibre书库中的任何书籍。这将在电子书阅读器中打开图书。您也可以从Windows的“开始”菜单中自行启动电子书阅读器。在macOS上，你可以把它固定在dock上，然后从那里启动它。在Linux上，您可以在桌面菜单中使用它的启动器，或者运行命令：“ebook-viewer”。

浏览电子书

您可以通过以下任一方式在书中“翻页”：

- 用鼠标单击左页边或右页边或页面

- 按下“空格键”、“向上翻页”、“向下翻页”或箭头键

- 在触摸屏上点击文本或左右滑动

您可以通过以下任一方式访问阅读器控件：

- 在文本上单击右键

- 按下“Esc”或者“菜单”键

- 在触摸屏上顶部1/3的屏幕区域轻触

阅读器有两种模式，“翻页”和“卷轴”。在翻页模式下，图书内容显示为页面，类似于纸质图书。在卷轴模式下，文本连续呈现，就像在Web浏览器中一样。您可以使用阅读器在它们之间切换：“首选项”下“页面布局”或同时按下“Ctrl+M”键。

书签

当你在一本图书中间并关闭电子书阅读器时，它将记住您停止阅读的位置，并在下次打开该书时返回该位置。您还可以使用电子书阅读器控件中的“书签”按钮或按“ctrl+b”在书中设置书签。当阅读EPUB格式的书籍时，这些书签实际上保存在EPUB文件中。您可以添加书签，然后将文件发送给朋友。当他们打开文件，他们将能够看到你的书签。您可以在阅读器“首选项”的图形界面中`其它`选项关闭此行为。

目录

如果您正在阅读的图书定义了目录，您可以通过按下“目录”按钮进行访问。这将调出书中各节的目录列表。您可以单击其中任何一个以跳转到图书的该部分。

按位置导航

电子书与纸质书不同，没有页面的概念。您可以使用阅读器控件中的“转到->位置”功能来引用电子书中的精确位置。

当你与朋友讨论或在其他作品中引用它时，你可以使用这个位置信息来明确地引用书中的某些部分。您可以在查看器控件的“转到->位置”下输入这些位置。

你可以复制URL到剪贴板并粘贴到其他程序或文档中。单击此URL将在当前位置的calible电子书阅读器中打开该书。

如果您单击电子书内部的链接，将您带到书的不同部分，例如尾注，则可以使用阅读器控件左上角的“返回”和“向前”按钮。这些按钮的行为与web浏览器中的按钮类似。

参考模式

Calibre还有一个非常方便的功能：“引用模式”。您可以通过单击阅读器中的“引用模式”按钮将其打开。执行此操作后，每个段落将在开头显示一个唯一的编号，该编号由章节编号和段落编号组成。

当你和朋友讨论或在其他作品中提到它时，你可以用这个数字明确地指代书中的某些部分。您可以在“转到函数”中输入这些数字，以浏览到特定的参考位置。

突出显示文本

当您在阅读器中选择文本时，所选内容旁边会出现一个小弹出条。您可以单击该栏中的亮点按钮来创建亮点。您可以添加注释并更改亮点的颜色。在触摸屏上，长时间点击一个单词将其选中并显示弹出条。进入突出显示模式后，您可以使用触摸屏友好的选择控制柄更改选择的文本。将控制柄拖动到顶部或底部边缘方向可以在选择时滚动。您还可以按下“Shift+click”或“右键单击”扩展选择范围，这对于多页选择特别有用。

您可以使用阅读器“浏览突出显示”按钮显示一个单独的面板，其中包含图书中所有突出显示的列表。

您可以通过右键单击“视图”按钮并选择“浏览注释”来浏览整个Calibre书库中的*所有突出显示*。

最后，如果您在浏览器查看器中使用Calibre内容服务器，您可以通过转到查看器首选项中的“首选项->其他杂项”并输入要与之同步的内容服务器查看器的用户名，让阅读器与浏览器查看器同步其注释。使用特定值``*``与匿名用户同步。

朗读

阅读器可以朗读图书文本。要使用它，您只需单击阅读器“朗读”按钮，即可开始朗读图书文本。当前正在阅读的单词将突出显示。语音是使用用于文本到语音转换的操作系统服务从文本合成的。当”大声朗读“处于活动状态时，您可以通过点击显示栏中的齿轮图标来更改正在使用的声音。

您还可以通过在阅读器首选项中的”选中内容后的操作“下的选择栏中添加”朗读“按钮来朗读亮点段落。

备注

浏览器中对文本-语音转换的支持非常不完整且充满错误，因此图形界面中的”朗读“在浏览器内阅读器中的工作情况取决于底层浏览器对文本-语音转换的支持程度。特别是，突出显示当前单词不起作用，更改速度或声音将导致重新从头开始阅读。

备注

在Linux上，“朗读”需要安装并运行“语音分配器<https://freebsoft.org/speechd>”。

备注

在Windows上，并非所有安装的语音对用于文本到语音转换的SAPI子系统都可见。参考“使所有声音可见的说明<https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108>”

搜索文本

阅读器具有非常强大的搜索功能。 按下“Ctrl+F”键或访问阅读器控件并单击搜索。最简单的搜索形式是只搜索您在文本框中输入的任何文本。 不同形式的搜索由搜索输入下方的搜索模式框选择。可用模式包括：

1. 图形界面中“包含”- 最简单的默认模式。在搜索框中输入的文本将在任何位置进行搜索。忽略所有标点符号、重音符号和空格。例如，搜索“Pena”将匹配以下所有内容：“penal，pen a，pen.a和Peña”。如果选择“区分大小写”框，则不再忽略重音符号、空格和标点符号。

2. 图形界面选框中“全词匹配”-搜索完整词语。例如，搜索“pena”将匹配“Peña”一词，但不匹配“Criminal”一词。与上面的“包含”搜索一样，除非选中“区分大小写”框，否则将忽略重音符号和标点符号。

3. 邻近单词-搜索彼此邻近的整个单词。因此，例如，搜索“calible cool”将匹配“calibre”和“cool”这两个词在60个字符内出现的位置。要更改临近的字符数，请将新数字添加到单词列表的末尾。例如，“calible cool awesome 120”将匹配三个单词出现在120个字符内的位置。请注意，这些搜索不会忽略标点符号和重音符号。

4. “Regex” - 将搜索文本解释为*正则表达式*。要了解有关使用正则表达式的更多信息，请参阅“本教程<regexp>”.

使用键盘快捷键跟踪链接

电子书阅读器有一个“提示模式”，允许您在不使用鼠标的情况下单击文本中的链接。 按“Alt+F”键，当前屏幕中的所有链接将以数字或字母突出显示。按键盘上的字母以单击链接。按“Esc”键将中止“提示模式”，而不选择任何链接。

如果屏幕上的链接超过35个，则其中一些链接将有多个字母，在这种情况下，键入第一个和第二个，或第一个，然后按`回车‘键激活。您也可以使用`空格`键来纠正输入错误。

自定义阅读视图的外观

通过在阅读器使用按钮“字体大小”或“Ctrl++”或“Ctrl+-”或按住“Ctrl”键并使用鼠标滚轮，可以动态更改字体大小。

可以在阅读器首选项的“颜色”部分更改颜色。

您可以在阅读器首选项的“页面排版”中更改屏幕上显示的页数和页边距。

您可以通过阅读器首选项的“页眉和页脚”部分显示自定义页眉和页脚，如剩余阅读时间、当前章节标题、图书位置等。

可以通过“样式”的设置获得更高级的自定义。在这里，您可以指定要在文本下显示的背景图像，还可以指定将应用于每本书的样式表。使用它，你可以做一些比如改变段落样式，文本对齐等等的事情。有关Calibre用户使用的自定义样式表的示例，请参阅‘论坛<https://www.mobileread.com/forums/showthread.php?t=51500>`_。

字典查询

您可以通过双击或长时间点击要查找的单词，然后单击看起来像书库的查找按钮来查找当前书籍中的单词的含义。

文字和图像复制中

您可以选择文本和图像，方法是用鼠标拖动内容，然后右键单击并选择“复制”以复制到剪贴板。复制的材料可以作为纯文本和图像粘贴到另一个应用程序中。

放大图像

您可以通过双击或长按来放大在单独的窗口中以全尺寸显示图像。您也可以右键单击它并选择“查看图像”。

键盘快捷键

与其他版本一样，阅读器也有丰富的键盘快捷键。它们可以在阅读器“首选项”中自定义。默认快捷键如下所示：

Calibre阅读器的键盘快捷键

键盘快捷键

操作

“Home, Ctrl+↑，Ctrl+←”

滚动到多文件图书中当前文件的开头

“Ctrl+Home”

滚动到书籍开头

“Ctrl+End”

滚动到书籍结尾

“End, Ctrl+↓, Ctrl+→”

滚动到多文件图书中当前文件的结尾

“↑”

向后滚动，在卷轴模式下平滑地滚动，在翻页模式下全屏显示

“↓”

向前滚动，在卷轴模式下平滑地滚动，在翻页模式下全屏翻页

“←”

在卷轴模式下向左滑动一点，在翻页模式下向左翻页

“→”

在卷轴模式下向右滑动一点，在翻页模式下向右翻一页

“PageUp, Shift+Spacebar”

向后滑动全屏

“PageDown, Spacebar”

向前滑动全屏

“Ctrl+PageUp”

翻到上一章

“Ctrl+PageDown”

翻到下一章

“Alt+←”

后退

“Alt+→”

前进

“Ctrl+T”

切换目录

“Ctrl+S”

朗读

“Alt+P”

通过创建并切换到“配置文件”来快速更改设置

“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+﹣, Meta+﹣”

减小字号

“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+ →”

当前所选内容向前一个字

“Ctrl+Shift+←”

当前所选内容向后一个字

“Shift+→”

当前所选内容向前一个字符

“Shift+←”

当前所选内容向后一个字符

“Shift+↓”

当前所选内容向前一行

“Shift+Home”

扩展选择到行首

“Shift+End”

扩展选择到行尾

“Ctrl+A”

全选

“Shift+↑”

当前所选内容向后一行

“Ctrl+Shift+↓”

当前所选内容向前一个段落

“Ctrl+Shift+↑”

当前选定内容向后一个段落

“Esc, MenuKey”

显示电子书阅读器控件

“Ctrl+，, Ctrl+Esc, Meta+Esc, Meta+，”

显示电子书阅读器首选项

“Ctrl+G, ;, :”

跳转到书籍指定位置

“Ctrl+Spacebar”

切换成自动滚动

“Alt+↑”

快速自动滚动

“Alt+↓”

缓慢自动滚动

“Ctrl+I”

显示/隐藏开发者工具

“Ctrl+L”

显示/隐藏查词界面

“Ctrl+Q (Cmd+Q on macOS)”

退出

“Ctrl+P”

将书籍打印到PDF

“Ctrl+F11”

切换工具栏

“Ctrl+H”

切换亮点面板

:键盘:`Ctrl+D`

编辑此书

不可重排的内容

标签标签标签标签有些书的内容非常广泛，不能在页面边界上拆分。例如表或“<pre>”标记。在这种情况下，您应该通过按键盘“Ctrl+M”阅读此内容，将阅读器切换到“滚动模式”。或者，您也可以将以下CSS添加到阅读器首选项的“样式”部分，以强制阅读器在“<pre>”中拆分文本行

code, pre { white-space: pre-wrap }

设计您的图书，使其与calibre阅读器配合使用

Calibre阅读器将在根元素上设置``is-Calibre-viewer``类。因此，您可以编写仅适用于它的CSS规则。此外，阅读器将在``body``元素上设置以下类：

body.calibre-viewer-dark-colors

使用深色方案时设置

body.calibre-viewer-light-colors

使用浅色方案时设置

body.calibre-viewer-paginated

在翻页模式下设置

body.calibre-viewer-scrolling

处于滚动（非分页）模式时设置

“body.calibre-footnote-container”

在显示弹出式脚注时设置

最后，您可以通过`CSS变量<https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties>`使用calibre配色方案颜色。calibre阅读器在定义链接颜色的颜色主题中定义以下变量：--calibre阅读器背景颜色、--calible阅读器前端颜色```和可选的–caribre阅读器链接颜色``。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 电子书转换

电子书转换
calibre has a conversion system that is designed to be very easy to use. Normally, you just add a book to calibre, click convert and calibre will try hard to generate output that is as close as possible to the input. However, calibre accepts a very large number of input formats, not all of which are as suitable as others for conversion to e-books. In the case of such input formats, or if you just want greater control over the conversion system, calibre has a lot of options to fine tune the conversion process. Note however that calibre’s conversion system is not a substitute for a full blown e-book editor. To edit e-books, I recommend first converting them to EPUB or AZW3 using calibre and then using the Edit book feature to get them into perfect shape. You can then use the edited e-book as input for conversion into other formats in calibre.

本文档将主要考虑能在转换对话框中找到的转换设置，如下图所示。 所有这些设置也可以通过命令行界面进行转换，文档在 generated/zh-CN/ebook-convert 中。 在 Calibre 中，您可以通过将鼠标悬停在任何单独的设置上来获取帮助，然后会出现一个工具提示来描述该设置。

目录

- 介绍

- 界面外观

- 页面设置

- 智能处理

- 搜索并替换

- 结构检测

- 目录

- Using images as chapter titles when converting HTML input documents

- Using tag attributes to supply the text for entries in the Table of Contents

- How options are set/saved for conversion

- Format specific tips

介绍

对转换系统，首先要了解的是它被设计为管道。从示意图上看，它看起来像这样：

输入首先被恰当的*Input 插件*转换到 XHTML 。 HTML 接着被*转换*。在最后一步，已处理的XHTML通过适当的*Output 插件*转换为指定的输出格式。根据输入格式的不同，转换的结果可能会有很大的差异。 有些格式的转换效果要好于其他格式。 `这里<best-source-formats>`提供了最佳的转换源格式列表。

The transforms that act on the XHTML output are where all the work happens. There are various transforms, for example, to insert book metadata as a page at the start of the book, to detect chapter headings and automatically create a Table of Contents, to proportionally adjust font sizes, et cetera. It is important to remember that all the transforms act on the XHTML output by the Input plugin, not on the input file itself. So, for example, if you ask calibre to convert an RTF file to EPUB, it will first be converted to XHTML internally, the various transforms will be applied to the XHTML and then the Output plugin will create the EPUB file, automatically generating all metadata, Table of Contents, et cetera.

You can see this process in action by using the debug option . Just specify the path to a folder for the debug output. During conversion, calibre will place the XHTML generated by the various stages of the conversion pipeline in different sub-folders. The four sub-folders are:

Stages of the conversion pipeline

文件名

描述

输入

This contains the HTML output by the Input plugin. Use this to debug the Input plugin.

已解析

The result of pre-processing and converting to XHTML the output from the Input plugin. Use to debug structure detection.

结构

Post structure detection, but before CSS flattening and font size conversion. Use to debug font size conversion and CSS transforms.

已处理

Just before the e-book is passed to the Output plugin. Use to debug the Output plugin.

If you want to edit the input document a little before having calibre convert it, the best thing to do is edit the files in the input sub-folder, then zip it up, and use the ZIP file as the input format for subsequent conversions. To do this use the Edit meta information dialog to add the ZIP file as a format for the book and then, in the top left corner of the conversion dialog, select ZIP as the input format.

This document will deal mainly with the various transforms that operate on the intermediate XHTML and how to control them. At the end are some tips specific to each input/output format.

界面外观

目录

- 字体

- 文本

- 布局

- 样式

- 改变样式

- Transform HTML

This group of options controls various aspects of the look and feel of the converted e-book.

字体

One of the nicest features of the e-reading experience is the ability to easily adjust font sizes to suit individual needs and lighting conditions. calibre has sophisticated algorithms to ensure that all the books it outputs have a consistent font sizes, no matter what font sizes are specified in the input document.

The base font size of a document is the most common font size in that document, i.e., the size of the bulk of text in that document. When you specify a Base font size, calibre automatically rescales all font sizes in the document proportionately, so that the most common font size becomes the specified base font size and other font sizes are rescaled appropriately. By choosing a larger base font size, you can make the fonts in the document larger and vice versa. When you set the base font size, for best results, you should also set the font size key.

Normally, calibre will automatically choose a base font size appropriate to the output profile you have chosen (see 页面设置). However, you can override this here in case the default is not suitable for you.

The Font size key option lets you control how non-base font sizes are rescaled. The font rescaling algorithm works using a font size key, which is simply a comma-separated list of font sizes. The font size key tells calibre how many “steps” bigger or smaller a given font size should be compared to the base font size. The idea is that there should be a limited number of font sizes in a document. For example, one size for the body text, a couple of sizes for different levels of headings and a couple of sizes for super/sub scripts and footnotes. The font size key allows calibre to compartmentalize the font sizes in the input documents into separate “bins” corresponding to the different logical font sizes.

Let’s illustrate with an example. Suppose the source document we are converting was produced by someone with excellent eyesight and has a base font size of 8pt. That means the bulk of the text in the document is sized at 8pts, while headings are somewhat larger (say 10 and 12pt) and footnotes somewhat smaller at 6pt. Now if we use the following settings:

Base font size : 12pt
Font size key : 7, 8, 10, 12, 14, 16, 18, 20

The output document will have a base font size of 12pt, headings of 14 and 16pt and footnotes of 8pt. Now suppose we want to make the largest heading size stand out more and make the footnotes a little larger as well. To achieve this, the font key should be changed to:

New font size key : 7, 9, 12, 14, 18, 20, 22

The largest headings will now become 18pt, while the footnotes will become 9pt. You can play with these settings to try and figure out what would be optimum for you by using the font rescaling wizard, which can be accessed by clicking the little button next to the Font size key setting.

All the font size rescaling in the conversion can also be disabled here, if you would like to preserve the font sizes in the input document.

A related setting is Line height. Line height controls the vertical height of lines. By default, (a line height of 0), no manipulation of line heights is performed. If you specify a non-default value, line heights will be set in all locations that don’t specify their own line heights. However, this is something of a blunt weapon and should be used sparingly. If you want to adjust the line heights for some section of the input, it’s better to use the Extra CSS.

In this section you can also tell calibre to embed any referenced fonts into the book. This will allow the fonts to work on reader devices even if they are not available on the device.

文本

Text can be either justified or not. Justified text has extra spaces between words to give a smooth right margin. Some people prefer justified text, others do not. Normally, calibre will preserve the justification in the original document. If you want to override it you can use the Text justification option in this section.

You can also tell calibre to Smarten punctuation which will replace plain quotes, dashes and ellipses with their typographically correct alternatives. Note that this algorithm is not perfect so it is worth reviewing the results. The reverse, namely, Unsmarted punctuation is also available.

Finally, there is Input character encoding. Older documents sometimes don’t specify their character encoding. When converted, this can result in non-English characters or special characters like smart quotes being corrupted. calibre tries to auto-detect the character encoding of the source document, but it does not always succeed. You can force it to assume a particular character encoding by using this setting. cp1252 is a common encoding for documents produced using Windows software. You should also read 如果我需要转换的文件中含有非英文字符或者引用时该怎么办？ for more on encoding issues.

布局

Normally, paragraphs in XHTML are rendered with a blank line between them and no leading text indent. calibre has a couple of options to control this. Remove spacing between paragraphs forcefully ensure that all paragraphs have no inter paragraph spacing. It also sets the text indent to 1.5em (can be changed) to mark the start of every paragraph. Insert blank line does the opposite, guaranteeing that there is exactly one blank line between each pair of paragraphs. Both these options are very comprehensive, removing spacing, or inserting it for all paragraphs (technically <p> and <div> tags). This is so that you can just set the option and be sure that it performs as advertised, irrespective of how messy the input file is. The one exception is when the input file uses hard line breaks to implement inter-paragraph spacing.

If you want to remove the spacing between all paragraphs, except a select few, don’t use these options. Instead add the following CSS code to Extra CSS:

p, div { margin: 0pt; border: 0pt; text-indent: 1.5em }
.spacious { margin-bottom: 1em; text-indent: 0pt; }

Then, in your source document, mark the paragraphs that need spacing with class=”spacious”. If your input document is not in HTML, use the Debug option, described in the Introduction to get HTML (use the input sub-folder).

Another useful options is Linearize tables. Some badly designed documents use tables to control the layout of text on the page. When converted these documents often have text that runs off the page and other artifacts. This option will extract the content from the tables and present it in a linear fashion. Note that this option linearizes all tables, so only use it if you are sure the input document does not use tables for legitimate purposes, like presenting tabular information.

样式

The Extra CSS option allows you to specify arbitrary CSS that will be applied to all HTML files in the input. This CSS is applied with very high priority and so should override most CSS present in the input document itself. You can use this setting to fine tune the presentation/layout of your document. For example, if you want all paragraphs of class endnote to be right aligned, just add:

.endnote { text-align: right }

or if you want to change the indentation of all paragraphs:

p { text-indent: 5mm; }

Extra CSS is a very powerful option, but you do need an understanding of how CSS works to use it to its full potential. You can use the debug pipeline option described above to see what CSS is present in your input document.

A simpler option is to use Filter style information. This allows you to remove all CSS properties of the specified types from the document. For example, you can use it to remove all colors or fonts.

改变样式

This is the most powerful styling related facility. You can use it to define rules that change styles based on various conditions. For example you can use it to change all green colors to blue, or remove all bold styling from the text or color all headings a certain color, etc.

Transform HTML

Similar to transform styles, but allows you to make changes to the HTML content of the book. You can replace one tag with another, add classes or other attributes to tags based on their content, etc.

页面设置

The Page setup options are for controlling screen layout, like margins and screen sizes. There are options to setup page margins, which will be used by the output plugin, if the selected output format supports page margins. In addition, you should choose an Input profile and an output profile. Both sets of profiles basically deal with how to interpret measurements in the input/output documents, screen sizes and default font rescaling keys.

If you know that the file you are converting was intended to be used on a particular device/software platform, choose the corresponding input profile, otherwise just choose the default input profile. If you know the files you are producing are meant for a particular device type, choose the corresponding output profile. Otherwise, choose one of the Generic output profiles. If you are converting to MOBI or AZW3 then you will almost always want to choose one of the Kindle output profiles. Otherwise, your best bet for modern E-book reading devices is to choose the Generic e-ink HD output profile.

The output profile also controls the screen size. This will cause, for example, images to be auto-resized to be fit to the screen in some output formats. So choose a profile of a device that has a screen size similar to your device.

智能处理

Heuristic processing provides a variety of functions which can be used to try and detect and correct common problems in poorly formatted input documents. Use these functions if your input document suffers from poor formatting. Because these functions rely on common patterns, be aware that in some cases an option may lead to worse results, so use with care. As an example, several of these options will remove all non-breaking-space entities, or may include false positive matches relating to the function.

Enable heuristic processing

This option activates calibre’s Heuristic processing stage of the conversion pipeline. This must be enabled in order for various sub-functions to be applied

Unwrap lines

Enabling this option will cause calibre to attempt to detect and correct hard line breaks that exist within a document using punctuation clues and line length. calibre will first attempt to detect whether hard line breaks exist, if they do not appear to exist calibre will not attempt to unwrap lines. The line-unwrap factor can be reduced if you want to ‘force’ calibre to unwrap lines.

Line-unwrap factor

This option controls the algorithm calibre uses to remove hard line breaks. For example, if the value of this option is 0.4, that means calibre will remove hard line breaks from the end of lines whose lengths are less than the length of 40% of all lines in the document. If your document only has a few line breaks which need correction, then this value should be reduced to somewhere between 0.1 and 0.2.

Detect and markup unformatted chapter headings and sub headings

If your document does not have chapter headings and titles formatted differently from the rest of the text, calibre can use this option to attempt to detect them and surround them with heading tags. <h2> tags are used for chapter headings; <h3> tags are used for any titles that are detected.

This function will not create a TOC, but in many cases it will cause calibre’s default chapter detection settings to correctly detect chapters and build a TOC. Adjust the XPath under Structure detection if a TOC is not automatically created. If there are no other headings used in the document then setting “//h:h2” under Structure detection would be the easiest way to create a TOC for the document.

The inserted headings are not formatted, to apply formatting use the Extra CSS option under the Look and Feel conversion settings. For example, to center heading tags, use the following:

h2, h3 { text-align: center }

Renumber sequences of <h1> or <h2> tags

Some publishers format chapter headings using multiple <h1> or <h2> tags sequentially. calibre’s default conversion settings will cause such titles to be split into two pieces. This option will re-number the heading tags to prevent splitting.

Delete blank lines between paragraphs

This option will cause calibre to analyze blank lines included within the document. If every paragraph is interleaved with a blank line, then calibre will remove all those blank paragraphs. Sequences of multiple blank lines will be considered scene breaks and retained as a single paragraph. This option differs from the Remove paragraph spacing option under Look and Feel in that it actually modifies the HTML content, while the other option modifies the document styles. This option can also remove paragraphs which were inserted using calibre’s Insert blank line option.

Ensure scene breaks are consistently formatted

With this option calibre will attempt to detect common scene-break markers and ensure that they are center aligned. ‘Soft’ scene break markers, i.e. scene breaks only defined by extra white space, are styled to ensure that they will not be displayed in conjunction with page breaks.

Replace scene breaks

If this option is configured then calibre will replace scene break markers it finds with the replacement text specified by the user. Please note that some ornamental characters may not be supported across all reading devices.

In general you should avoid using HTML tags, calibre will discard any tags and use pre-defined markup. <hr /> tags, i.e. horizontal rules, and <img> tags are exceptions. Horizontal rules can optionally be specified with styles, if you choose to add your own style be sure to include the ‘width’ setting, otherwise the style information will be discarded. Image tags can used, but calibre does not provide the ability to add the image during conversion, this must be done after the fact using the ‘Edit book’ feature.

Example image tag (place the image within an ‘Images’ folder inside the EPUB after conversion):

<img style=”width:10%” src=”../Images/scenebreak.png” />

Example horizontal rule with styles:

<hr style=”width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;”/>

Remove unnecessary hyphens

calibre will analyze all hyphenated content in the document when this option is enabled. The document itself is used as a dictionary for analysis. This allows calibre to accurately remove hyphens for any words in the document in any language, along with made-up and obscure scientific words. The primary drawback is words appearing only a single time in the document will not be changed. Analysis happens in two passes, the first pass analyzes line endings. Lines are only unwrapped if the word exists with or without a hyphen in the document. The second pass analyzes all hyphenated words throughout the document, hyphens are removed if the word exists elsewhere in the document without a match.

Italicize common words and patterns

When enabled, calibre will look for common words and patterns that denote italics and italicize them. Examples are common text conventions such as ~word~ or phrases that should generally be italicized, e.g. latin phrases like ‘etc.’ or ‘et cetera’.

Replace entity indents with CSS indents

Some documents use a convention of defining text indents using non-breaking space entities. When this option is enabled calibre will attempt to detect this sort of formatting and convert them to a 3% text indent using CSS.

搜索并替换

These options are useful primarily for conversion of PDF documents or OCR conversions, though they can also be used to fix many document specific problems. As an example, some conversions can leaves behind page headers and footers in the text. These options use regular expressions to try and detect headers, footers, or other arbitrary text and remove or replace them. Remember that they operate on the intermediate XHTML produced by the conversion pipeline. There is a wizard to help you customize the regular expressions for your document. Click the magic wand beside the expression box, and click the ‘Test’ button after composing your search expression. Successful matches will be highlighted in Yellow.

The search works by using a Python regular expression. All matched text is simply removed from the document or replaced using the replacement pattern. The replacement pattern is optional, if left blank then text matching the search pattern will be deleted from the document. You can learn more about regular expressions and their syntax at 关于在 calibre 中使用正则表达式的所有内容.

结构检测

Structure detection involves calibre trying its best to detect structural elements in the input document, when they are not properly specified. For example, chapters, page breaks, headers, footers, etc. As you can imagine, this process varies widely from book to book. Fortunately, calibre has very powerful options to control this. With power comes complexity, but if once you take the time to learn the complexity, you will find it well worth the effort.

Chapters and page breaks
calibre has two sets of options for chapter detection and inserting page breaks. This can sometimes be slightly confusing, as by default, calibre will insert page breaks before detected chapters as well as the locations detected by the page breaks option. The reason for this is that there are often location where page breaks should be inserted that are not chapter boundaries. Also, detected chapters can be optionally inserted into the auto generated Table of Contents.

calibre uses XPath, a powerful language to allow the user to specify chapter boundaries/page breaks. XPath can seem a little daunting to use at first, fortunately, there is a XPath tutorial in the User Manual. Remember that Structure detection operates on the intermediate XHTML produced by the conversion pipeline. Use the debug option described in the 介绍 to figure out the appropriate settings for your book. There is also a button for a XPath wizard to help with the generation of simple XPath expressions.

By default, calibre uses the following expression for detecting chapters:

//*[((name()='h1' or name()='h2') and re:test(., 'chapter|book|section|part\s+', 'i')) or @class = 'chapter']

This expression is rather complex, because it tries to handle a number of common cases simultaneously. What it means is that calibre will assume chapters start at either <h1> or <h2> tags that have any of the words (chapter, book, section or part) in them or that have the class=”chapter” attribute.

A related option is Chapter mark, which allows you to control what calibre does when it detects a chapter. By default, it will insert a page break before the chapter. You can have it insert a ruled line instead of, or in addition to the page break. You can also have it do nothing.

The default setting for detecting page breaks is:

//*[name()='h1' or name()='h2']

which means that calibre will insert page breaks before every <h1> and <h2> tag by default.

备注

The default expressions may change depending on the input format you are converting.

杂项

There are a few more options in this section.

Insert metadata as page at start of book

One of the great things about calibre is that it allows you to maintain very complete metadata about all of your books, for example, a rating, tags, comments, etc. This option will create a single page with all this metadata and insert it into the converted e-book, typically just after the cover. Think of it as a way to create your own customised book jacket.

Remove first image

Sometimes, the source document you are converting includes the cover as part of the book, instead of as a separate cover. If you also specify a cover in calibre, then the converted book will have two covers. This option will simply remove the first image from the source document, thereby ensuring that the converted book has only one cover, the one specified in calibre.

目录

When the input document has a Table of Contents in its metadata, calibre will just use that. However, a number of older formats either do not support a metadata based Table of Contents, or individual documents do not have one. In these cases, the options in this section can help you automatically generate a Table of Contents in the converted e-book, based on the actual content in the input document.

备注

Using these options can be a little challenging to get exactly right. If you prefer creating/editing the Table of Contents by hand, convert to the EPUB or AZW3 formats and select the checkbox at the bottom of the Table of Contents section of the conversion dialog that says Manually fine-tune the Table of Contents after conversion. This will launch the ToC Editor tool after the conversion. It allows you to create entries in the Table of Contents by simply clicking the place in the book where you want the entry to point. You can also use the ToC Editor by itself, without doing a conversion. Go to Preferences → Interface → Toolbars and add the ToC Editor to the main toolbar. Then just select the book you want to edit and click the ToC Editor button.

The first option is Force use of auto-generated Table of Contents. By checking this option you can have calibre override any Table of Contents found in the metadata of the input document with the auto generated one.

The default way that the creation of the auto generated Table of Contents works is that, calibre will first try to add any detected chapters to the generated table of contents. You can learn how to customize the detection of chapters in the 结构检测 section above. If you do not want to include detected chapters in the generated table of contents, check the Do not add detected chapters option.

If less than the Chapter threshold number of chapters were detected, calibre will then add any hyperlinks it finds in the input document to the Table of Contents. This often works well: many input documents include a hyperlinked Table of Contents right at the start. The Number of links option can be used to control this behavior. If set to zero, no links are added. If set to a number greater than zero, at most that number of links is added.

calibre will automatically filter duplicates from the generated Table of Contents. However, if there are some additional undesirable entries, you can filter them using the TOC Filter option. This is a regular expression that will match the title of entries in the generated table of contents. Whenever a match is found, it will be removed. For example, to remove all entries titles “Next” or “Previous” use:

Next|Previous

The Level 1,2,3 TOC options allow you to create a sophisticated multi-level Table of Contents. They are XPath expressions that match tags in the intermediate XHTML produced by the conversion pipeline. See the 介绍 for how to get access to this XHTML. Also read the XPath教程, to learn how to construct XPath expressions. Next to each option is a button that launches a wizard to help with the creation of basic XPath expressions. The following simple example illustrates how to use these options.

Suppose you have an input document that results in XHTML that look like this:

<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>

Then, we set the options as:

Level 1 TOC : //h:h1
Level 2 TOC : //h:h2

This will result in an automatically generated two level Table of Contents that looks like:

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

警告

Not all output formats support a multi level Table of Contents. You should first try with EPUB output. If that works, then try your format of choice.

Using images as chapter titles when converting HTML input documents

Suppose you want to use an image as your chapter title, but still want calibre to be able to automatically generate a Table of Contents for you from the chapter titles. Use the following HTML markup to achieve this:

<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>

Set the Level 1 TOC setting to //h:h2. Then, for chapter two, calibre will take the title from the value of the title attribute on the <h2> tag, since the tag has no text.

Using tag attributes to supply the text for entries in the Table of Contents

If you have particularly long chapter titles and want shortened versions in the Table of Contents, you can use the title attribute to achieve this, for example:

<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>

Set the Level 1 TOC setting to //h:h2/@title. Then calibre will take the title from the value of the title attribute on the <h2> tags, instead of using the text inside the tag. Note the trailing /@title on the XPath expression, you can use this form to tell calibre to get the text from any attribute you like.

How options are set/saved for conversion

There are two places where conversion options can be set in calibre. The first is in Preferences->Conversion. These settings are the defaults for the conversion options. Whenever you try to convert a new book, the settings set here will be used by default.

You can also change settings in the conversion dialog for each book conversion. When you convert a book, calibre remembers the settings you used for that book, so that if you convert it again, the saved settings for the individual book will take precedence over the defaults set in Preferences. You can restore the individual settings to defaults by using the Restore defaults button in the individual book conversion dialog. You can remove the saved settings for a group of books by selecting all the books and then clicking the Edit metadata button to bring up the bulk metadata edit dialog, near the bottom of the dialog is an option to remove stored conversion settings.

When you bulk convert a set of books, settings are taken in the following order (last one wins):

- From the defaults set in Preferences->Conversion

- From the saved conversion settings for each book being converted (if any). This can be turned off by the option in the top left corner of the Bulk conversion dialog.

- From the settings set in the Bulk conversion dialog

Note that the final settings for each book in a Bulk conversion will be saved and re-used if the book is converted again. Since the highest priority in Bulk Conversion is given to the settings in the Bulk conversion dialog, these will override any book specific settings. So you should only bulk convert books together that need similar settings. The exceptions are metadata and input format specific settings. Since the Bulk conversion dialog does not have settings for these two categories, they will be taken from book specific settings (if any) or the defaults.

备注

You can see the actual settings used during any conversion by clicking the rotating icon in the lower right corner and then double clicking the individual conversion job. This will bring up a conversion log that will contain the actual settings used, near the top.

Format specific tips

Here you will find tips specific to the conversion of particular formats. Options specific to particular format, whether input or output are available in the conversion dialog under their own section, for example TXT input or EPUB output.

转换Microsoft Word文档
calibre can automatically convert .docx files created by Microsoft Word 2007 and newer. Just add the file to calibre and click convert.

备注

There is a demo .docx file [https://calibre-ebook.com/downloads/demos/demo.docx] that demonstrates the capabilities of the calibre conversion engine. Just download it and convert it to EPUB or AZW3 to see what calibre can do.

calibre will automatically generate a Table of Contents based on headings if you mark your headings with the Heading 1, Heading 2, etc. styles in Microsoft Word. Open the output e-book in the calibre E-book viewer and click the Table of Contents button to view the generated Table of Contents.

Older .doc files

For older .doc files, you can save the document as HTML with Microsoft Word and then convert the resulting HTML file with calibre. When saving as HTML, be sure to use the “Save as Web Page, Filtered” option as this will produce clean HTML that will convert well. Note that Word produces really messy HTML, converting it can take a long time, so be patient. If you have a newer version of Word available, you can directly save it as .docx as well.

Another alternative is to use the free LibreOffice. Open your .doc file in LibreOffice and save it as .docx, which can be directly converted in calibre.

转换文本文档

TXT documents have no well defined way to specify formatting like bold, italics, etc, or document structure like paragraphs, headings, sections and so on, but there are a variety of conventions commonly used. By default calibre attempts automatic detection of the correct formatting and markup based on those conventions.

TXT input supports a number of options to differentiate how paragraphs are detected.

Paragraph style: Auto

Analyzes the text file and attempts to automatically determine how paragraphs are defined. This option will generally work fine, if you achieve undesirable results try one of the manual options.

Paragraph style: Block

Assumes one or more blank lines are a paragraph boundary:

This is the first.

This is the
second paragraph.

Paragraph style: Single

Assumes that every line is a paragraph:

This is the first.
This is the second.
This is the third.

Paragraph style: Print

Assumes that every paragraph starts with an indent (either a tab or 2+ spaces). Paragraphs end when the next line that starts with an indent is reached:

This is the
first.
This is the second.

This is the
third.

Paragraph style: Unformatted

Assumes that the document has no formatting, but does use hard line breaks. Punctuation and median line length are used to attempt to re-create paragraphs.

Formatting style: Auto

Attempts to detect the type of formatting markup being used. If no markup is used then heuristic formatting will be applied.

Formatting style: Heuristic

Analyzes the document for common chapter headings, scene breaks, and italicized words and applies the appropriate HTML markup during conversion.

Formatting style: Markdown

calibre also supports running TXT input though a transformation preprocessor known as Markdown. Markdown allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables, lists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection expression to “//h:h1” is the easiest way to have a proper table of contents generated from a TXT document. You can learn more about the Markdown syntax at daringfireball [https://daringfireball.net/projects/markdown/syntax].

Formatting style: None

Applies no special formatting to the text, the document is converted to HTML with no other changes.

转换PDF文档

PDF documents are one of the worst formats to convert from. They are a fixed page size and text placement format. Meaning, it is very difficult to determine where one paragraph ends and another begins. calibre will try to unwrap paragraphs using a configurable, Line un-wrapping factor. This is a scale used to determine the length at which a line should be unwrapped. Valid values are a decimal between 0 and 1. The default is 0.45, just under the median line length. Lower this value to include more text in the unwrapping. Increase to include less. You can adjust this value in the conversion settings under PDF Input.

Also, they often have headers and footers as part of the document that will become included with the text. Use the Search and replace panel to remove headers and footers to mitigate this issue. If the headers and footers are not removed from the text it can throw off the paragraph unwrapping. To learn how to use the header and footer removal options, read 关于在 calibre 中使用正则表达式的所有内容.

PDF输入限制:

- Complex, multi-column, and image based documents are not supported.

- Extraction of vector images and tables from within the document is also not supported.

- Some PDFs use special glyphs to represent ll or ff or fi, etc. Conversion of these may or may not work depending on just how they are represented internally in the PDF.

- 链接和目录不支持

- PDFs that use embedded non-Unicode fonts to represent non-English characters will result in garbled output for those characters

- Some PDFs are made up of photographs of the page with OCRed text behind them. In such cases calibre uses the OCRed text, which can be very different from what you see when you view the PDF file

- PDFs that are used to display complex text, like right to left languages and math typesetting will not convert correctly

To re-iterate PDF is a really, really bad format to use as input. If you absolutely must use PDF, then be prepared for an output ranging anywhere from decent to unusable, depending on the input PDF.

Comic book collections

A comic book collection is a .cbc file. A .cbc file is a ZIP file that contains other CBZ/CBR files. In addition the .cbc file must contain a simple text file called comics.txt, encoded in UTF-8. The comics.txt file must contain a list of the comics files inside the .cbc file, in the form filename:title, as shown below:

one.cbz:Chapter One
two.cbz:Chapter Two
three.cbz:Chapter Three

The .cbc file will then contain:

comics.txt
one.cbz
two.cbz
three.cbz

calibre will automatically convert this .cbc file into a e-book with a Table of Contents pointing to each entry in comics.txt.

EPUB advanced formatting demo

Various advanced formatting for EPUB files is demonstrated in this demo file [https://calibre-ebook.com/downloads/demos/demo.epub]. The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts.

The source HTML it was created from is available demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. The settings used to create the EPUB from the ZIP file are:

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

Note that because this file explores the potential of EPUB, most of the advanced formatting is not going to work on readers less capable than calibre’s built-in EPUB viewer.

Convert ODT documents
calibre can directly convert ODT (OpenDocument Text) files. You should use styles to format your document and minimize the use of direct formatting. When inserting images into your document you need to anchor them to the paragraph, images anchored to a page will all end up in the front of the conversion.

To enable automatic detection of chapters, you need to mark them with the built-in styles called Heading 1, Heading 2, …, Heading 6 (Heading 1 equates to the HTML tag <h1>, Heading 2 to <h2>, etc). When you convert in calibre you can enter which style you used into the Detect chapters at box. Example:

- If you mark Chapters with style Heading 2, you have to set the ‘Detect chapters at’ box to //h:h2

- For a nested TOC with Sections marked with Heading 2 and the Chapters marked with Heading 3 you need to enter //h:h2|//h:h3. On the Convert - TOC page set the Level 1 TOC box to //h:h2 and the Level 2 TOC box to //h:h3.

Well-known document properties (Title, Keywords, Description, Creator) are recognized and calibre will use the first image (not to small, and with good aspect-ratio) as the cover image.

There is also an advanced property conversion mode, which is activated by setting the custom property opf.metadata (‘Yes or No’ type) to Yes in your ODT document (File->Properties->Custom Properties). If this property is detected by calibre, the following custom properties are recognized (opf.authors overrides document creator):

opf.titlesort
opf.authors
opf.authorsort
opf.publisher
opf.pubdate
opf.isbn
opf.language
opf.series
opf.seriesindex

In addition to this, you can specify the picture to use as the cover by naming it opf.cover (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the ‘smart’ method is used. As the cover detection might result in double covers in certain output formats, the process will remove the paragraph (only if the only content is the cover!) from the document. But this works only with the named picture!

To disable cover detection you can set the custom property opf.nocover (‘Yes or No’ type) to Yes in advanced mode.

转换为PDF

The first, most important, setting to decide on when converting to PDF is the page size. By default, calibre uses a page size of “U.S. Letter”. You can change this to another standard page size or a completely custom size in the PDF Output section of the conversion dialog. If you are generating a PDF to be used on a specific device, you can turn on the option to use the page size from the output profile instead. So if your output profile is set to Kindle, calibre will create a PDF with page size suitable for viewing on the small Kindle screen.

页头和页脚

You can insert arbitrary headers and footers on each page of the PDF by specifying header and footer templates. Templates are just snippets of HTML code that get rendered in the header and footer locations. For example, to display page numbers centered at the bottom of every page, in green, use the following footer template:

<footer><div style="margin: auto; color: green">_PAGENUM_</div></footer>

calibre will automatically replace _PAGENUM_ with the current page number. You can even put different content on even and odd pages, for example the following header template will show the title on odd pages and the author on even pages:

<header style="justify-content: flex-end">
<div class="even-page">_AUTHOR_</div>
<div class="odd-page"><i>_TITLE_</i></div>
</header>

calibre will automatically replace _TITLE_ and _AUTHOR_ with the title and author of the document being converted. Setting justify-content to flex-end will cause the text to be right aligned.

You can also display text at the left and right edges and change the font size, as demonstrated with this header template:

<header style="justify-content: space-between; font-size: smaller">
<div>_TITLE_</div>
<div>_AUTHOR_</div>
</header>

This will display the title at the left and the author at the right, in a font size smaller than the main text.

You can also use the current section in templates, as shown below:

<header><div>_SECTION_</div></header>

_SECTION_ is replaced by whatever the name of the current section is. These names are taken from the metadata Table of Contents in the document (the PDF Outline). If the document has no table of contents then it will be replaced by empty text. If a single PDF page has multiple sections, the first section on the page will be used. Similarly, there is a variable named _TOP_LEVEL_SECTION_ that can be used to get the name of the current top-level section.

You can even use JavaScript inside the header and footer templates, for example, the following template will cause page numbers to start at 4 instead of 1:

<footer>
<div></div>
<script>document.currentScript.parentNode.querySelector("div").innerHTML = "" + (_PAGENUM_ + 3)</script>
</footer>

In addition there are some more variables you can use in the headers and footers, documented below:

- _TOTAL_PAGES_ - total number of pages in the PDF file, useful for implementing a progress counter, for example.

- _TOP_LEVEL_SECTION_PAGES_ - total number of pages in the current top level section

- _TOP_LEVEL_SECTION_PAGENUM_ - the page number of the current page within the current top level section

备注

When adding headers and footers make sure you set the page top and bottom margins to large enough values, under the PDF Output section of the conversion dialog.

可打印目录

You can also insert a printable Table of Contents at the end of the PDF that lists the page numbers for every section. This is very useful if you intend to print out the PDF to paper. If you wish to use the PDF on an electronic device, then the PDF Outline provides this functionality and is generated by default.

You can customize the look of the generated Table of contents by using the Extra CSS conversion setting under the Look & feel part of the conversion dialog. The default CSS used is listed below, simply copy it and make whatever changes you like.

.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 }

Custom page margins for individual HTML files

If you are converting an EPUB or AZW3 file with multiple individual HTML files inside it and you want to change the page margins for a particular HTML file you can add the following style block to the HTML file using the calibre E-book editor:

<style>
@page {
margin-left: 10pt;
margin-right: 10pt;
margin-top: 10pt;
margin-bottom: 10pt;
}
</style>

Then, in the PDF output section of the conversion dialog, turn on the option to Use page margins from the document being converted. Now all pages generated from this HTML file will have 10pt margins.

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 编辑书籍

编辑书籍
calibre有一个整合的电子书编辑器，用于编辑EPUB和AZW3(Kindle) 格式的电子书。此编辑器把书的内容以HTML和CSS的格式显示出来，并提供了可预览的界面，用于查看您的更改。它同时包含了大量的自动化工具执行常见的清理和修复任务。

你可以通过在calibre里右键单击任意一本书–选择“编辑书籍”来进入编辑器

目录

- 基本的工作流程

- 文件浏览器

- 重命名文件

- 合并文件

- 更改文本文件顺序

- 标记封面

- 删除文件

- 正在导出文件

- 增加新的图片/字体等或者创建新空文件

- 替代文件

- 有效地将样式表链接到HTML文件

- 搜索并替换

- 保存的查找

- 功能模式

- 搜索时忽略 HTML 标签

- 自动化工具

- 正在编辑目录

- 正在检查书籍

- 正在添加封面

- 嵌入使用到的字体

- 子集化嵌入字体

- 规范标点

- 转换 CSS 属性

- 移除无用的 CSS 规则

- 正在修正HTML

- 美化文件

- 插入内联目录

- 正在设置语义

- 过滤样式信息

- 升级本书的内部结构

- 还原点

- 实时预览面板

- 分割HTML文件

- 实时 CSS 面板

- 杂项工具

- 目录视图

- 检查图书中单词的拼写

- 插入特殊字符

- 代码检查器视图

- 正在检查外部链接

- 正在下载外部资源

- 按类型将文件整理到文件夹中

- 将其他电子书格式的文件导入为 EPUB

- 报表工具

- 代码编辑器中的特殊功能

- 语法高亮

- 上下文帮助

- 自动完成

- 片段

基本的工作流程

备注
calibre电子书编辑器的视频向导在 此处 <demo#tutorials>。

当你进入编辑器时，屏幕左侧将会显示出内容列表：包括单独的HTML文档、样式表、图像等。若想编辑某个特定的文件，只需双击即可。请注意，如果你不仅仅想做一些细微的调整，而是想进行更复杂的操作，你需要掌握 HTML [http://html.net/tutorials/html/] 和 CSS [http://html.net/tutorials/css/].的知识。

当您在编辑器中更改HTML或CSS时，更改将在右侧的预览面板中实时预览。如果您对所做更改的外观感到满意，请单击：guilabel：`保存`按钮或使用：guilabel：`文件->保存`将更改保存到电子书中。

一项有用的功能是“检查点”。 在开始进行一些雄心勃勃的编辑之前，您可以创建一个检查点。 检查点将保留图书的当前状态，如果将来您决定不喜欢所做的更改，则可以返回到创建检查点时的状态。 要创建检查点，请使用“编辑->创建检查点”。 每当您运行任何自动化工具时，例如全局搜索和替换，也会自动为您创建检查点。 检查点功能是编辑单个文件时正常撤消/重做机制之外的功能。 当更改分布在书中的多个文件时，需要检查点。

这是编辑图书的基本工作流程–打开文件、进行更改、预览和保存。本手册的其余部分将讨论现有的各种工具和功能，使您能够高效地执行特定任务。

文件浏览器

`文件浏览器`为您提供正在编辑的书本内各种文件的概览。文件按类别排列，文本(HTML)文件位于顶部，后面是样式表(CSS)文件、图像等。只需双击文件即可开始编辑。HTML、CSS和图像文件支持编辑。如果您正在阅读图书，则文本文件的顺序与它们的显示顺序相同。所有其他文件都按字母顺序排列。

通过将鼠标悬停在条目上，您可以看到它的大小，还可以在屏幕底部看到书中文件的完整路径。请注意，电子书中的文件是压缩的，因此最终图书的大小不是单个文件大小的总和。

在这本书中，许多文件都有特殊的意义。它们的名称旁边通常会有一个图标，表示特殊含义。例如，在左边的图片中，您可以看到文件`cover_image.jpg`和`title lepage.xhtml`旁边有封面图标，这表示它们是图书封面图片和标题页。同样，`content.opf`文件旁边有一个元数据图标，表示其中存在图书元数据，而`toc.ncx`文件旁边有一个T图标，表示它是目录。

通过右键单击单个文件，可以对它们执行许多操作。

重命名文件

您可以通过右键单击单个文件并选择：`Rename`来重命名该文件。重命名文件会自动更新整本书中对该文件的所有链接和引用。所以你要做的就是提供新的名字，其他的都会由Calibre来处理。

您还可以一次批量重命名多个文件。 如果您希望文件具有一些简单的名称模式，这非常有用。 例如，您可能想要将所有 HTML 文件重命名为 Chapter-1.html、Chapter-2.html 等。 按住“Shift”或“Ctrl”键并单击文件，选择要批量重命名的文件。 然后右键单击并选择“批量重命名”。 输入前缀以及您希望自动编号从哪个数字开始，单击“确定”即可完成。 批量重命名对话框还允许您按照文件在书中出现的顺序重命名文件，而不是您选择它们的顺序重命名文件，这很有用，例如按它们出现的顺序重命名所有图像。

最后，您可以批量更改所有选定文件的文件扩展名。如上选择多个文件，右键选择`更改所选文件的文件扩展名`。

合并文件

有时，您可能希望将两个HTML文件或两个CSS文件合并在一起。有时将所有内容都放在一个文件中会很有用。不过要小心，在典型的电子书阅读器中查看图书时，将大量内容放入单个文件会导致性能问题。

要将多个文件合并在一起，请按住“Ctrl”键并单击它们来选择它们（确保仅选择一种类型的文件，可以是所有 HTML 文件或所有 CSS 文件等）。 然后右键单击并选择合并。 就这样，calibre 将合并文件，自动迁移所有链接和对合并文件的引用。 请注意，合并文件有时会导致文本样式发生更改，因为各个文件可能使用不同的样式表。

您还可以选择文本文件，然后将文本文件拖放到另一个文本文件上，以将拖放的文本文件合并到目标文本文件中。

更改文本文件顺序

您可以在阅读本书时重新排列文本（HTML）文件的打开顺序，只需将它们拖放到“文件浏览器”中，或者单击要移动的文件，然后按“Ctrl+Shift”修饰符即可 “向上”、“向下”、“Home”或“End”键。 对于技术性的人来说，这称为重新排序 书脊。

请注意，您必须将项目放在其他项目*之间*，而不是放在它们上面，这可能有点麻烦，直到您习惯为止。 放在另一个文件的顶部将导致文件被合并。

标记封面

电子书通常都有封面图片。此图像在`文件浏览器`中由图像名称旁边的棕色书图标表示。如果要指定其他图像作为封面，可以通过右键单击该文件并选择`标记为封面‘来实现。

此外，EPUB文件还具有*title page*的概念。标题页是充当图书标题页/封面的HTML文件。在编辑ePub时，您可以通过右键单击将HTML文件标记为标题页。请注意，您标记的文件仅包含封面信息。如果它包含其他内容，如第一章，则如果用户将Calibre格式的EPUB文件转换为另一种格式，则该内容将丢失。这是因为在转换时，Calibre假定标记的标题页只包含封面，不包含其他内容。

删除文件

您可以通过右键单击文件或选择文件并按Delete键来删除文件。删除文件会从OPF文件中删除对该文件的所有引用，从而省去了这项工作。但是，其他位置的参照不会被删除，您可以使用Check Book工具轻松地查找和删除/替换它们。

正在导出文件

您可以将文件从书内导出到计算机上的其他位置。如果您希望使用专门的工具单独处理文件，这将非常有用。要执行此操作，只需右键单击该文件并选择“导出”。

处理完导出的文件后，您可以将其重新导入到书中，方法是再次右键单击该文件，然后选择“替换为文件…”，这将允许您将书籍中的文件替换为以前导出的文件。

您还可以在多个编辑器实例之间复制文件。在`文件浏览器`中选择要复制的文件，然后右键单击并选择、将所选文件复制到另一个编辑器实例。然后，在另一个编辑器实例中，右键单击`文件浏览器`并选择`从其他编辑器实例粘贴文件`。

增加新的图片/字体等或者创建新空文件

您可以通过单击“文件->新建文件”将计算机中的新图像、字体、样式表等添加到书本中。这允许您通过单击“导入资源文件”按钮导入文件，或者只需在新文件的框中输入文件名即可创建新的空白HTML文件或样式表。

您还可以使用文件->将文件导入书本，一次将多个文件导入书本。

替代文件

通过右键单击文件并选择替换，您可以轻松地替换图书中的现有文件。这将自动更新所有链接和引用，以防替换文件的名称与要替换的文件不同。

有效地将样式表链接到HTML文件

为了方便起见，您可以在文件浏览器中选择多个HTML文件，右键单击并选择链接样式表，以便Calibre自动将这些样式表的 <link> 标记插入到所有选定的HTML文件中。

搜索并替换

编辑书籍有一个非常强大的搜索和替换界面，允许您搜索和替换当前文件中的文本，跨所有文件，甚至在当前文件的标记区域中搜索和替换文本。您可以使用普通搜索或使用正则表达式进行搜索。要了解如何使用正则表达式进行高级搜索，请参阅`regexptutorial`。

通过“搜索->查找/替换”菜单项开始搜索和替换(您必须编辑HTML或CSS文件)。

在“查找”框中键入要查找的文本，并在“替换”框中替换它。您可以单击相应的按钮查找下一个匹配项、替换当前匹配项和替换所有匹配项。

使用框底部的下拉列表，您可以对当前文件、所有文本文件、所有样式文件或所有文件进行搜索。您还可以将搜索模式选择为普通(字符串)搜索或正则表达式搜索。

您可以通过“搜索->全部计数”统计搜索表达式的所有匹配项。计数将遍历您在下拉框中选择的任何文件/区域。

您还可以通过“搜索->转到行”转到当前打开的编辑器中的特定行。

备注

请记住，要充分利用搜索和替换的功能，您将需要使用正则表达式。 请参阅“正则表达式教程”。

保存的查找

您可以保存常用的搜索/替换表达式（包括函数模式表达式）并多次重复使用。 要保存搜索，只需右键单击“查找”框并选择“保存当前搜索”。

您可以通过“搜索->保存的搜索”调出保存的搜索。 这将为您提供可以应用的搜索和替换表达式的列表。 您甚至可以通过按住“Ctrl”键并单击来选择列表中的多个条目，以便在单个操作中运行多个搜索和替换表达式。

功能模式

函数模式允许您编写在每个查找/替换上运行的任意强大的 Python 函数。 您可以在功能模式下进行几乎任何您喜欢的文本操作。 有关更多信息，请参阅“function_mode”。

搜索时忽略 HTML 标签

还有一个专门的工具用于搜索文本，忽略之间的任何 HTML 标签。 例如，如果这本书有 HTML“Empahisis on a <i>word</i>.”，您可以搜索“on a word”，即使中间有“<i>”标签，也会找到它。 通过“搜索->搜索忽略 HTML 标记”菜单项使用此工具。

自动化工具

Edit book 有各种工具可以帮助完成常见任务。 这些可以通过“工具”菜单访问。

正在编辑目录

有一个专用工具可以轻松编辑目录。 使用“工具->目录->编辑目录”启动它。

编辑目录工具在左侧显示当前目录（如果有）。 只需双击任何条目即可更改其文本。 您还可以通过拖放或使用右侧的按钮来重新排列条目。

对于没有预先存在目录的书籍，该工具为您提供了各种选项来从文本自动生成目录。 您可以从文档中的标题、链接、单个文件等生成。

您可以通过单击单个条目，然后单击“更改此条目指向的位置”按钮来编辑各个条目。 这将打开书籍的迷你预览，只需将鼠标光标移到书籍视图面板上，然后单击您希望条目指向的位置即可。 一条粗绿线将向您显示该位置。 对位置感到满意后，单击“确定”。

正在检查书籍

“检查图书”工具会搜索您的图书以查找可能妨碍其在实际阅读器设备上正常运行的问题。 通过“工具->支票簿”激活它。

发现的任何问题都会报告在一个漂亮且易于使用的列表中。 单击列表中的任何条目会显示有关该错误的一些帮助，并为您提供自动修复该错误的选项，如果该错误可以自动修复。 您还可以双击错误以在编辑器中打开错误位置，以便您可以自行修复。

执行的一些检查包括：

- HTML 标记格式错误。 任何不能解析为格式良好的 XML 的 HTML 标记都会被报告。 纠正它将确保您的标记在所有上下文中都能按预期工作。 calibre 还可以自动修复这些错误，但自动修复有时会产生意想不到的效果，因此请谨慎使用。 与往常一样，在自动修复之前会创建一个检查点，以便您可以轻松恢复所有更改。 自动修复的工作原理是使用 HTML5 算法解析标记，该算法具有高度容错性，然后转换为格式良好的 XML。

- CSS 样式格式错误或未知。 任何无效的 CSS 或具有 CSS 2.1 标准（以及 CSS 3 中的一些）中未定义的属性的 CSS 都会被报告。 所有样式表、内联样式属性和 HTML 文件中的<style> 标签均检查 CSS。

- 损坏的链接。 报告指向书中丢失的文件的链接。

- 未引用的文件。 报告书中未被任何其他文件引用或不在 书脊中的文件。

- OPF 文件中的各种常见问题，例如重复的 书脊或清单项、损坏的 idref 或元覆盖标签、缺少必需的部分等。

- 针对可能导致图书在阅读器设备上出现故障的已知问题进行各种兼容性检查。

正在添加封面

您可以通过“工具->添加封面”轻松为书籍添加封面。 这允许您选择书中的现有图像作为封面，或将新图像导入书中并将其设为封面。 编辑 EPUB 文件时，会自动生成封面的 HTML 包装器。 如果在书中找到现有封面，则将其替换。 该工具还会自动将封面文件正确标记为 OPF 中的封面。

嵌入使用到的字体

通过“工具->嵌入参考字体”访问，该工具会查找书中引用的所有字体，如果尚未嵌入，则会在您的计算机中搜索它们，如果找到，并将其嵌入到书中。 在执行此操作之前，请确保您拥有嵌入商业许可字体所需的版权。

子集化嵌入字体

通过“工具->子集嵌入字体”访问，该工具将书中的所有字体减少到仅包含书中实际存在的文本的字形。 这通常会将字体文件的大小减少约 50%。 但是，请注意，一旦字体成为子集，如果您添加的新文本的字符以前不存在于子集字体中，则该字体将不适用于新文本。 因此，仅将此作为工作流程的最后一步。

规范标点

将纯文本破折号、省略号、引号、多个连字符等转换为印刷正确的等效内容。 请注意，该算法有时会生成不正确的结果，尤其是在缩写开头涉及单引号时。 通过“工具->智能标点符号”访问。

转换 CSS 属性

创建规则来改变书籍的样式。 例如，创建一条规则将所有红色文本转换为绿色，或者将书中所有文本的字体大小加倍，或者将特定字体系列的文本设置为斜体等。

创建规则很简单，规则遵循自然语言格式，如下所示：

- 如果属性*颜色*是*红色*，将其*更改*为*绿色*

- 如果属性 font-size 是 任何值 将该值乘以 *2

通过“工具->变换样式”访问。

移除无用的 CSS 规则

从样式表和 <style> 标签中删除所有未使用的 CSS 规则。 一些从生产模板创建的书籍可能具有大量与任何实际内容不匹配的额外 CSS 规则。 这些额外的规则可能会减慢需要处理所有规则的读者的速度。 通过“工具->删除未使用的 CSS”访问。

正在修正HTML

该工具只是将无法解析为 XML 的 HTML 转换为格式良好的 XML。 在电子书中，格式不正确的 XML 很常见，因此该工具只是自动执行修复此类 HTML 的过程。 该工具的工作原理是使用 HTML5 算法（所有现代浏览器中使用的算法）解析 HTML，然后将结果转换为 XML。 请注意，自动修复有时会产生违反直觉的结果。 如果您愿意，可以使用上面讨论的 Check Book 工具来查找并手动更正 HTML 中的问题。 通过“工具->修复 HTML”访问。

美化文件

该工具用于自动格式化所有 HTML 和 CSS 文件，使它们“看起来很漂亮”。 代码会自动缩进，以便很好地排列，在适当的位置插入空行等等。 请注意，美化还会自动修复损坏的 HTML/CSS。 因此，如果您不想执行任何自动修复，请先使用检查簿工具修复所有问题，然后再运行美化。 通过“工具->美化所有文件”访问。

备注

在 HTML 中，通过 CSS 空白指令，任何文本都可以具有显着的空白。 因此，美化可能会改变 HTML 的呈现。 为了尽可能避免这种情况，美化算法仅美化包含其他块级标签的块级标签。 因此，例如，<p> 标记内的文本不会更改其空格。 但是只包含其他<p>和 <div> 标签的<body>标签将会被美化。 这有时意味着特定文件不会受到美化的影响，因为它没有合适的块级标签。 在这种情况下，您可以尝试不同的美化工具，这些工具不太仔细，例如：HTML Tidy [https://infohound.net/tidy/]。

插入内联目录

通常在电子书中，目录与正文是分开的，并且通常通过电子书阅读设备中的特殊目录按钮/菜单来访问。 您还可以让 calibre 自动生成*内联*目录，该目录成为书籍文本的一部分。 它是根据当前定义的目录生成的。

如果多次使用此工具，每次调用都会导致先前创建的内联目录被替换。 可以通过“工具->目录->插入内联目录”访问该工具。

正在设置语义

该工具用于设置 EPUB 文件中的*语义*。 语义很简单，就是 OPF 文件中的链接，用于将书中的某些位置标识为具有特殊含义。 您可以使用它们来识别前言、奉献、封面、目录等。只需选择要指定的语义信息类型，然后选择链接应指向的书中位置。 可以通过“工具->设置语义”访问该工具。

过滤样式信息

该工具可用于轻松地从整本书中删除指定的 CSS 样式属性。 你可以告诉它你想要删除哪些属性，例如“颜色、背景颜色、行高”，它会从它们出现的任何地方删除它们——样式表、“<style>”标签和内联“ style`` 属性。 删除样式信息后，将显示所有所做更改的摘要，以便您可以准确地看到更改的内容。 可以通过“工具->过滤样式信息”访问该工具。

升级本书的内部结构

如果可能的话，该工具可用于升级本书的内部结构。 例如，它将 EPUB 2 图书升级为 EPUB 3 图书。 该工具可以通过“升级图书内部结构”来访问。

还原点

“检查点”是一种将书籍当前状态标记为“特殊”的方法。 然后，您可以继续对本书进行任何您想要的更改，如果您不喜欢结果，请返回到检查点状态。 每次运行上一节中描述的任何自动化工具时，都会自动创建检查点。

您可以通过“编辑->创建检查点”创建检查点。 并使用“编辑->恢复到…”返回到上一个检查点

检查点功能是编辑单个文件时正常撤消/重做机制之外的功能。 当更改分布在书中的多个文件中或当您希望能够整体恢复一大组相关更改时，需要检查点。

您可以通过“视图->检查点”查看可用检查点的列表。 您可以使用“diff”工具将书籍的当前状态与指定的检查点进行比较 - 通过选择感兴趣的检查点并单击“比较”按钮。 “恢复到”按钮将书籍恢复到选定的检查点，撤消自创建该检查点以来的所有更改。

实时预览面板

“文件预览”为您提供了内部各种文件的概述。实时预览面板向您显示正在实时进行的更改（有一两秒的延迟）。 当您编辑 HTML 或 CSS 文件时，预览面板会自动更新以反映您的更改。 当您在编辑器中移动光标时，预览面板将跟踪其位置，向您显示书中的相应位置。 在预览面板中单击，将导致编辑器中的光标位于您单击的元素上。 如果您单击指向书中另一个文件的链接，该文件将在编辑和预览面板中自动打开。

您可以通过预览面板下的按钮关闭位置自动同步和更改的实时预览。 预览面板的实时更新仅在您未在编辑器中主动输入时发生，以免在等待预览渲染时分散您的注意力或减慢您的速度。

预览面板向您显示文本在查看时的外观。 但是，预览面板并不能替代在实际阅读器设备上实际测试您的图书。 它比实际读者的能力更强，也更弱。 它比大多数阅读器设备更好地容忍错误和草率标记。 它还不会显示页边距、分页符和使用字体名称别名的嵌入字体。 在阅读本书时使用预览面板，但完成后，请在实际的阅读器设备或软件模拟器中查看它。

备注

如果字体文件中的字体名称与 CSS @font-face 规则中的名称不匹配，则预览面板不支持嵌入字体。 您可以使用检查簿工具快速查找并修复任何此类问题字体。

分割HTML文件

预览面板的一种用途是分割长 HTML 文件，也许不是显而易见的。 查看要拆分的文件时，单击预览面板 下的“拆分模式”按钮。 然后只需将鼠标移动到要分割文件的位置并单击即可。 当您移动鼠标时，一条粗绿线将准确显示分割发生的位置。 找到所需位置后，只需单击即可执行分割。

拆分文件将自动更新指向文件下半部分的所有链接和引用，并将在编辑器中打开新拆分的文件。

您还可以通过在编辑器中右键单击文件内部并选择“在多个位置拆分”来自动在多个位置拆分单个 HTML 文件。 这将允许您轻松地在所有标题标签或具有特定类别的所有标签等处拆分大文件。

实时 CSS 面板

“实时CSS”面板向您显示适用于您当前正在编辑的标签的所有样式规则。 显示标签的名称及其在编辑器中的行号，后面是匹配样式规则的列表。

这是快速查看哪些样式规则适用于任何 标签的好方法。 该视图还有可单击的链接（蓝色），如果您希望对样式规则进行任何更改，可以直接转到定义样式的位置。 显示直接应用于标记的样式规则以及从父标记继承的规则。

该面板还向您显示最终计算出的标签样式。 列表中被更高优先级规则取代的属性用一条线显示。

您可以通过“视图->实时CSS”启用实时CSS 面板。

杂项工具

在您编辑本书时，还有一些有用的工具。

目录视图

目录视图显示书中当前的目录。 双击任何条目会在编辑器中打开该条目所指向的位置。 您可以右键单击以编辑目录、刷新视图或展开/折叠所有项目。 通过“视图->目录”访问此视图。

检查图书中单词的拼写

您可以通过“工具->检查拼写”运行拼写检查器。

单词会显示它们在书中出现的次数以及该单词所属的语言。 语言信息取自书籍元数据和 HTML 文件中的“lang”属性。 这使得拼写检查器即使在包含多种语言文本的书籍上也能正常工作。 例如，在以下 HTML 摘录中，将使用美式英语检查单词 color，使用英式英语检查单词 color：

<div lang="en_US">color <span lang="en_GB">colour</span></div>

备注

您可以双击某个单词以突出显示该单词在编辑器中下一次出现的位置。 如果您希望手动编辑单词或查看它所处的上下文，这非常有用。

要更改单词，只需双击右侧建议的替代拼写之一，或者输入您自己更正的拼写，然后单击“将所选单词更改为”按钮。 这将替换书中出现的所有单词。 您还可以右键单击主单词列表中的单词，从右键菜单中方便地更改单词。

您可以通过单击“忽略”按钮让拼写检查器忽略当前会话的单词。 您还可以通过单击“添加到词典”按钮将单词添加到用户词典中。 拼写检查器支持多个用户词典，因此您可以选择要将单词添加到的词典。

您还可以让拼写检查器显示书中的所有单词，而不仅仅是拼写错误的单词。 这对于查看书中最常见的单词以及对单个单词进行简单搜索和替换非常有用。

备注

如果您在拼写检查工具打开时通过编辑文件对图书进行任何更改，则应单击拼写检查工具中的“刷新”按钮。 如果您不这样做并继续使用拼写检查工具，您可能会丢失在编辑器中所做的更改。

备注

要在运行拼写检查工具时排除单个文件进行拼写检查，您可以使用“排除文件”按钮或在文件的开始 标记下方添加以下注释：

<!-- calibre-no-spell-check -->

正在添加新的字典

拼写检查器附带英语和西班牙语的内置词典。 您可以通过“首选项->编辑器->管理拼写词典”安装自己的词典。 拼写检查器可以使用 LibreOffice 程序中的词典（.oxt 格式）。 您可以从“LibreOffice 扩展存储库 <https://extensions.libreoffice.org/?Tags%5B%5D=50>”下载这些词典。

插入特殊字符

您可以使用“编辑->插入特殊字符”工具插入难以键入的字符。 这会显示所有 Unicode 字符，只需单击您要输入的字符即可。 如果在单击时按住“Ctrl”，窗口将在插入所选字符后自行关闭。 该工具可用于将特殊字符插入到主文本或用户界面的任何其他区域，例如搜索和替换工具。

因为角色很多，您可以定义自己的“最喜欢的”角色，这些角色将首先显示。 只需右键单击一个角色即可将其标记为收藏夹。 您还可以右键单击收藏夹中的角色将其从收藏夹中删除。 最后，您可以通过单击“重新排列收藏夹”按钮，然后拖放收藏夹中的字符来重新排列收藏夹中的字符顺序。

您也可以使用键盘直接输入特殊字符。 为此，请键入该字符的 Unicode 代码（十六进制），然后按“Alt+X”键，这会将之前键入的代码转换为相应的字符。 例如，要输入 ÿ，您可以输入 ff，然后输入“Alt+X”。 要输入不间断空格，您可以使用 a0，然后使用“Alt+X”，要输入水平省略号，您可以使用 2026 和“Alt+X”，依此类推。

最后，您可以使用 HTML 命名实体输入特殊字符。 例如，输入 空格; 当您键入分号时，将被不间断空格替换。 仅当键入分号时才会进行替换。

代码检查器视图

此视图向您显示适用于当前感兴趣元素的 HTML 编码和 CSS。 您可以通过右键单击预览面板中的某个位置并选择“检查”来打开它。 它允许您查看该元素的 HTML 编码，更重要的是，查看适用于该元素的 CSS 样式。 您甚至可以动态编辑样式并立即查看更改的效果。 请注意，编辑样式实际上并不会更改书籍内容，它仅允许快速实验。 Inspector 内的实时编辑功能正在开发中。

正在检查外部链接

您可以使用此工具检查书中指向外部网站的所有链接。 该工具将尝试访问每个外部链接的网站，如果访问失败，它将以方便的格式报告所有损坏的链接，以便您修复。

正在下载外部资源

您可以使用此工具自动下载任何图像/样式表/等。 书中未与本书捆绑在一起的内容（即它们的 URL 指向互联网上的某个位置）。 该工具将找到所有此类资源并自动下载它们，将它们添加到书中并替换对它们的所有引用以使用下载的文件。

按类型将文件整理到文件夹中

通常，当编辑从某处获取的 EPUB 文件时，您会发现 EPUB 内的文件随意排列在不同的子文件夹中。 该工具允许您根据文件的类型自动将所有文件移动到子文件夹中。 通过“工具->排列到文件夹”访问它。 请注意，此工具仅更改文件在 EPUB 内的排列方式，不会更改文件在文件浏览器中的显示方式。

将其他电子书格式的文件导入为 EPUB

该编辑器能够将其他一些电子书格式的文件直接导入为新的 EPUB，而无需进行完全转换。 这对于直接从您自己手工编辑的 HTML 文件创建 EPUB 文件特别有用。 您可以通过“文件->导入 HTML 或 DOCX 文件作为新书”来完成此操作。

报表工具

该编辑器包括一个很好的*报告*工具（通过“工具->报告”），显示书中使用的文件、图像、链接、单词、字符和样式的摘要。 报告中的每一行都是热链接的。 双击一行可跳转到书中使用或定义该项目的位置（视情况而定）。 例如，在“链接”视图中，您可以双击“源”列中的条目以跳转到定义链接的位置，双击“目标”列中的条目以跳转到链接指向的位置。

代码编辑器中的特殊功能
calibre HTML 编辑器非常强大。 它具有许多使 HTML（和 CSS）编辑变得更容易的功能。

语法高亮

HTML 编辑器具有非常复杂的语法突出显示。 特点包括：

- 粗体、斜体和标题标签内的文本变为粗体/斜体

- 当您在 HTML 中移动光标时，匹配的 HTML 标记会突出显示，您可以使用键盘快捷键“Ctrl+{”和“Ctrl+}”跳转到开始或结束标记。 同样，您可以使用“Ctrl+Alt+T”或“Ctrl+Shift+T”选择 标签的内容。

- 已用紅線間底無效的HTML

- HTML 标签内的文本和标题等属性中的拼写错误会突出显示。 拼写检查是语言感知的，基于当前标签的“lang”属性值和整体书籍语言。

- 嵌入在``<style>``标签内的CSS被突出显示

- 难以区分的特殊字符会突出显示，例如不间断空格、不同类型的连字符等。

- <a> 标签、<img> 和``<link>`` 标签中其他文件的链接都突出显示文件名。 如果它们指向的文件名不存在，则该文件名会标有红色下划线。

上下文帮助

您可以右键单击 HTML 标记名称或 CSS 属性名称来获取该标记或属性的帮助。

您还可以按住“Ctrl”键并单击链接标记内的任何文件名，以在编辑器中自动打开该文件。 同样，“Ctrl”单击类名将带您到达与 标签和类匹配的第一个样式规则。

右键单击 HTML 文件中的类名将允许您重命名该类，从而更改该类在本书及其所有样式表中出现的所有位置。

自动完成

编辑电子书时，最繁琐的任务之一是创建指向书中其他文件、CSS 样式表或图像的链接。 您必须找出正确的文件名和文件的相对路径。 编辑器具有自动完成功能，使操作变得更容易。

当您键入文件名时，编辑器会自动弹出建议。 只需使用“Tab”键即可选择正确的文件名。 该编辑器甚至还提供了指向另一个 HTML 文件中的锚点的链接的建议。 输入“#”字符后，编辑器将显示目标文件中所有锚点的列表，其中包含一小段文本以帮助您选择正确的锚点。

请注意，与大多数其他完成系统不同，编辑器的完成系统使用子序列匹配。 这意味着您只需键入文件名中任意位置的两个或三个字母即可完成文件名。 例如，假设您想要文件名“../images/arrow1.png”，您可以简单地键入“ia1”并按“Tab”来完成文件名。 搜索匹配项时，补全系统会优先考虑位于单词开头或紧接路径分隔符之后的字母。 一旦你习惯了这个系统，你会发现它为你节省了大量的时间和精力。

片段
calibre 电子书编辑器支持*片段*。 片段是经常重复使用或包含大量冗余文本的一段文本。 该编辑器允许您只需敲击几个键即可插入片段。 这些片段非常强大，具有许多功能，例如可以在之间跳转的占位符、重复文本的自动镜像等等。 有关更多信息，请参阅“片段”。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 编辑书籍 »
- 编辑器中搜索和替换功能

编辑器中搜索和替换功能

编辑器中的“搜索和替换”工具支持*功能模式*。 在此模式下，您可以将正则表达式（请参阅“regexp”）与任意强大的 Python 函数结合起来，以执行各种高级文本处理。

在搜索和替换的标准 regexp 模式中，您指定要搜索的正则表达式以及用于替换所有找到的匹配项的模板。 在函数模式下，您不使用固定模板，而是在“Python 编程语言 <https://docs.python.org>”中指定任意函数。 这使您可以完成许多使用简单模板无法完成的事情。

将通过示例描述使用函数模式和语法的技术，向您展示如何创建函数来执行逐渐复杂的任务。

自动修复文档中标题的大小写

在这里，我们将利用编辑器中的内置函数之一自动将标题标签内所有文本的大小写更改为标题大小写：

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

对于该函数，只需选择“标题大小写文本（忽略标签）”内置函数即可。 会将如下所示的标题更改为“<h1> some TITLE </h1>” 为“<h1> Some Title </h1>”。 即使标题标签内有其他 HTML 标签，它也能工作。

您的第一个自定义函数 - 智能连字符

函数模式的真正强大之处在于能够创建自己的函数以任意方式处理文本。 编辑器中的 Smarten 标点符号工具会保留单个连字符，因此您可以使用此功能将它们替换为破折号。

要创建新函数，只需单击“创建/编辑”按钮即可创建新函数并复制下面的 Python 代码。

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

每个“搜索和替换”自定义函数都必须有一个唯一的名称，并由一个名为“replace”的 Python 函数组成，该函数接受上面显示的所有参数。 目前，我们不必担心“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>”标记中的标题，看起来像“<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 文件上运行此搜索，则文件将按照它们出现的顺序进行处理。 这本书。 有关 详细信息，请参阅“file_order_replace_all”。

自动创建目录

最后，让我们尝试一些更雄心勃勃的事情。 假设您的书的“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'

并将其与 find 表达式一起使用：

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

在“所有文本文件”上运行搜索，在搜索结束时，将弹出一个窗口，其中显示“调试函数的输出”，其中包含 HTML 目录，可以粘贴到“toc.html”中。

上面的函数有很多注释，所以应该很容易理解。 关键的新功能是对“replace()”函数使用另一个有用的额外参数，即“data”对象。 “data” 对象是一个 Python 字典，在单个“全部替换”操作期间，在所有连续调用“replace()”之间持续存在。

另一个新功能是使用“call_after_last_match”——在“replace()”函数上将其设置为“True”意味着编辑器将额外调用一次“replace()” 已找到匹配项。 对于这个额外的调用，匹配对象将为“None”。

这只是一个演示，向您展示功能模式的强大功能，如果您确实需要根据书中的标题生成目录，您最好使用“工具”->“目录”中的专用目录工具 。

功能模式的API

所有函数模式函数必须是名为replace的Python函数，具有以下签名：

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()”。 第一场比赛的号码为 1。

“file_name”参数

这是找到当前匹配项的文件的文件名。 在标记文本内搜索时，file_name 为空。 “file_name” 是规范形式，是相对于本书根目录的路径，使用“/” 作为路径分隔符。

“metadata” 参数

这代表当前书籍的元数据，例如标题、作者、语言等。它是`calibre.ebooks.metadata.book.base.Metadata` 类的对象。 有用的属性包括“标题”、“作者”（作者列表）和“语言”（语言代码）。

“dictionaries” 参数

这代表用于对当前书籍进行拼写检查的词典集合。 它最有用的方法是“dictionaries.recognized(word)”，如果传入的单词被当前书籍语言的字典识别，它将返回“True”。

“data” 参数

这是一个简单的 Python“字典”。 当您运行“全部替换”时，每个连续的匹配都会导致使用与数据相同的“字典”来调用“replace()”。 因此，您可以使用它在“全部替换”操作期间调用“replace()”之间存储任意数据。

“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']
...

您还可以使用函数对象来存储持久数据，这些数据可以被其他函数重用。 例如，您可以拥有一个函数，当使用“全部替换”运行时会收集一些数据，而另一个函数则在随后运行时使用这些数据。 考虑以下两个函数：

# 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']
...

调试你的函数

您可以使用 Python 中的标准“print()”函数来调试您创建的函数。 查找/替换完成后，打印的输出将显示在弹出窗口中。 您在上面看到了使用“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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 编辑书籍 »
- 片段

片段

Calibre电子书编辑器支持*代码片段*。片段是一段经常重复使用或包含大量冗余文本的文本。该编辑器使您能够只需敲击几次键盘即可插入代码段。例如，假设您经常在编辑HTML文件时插入链接标记，那么您只需在编辑器中键入 <a，然后按|ct|即可。编辑将其展开为：

<a href="filename"></a>

不仅如此，“文件名”一词如被选中，并将光标放在它上面，这样您就可以使用编辑器漂亮的“自动完成编辑”功能轻松输入真实的文件名。一旦您输入完文件名，再次按 Control+J，光标将跳到<a>标记之间的位置，这样您就可以轻松地输入链接的文本。

编辑器中的代码片段系统非常复杂，有几个内置的代码片段，您可以创建自己的代码片段来适应您的编辑风格。

下面关于内置代码段的讨论应该有助于说明代码段系统的强大功能。

备注

您也可以在`搜索和替换`面板的文本输入字段中使用代码片段，但是占位符(使用|ct|跳转)不起作用。

内置代码段

内置的代码片段如下所述。请注意，您可以通过使用相同的触发器文本创建您自己的代码段来覆盖它们。

插入填充文字[Lorem]

第一个内置代码段，也是最简单的，用于将填充文本插入到文档中。填充文摘自”《善与恶的终结》<https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum>”_这是西塞罗的一部哲学著作(翻译成英文)。要使用它，只需在HTML文件中键入“Lorem”，然后按|ct|。它将被几段填充文字所取代。

这段代码的定义非常简单，触发器文本被定义为``Lorem``，模板被简单定义为要插入的文字文本。您可以轻松地对其进行定制，以使用您最喜欢的填充文本形式。

插入自闭合HTML标签 [<>]

现在让我们来看一个简单的例子，它展示了*占位符*这个强大的概念。假设您想要插入自动结束标记``<hr/>``。只需键入``<>``，然后按 Control+J，编辑器将扩展代码片段为：

<|/>

这里，“|”符号表示当前光标位置。然后，您可以键入 “hr”，然后按|ct|将光标移动到标记的末尾。此代码段定义为：

Trigger: <>
Template: <$1/>$2

占位符只是美元($)符号后面跟着一个数字。按|ct|展开代码段时，光标位于第一个占位符(数字最小的占位符)。再次按|ct|时，光标会跳到下一个占位符(数字较大的占位符)。

插入HTML链接标记 [<a]

所有的HTML链接标记都共享一个公共结构。它们有一个``href`‘属性，在开始标记和结束标记之间有一些文本。一段让输入效率更高的代码片段将向我们介绍占位符的一些更多功能。要使用此代码段，只需键入``<a``，然后按 Control+J。编辑会将其扩展为：

<a href="filename|"></a>

不仅如此，如选中单词``文件名``，并将光标放在该单词上，这样您就可以使用该编辑器的；编辑器自动插入”功能轻松地输入真实的文件名。一旦你输入完文件名，再按 Control+J，光标就会跳到“<a>”标记之间的位置，这样你就可以轻松地输入链接的文本。输入完文本后，再次按|ct|可跳到结束标记之后的位置。此代码段定义为：

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

这里有几个新功能。首先，$1``占位符变得更加复杂。它现在包括一些*默认文本*(单词``文件名)。如果占位符包含默认文本，则在展开代码段时将用默认文本替换该占位符。此外，当您使用|ct|跳到具有默认文本的占位符时，默认文本也会被选中。通过这种方式，您可以使用默认文本来提醒您填写模板的重要部分。您可以使用以下语法为占位符指定默认文本：${<number>：默认文本}。

另一个新特性是，第二个占位符后面有一个星号(${2*})。这意味着在展开模板之前选择的任何文本都将替换占位符。要查看实际操作，请在编辑器中选择一些文本，按 Control+J，键入``<a``，然后再次按|ct|，模板将扩展为：

<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标签(或在标签中换行先前选择的文本)。要使用它，只需键入``<<``，然后按|ct|。编辑器将其扩展为：

<|></>

键入标记名称，例如：“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，键入类名，按 |ct |键入标记的内容，最后按|ct||跳出标记。结束标记将自动填充。

创建您自己的代码片段

片段确实很出色，因为您可以创建自己的片段以适合您的编辑风格。 要创建您自己的代码片段，请转到编辑器中的“编辑”->“首选项”->“编辑器设置”->“管理代码片段”。 这将弹出一个易于使用的对话框来帮助您创建自己的片段。 只需单击“添加片段”按钮，您将看到一个如下所示的对话框：

首先，为您的代码段命名一个描述性的名称，以帮助在将来识别该代码段。然后指定*触发器*。触发器只是在按|ct|以展开代码片断之前必须在编辑器中键入的文本。

然后指定代码段模板。您应该从上面的一个示例开始，然后修改它以满足您的需要。最后，指定要激活代码段的文件类型。这样，您就可以拥有具有相同触发器文本的多个代码段，这些代码段在不同的文件类型中工作方式不同。

下一步是测试新创建的代码段。使用底部的“测试”框。键入触发器文本，然后按|ct|展开代码段并在占位符之间跳转。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- calibre 内容服务器

calibre 内容服务器
calibre菜单“内容服务器”允许您访问您的calibre书库，并直接在您喜爱的手机或平板电脑设备上的浏览器中阅读书籍。因此，您无需在手机上安装任何专用的图书阅读/管理应用程序。只需使用浏览器。服务器将您正在阅读的书籍下载并存储在离线缓存中，这样即使在没有互联网连接的情况下也可以阅读。

目录

- 从其他设备访问内容服务器

- 从家庭网络上的设备访问服务器

- 从互联网上任何地方访问服务器

- 服务器接口界面

- 书籍列表

- 电子书阅读人

- 浏览器支持

- 启用脱机支持

- 仅从命令行管理用户帐户

- 将calibre内容服务器集成到其他服务器中

- 使用完整的虚拟主机

- 使用URL前缀

- 在现代Linux系统上为calibre服务器创建服务

要启动服务器，请单击“连接/共享”按钮，然后选择“启动内容服务器”。您可能会收到来自计算机防火墙或防病毒程序的消息，询问是否可以允许访问“calible.exe”。单击“允许”或“确定”按钮。然后在电脑中打开浏览器（最好是Chrome或Firefox），输入以下地址：

http://127.0.0.1:8080

This will open a page in the browser showing you your calibre libraries, click on any one and browse the books in it. Click on a book, and it will show you all the metadata about the book, along with buttons to Read and Download the book. Click the Read button to start reading the book.

备注

上面使用的地址“http://127.0.0.1:8080”只能在运行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. 请检查您的设备和计算机是否位于同一网络上。这意味着它们都应该连接到同一个无线路由器。特别地，两者都不应该使用蜂窝网络或ISP提供的直接WiFi连接。

4. 如果您的网络设置不标准，可能是菜单：“连接/共享”菜单上显示的IP地址不正确。在这种情况下，你必须自己找出正确的IP地址。不幸的是，考虑到网络配置的无限多样性，不可能为您提供这样做的路线图。

5. 如果您已经设置了用户名和密码，请先在没有设置用户名和密码的情况下进行尝试，看看是否会导致问题。一些电子墨水设备的浏览器不处理身份验证。您有时可以通过在URL中包含用户名和密码来解决此问题，例如：“http://username:password@192.168.1.2:8080”。

6. 如果你陷入困境，你可以随时在“calibre用户论坛”_上寻求帮助。

从互联网上任何地方访问服务器

警告

在此之前，您应该在服务器中打开用户名/密码保护，否则世界上的任何人都可以访问您的书籍。转到菜单“首选项”->“共享”->“通过网络共享”，并启用菜单选项“访问内容服务器需要用户名和密码”。

虽然设置互联网访问的具体细节因网络配置和您使用的计算机类型而异，但基本模式如下。

1. 找出要运行服务器的计算机的外部IP地址。您可以访问“我的IP地址是什么”网站<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完全相同。搜索页面甚至允许您通过点击作者/标签等来构建搜索查询。就像在主程序calibre中使用标签浏览器一样。

主程序的一个备受喜爱的功能是：“虚拟书库”也出现在服务器界面中。单击右上角的三个垂直点以选择虚拟书库。

电子书阅读人

You can read any book in your calibre library by simply tapping on it and then tapping the Read button. The book reader is very simple to operate. You can both tap and swipe to turn pages. Swiping up/down skips between chapters. Tapping the top quarter of the screen gets you the detailed controls and viewer preferences.

如果你让内容服务器继续运行，你甚至可以在多个设备上打开同一本书，它会记住你上次阅读的位置。如果没有，您可以通过点击上方四分之一区域并选择菜单：“同步”来强制同步。

浏览器支持

新版本的calibre服务器充分利用了高级HTML5和CSS 3功能。因此，它需要使用最新的浏览器。它已经在Android Chrome和iOS Safari以及桌面上的Chrome和Firefox上进行了测试。

服务器正在谨慎使用已经标准化或正在进行标准化上的功能。因此，如果它目前无法正常与您喜爱的浏览器使用，那么一旦该浏览器功能赶上，它很可能会满足你的使用。

如果您使用的是特别旧或受限的浏览器，或者您不喜欢运行Java脚本，您可以使用*手机*视图，只需在服务器地址后添加``/mobile``即可。

备注

在iOS上，苹果只允许使用一个浏览器引擎，因此Firefox、Chrome和Safari实际上都是同一个浏览器。新的服务器界面需要iOS 10.3.2或更新版本。在安卓系统上，该服务器已通过Chrome 58及更新版本的测试。

启用脱机支持

Browser makers have been trying to force people to use SSL by disabling advanced features in their browsers for plain HTTP connections. One such casualty is ApplicationCache, which was what was used in calibre for offline support. As a result now-a-days sadly, offline mode works only as long as you keep the browser tab open. In addition, in Firefox on Android, you will need to type about:config and create a preference called browser.tabs.useCache and set it to true.

仅从命令行管理用户帐户

Calibre程序在中有一个很好的分部设计在菜单“首选项”，允许您管理服务器的用户帐户。但是，如果您想运行独立服务器，但无法在同一台计算机/用户帐户上运行calibre主程序，您也可以仅使用命令行管理用户。

您可以使用独立的Calibre服务器程序的“–管理用户”选项来管理用户帐户。假设您希望将用户数据库存储在文件夹“/srv/calbre”中，然后通过运行以下命令来创建它：

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前缀/calibre”。这将导致内容服务器为所有前缀为“/calibre”的URL提供服务。要查看此操作，请访问“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”下的所有请求传递给运行在端口8080上的calibre服务器，并且由于上面的“–url前缀”选项，calible服务器透明地处理这些请求。

备注

当使用反向代理时，您应该通过使用“–listen on 127.0.0.1”告诉calibre内容服务器只在本地服务器上侦听。这样，服务器将只侦听来自同一台计算机的连接，即来自反向代理的连接。

备注

如果您已经为主服务器设置了SSL，您应该告诉calibre服务器使用基本身份验证，而不是摘要身份验证，因为这会更快。要执行此操作，请将“–auth-mode=basic”选项传递给“calibre服务器”。

在现代Linux系统上为calibre服务器创建服务

You can easily create a service to run calibre at boot on a modern (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) based Linux system. Just create the file /etc/systemd/system/calibre-server.service with the contents shown below:

[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

将“mylinxuser”和“mylinuxgroup”更改为您希望服务器以何种用户和组的身份运行。这应该是拥有calibre书库文件夹中文件的同一用户和组。请注意，以root用户身份运行服务器通常不是一个好主意。同时更改Calibre书库文件夹的路径以适合您的系统。如果需要，可以添加多个库。请参阅“calibre服务器”命令的帮助。

现在运行：

sudo systemctl start calibre-server

来启动服务器。使用以下命令检查其状态：

sudo systemctl status calibre-server

要使其在引导时启动，请运行：：

sudo systemctl enable calibre-server

备注
calibre服务器确实不需要运行X服务器，但它确实需要安装X库，因为它使用的某些组件会与它们链接。

备注
calibre服务器还支持systemd套接字激活，因此如果需要，您也可以使用它。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 对比电子书

对比电子书

Calibre包括一个集成的电子书比较工具，可以用来查看电子书在编辑或转换后发生了什么变化。它可以比较EPUB和AZW3格式的图书。

要使用它，请在工具中打开电子书“编辑”，然后单击“文件->与其他图书比较”，或使用“书籍_详细信息”面板。如果执行从EPUB到EPUB的转换，则原始EPUB文件将另存为Original_EPUB。只需右键单击图书 详细信息面板中的Original_EPUB条目，然后选择“比较到EPUB格式”。

打开的比较工具将如下面的屏幕截图所示。它向你展示了所选书籍在文本、风格和图像方面的不同之处。

理解对比文件视图

从上面的截图中可以看出，比较视图显示了两本书的区别。只显示了它们之间的不同之处，以及它们周围有几行上下文。这样就很容易一目了然地看到像书籍这样的大文档中发生了哪些更改。

已添加的文本显示为绿色背景，删除的文本显示为红色背景，更改的文本显示为蓝色背景。

所有更改的文本的行号都显示在边上，这样可以很容易地转到编辑器中的特定更改。当您从编辑器中打开比较工具时，您还可以双击右侧面板中的一行，以自动转到编辑器中的该行。

比较书籍时一种有用的技术是告诉比较工具在计算差异之前先美化文本和文件样式。这通常可以导致更清晰，更容易理解差异。 要执行此操作，请单击右下角的“选项”按钮，然后选择“比较前美化文件”。请注意，美化有时可能会产生不需要的效果，因为它可能会导致更改无效的标记以使其有效。还可以通过“选项”按钮更改显示在差异周围的上下文行数。

可以通过底部的搜索栏搜索差异中的任何文本。您需要指定要搜索的面板，是“左边”还是“右边”。

运行对比工具

当您有同一本书的两个版本，并且您想要查看它们之间的不同之处时，比较工具最有用。为此，有几种方法可以启动该工具。

对比两个电子书文件

在“编辑”工具中打开第一个文件。现在点击“文件->与另一本书比较”并选择第二个文件(它必须与第一个文件的格式相同)。将打开比较视图，右侧显示正在编辑的文件，左侧显示第二个文件。

在 ORIGINAL_FMT 和 FMT 之间比较

当您执行从FMT到其自身的Calibre转换时，原始文件将另存为Original_FMT。您可以通过右键单击主Calibre窗口中“书籍_详细信息”面板中的Original_fmt条目并选择“与FMT比较”来查看转换更改了哪些内容。比较视图将打开，左边是原始的FMT，右边是FMT。

在编辑时比较书籍的还原点和当前状态

“编辑”工具有一个非常有用的功能，叫做“检查点”。这允许您将图书的当前状态保存为命名的*检查点*，如果不喜欢创建检查点后所做的更改，可以将其还原到该状态。当您在编辑器中执行各种自动操作时，也会自动创建检查点。您可以通过以下方式查看检查点列表，转到“查看->检查点”查看检查点列表，然后使用“比较”按钮将所选检查点的图书与当前状态进行比较。比较工具将在左侧显示检查点，在右侧显示当前状态。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 编辑电子书元数据

编辑电子书元数据

目录

- 编辑一本书的元数据

- 下载元数据

- 管理书籍格式

- 有关封面

- 同时编辑多本书的元数据

- 搜索与替换

- 批量下载元数据

- 将额外的数据文件添加到图书

电子书存在各种不同的类型和大小，往往他们的元数据（如标题、作者、序列表、出版商）是不完整或者不正确的。在 calibre 中最简单修改元数据的方式是在对应的元数据条目上双击修改，调整为正确的值。如果是更复杂的编辑，可以使用下文中讨论的元数据工具来达到实现 “增强编辑”

编辑一本书的元数据

选中你想要编辑的书籍，点击编辑元数据按钮或者按下字母 E。会打开一个对话框，允许你编辑各类的元数据信息。对话框有很多功能允许你快速有效率的编辑，下面有一些经常使用的实用技巧：

- 你可以点击在标题和作者中间的按钮来自动互换它俩的值

- 你可以通过点击按作者排序边上的按钮来让 calibre 自动填充排序好的值。使用 :guilable: 管理作者 对话框可以列出及变更按作者排序的值。这个对话框也可以通过点击并长按作者排序边上的按钮

- 你可以点击标签编辑边上的标签管理按钮来管理所有和这本书相关的标签

- “ids” 框可以用来输入 ISBN(获取其他类型的 id)，如果输入了无效的 ISBN，背景色会变成红色。如果是有效的 ISBN ，背景色会变成绿色

- 如果按作者排序的值和 calibre 计算出来的值不一致，按作者排序的框会变成红色

下载元数据

编辑元数据对话框最出色的功能是它能够从各种网站获取的元数据来自动填充许多元数据字段。目前，calibre使用谷歌图书和亚马逊。元数据下载可以为您填写标题、作者、系列、标签、评分、说明和ISBN。

要使用下载，请填写标题和作者字段，然后单击“提取元数据”按钮。Calibre将为您提供与书名和作者最匹配的图书列表。如果您首先填写ISBN字段，它将优先于标题和作者使用。如果找不到匹配项，请尝试通过在标题中仅包含一些关键字和仅包含作者姓氏来使您的搜索不那么具体。

管理书籍格式

在Calibre中，单个图书条目可以有许多不同的*格式*与其关联。例如，您可能获得了EPUB格式的莎士比亚全集，然后将其转换为MOBI在您的Kindle上阅读。Calibre自动为您管理多种格式。在编辑元数据对话框的“可用格式”部分中，您可以管理这些格式。您可以添加新格式、删除现有格式，还可以要求Calibre从其中一种格式的元数据中设置图书条目的元数据和封面。

有关封面

你可以让Calibre为你下载书籍封面，前提是这本书有一个已知的ISBN。或者，您可以指定计算机上的一个文件作为封面。Calibre甚至可以为你生成一个带有基本元数据的默认封面。您可以将图像拖放到封面上进行更改，也可以右键单击以复制/粘贴封面图像。

此外，还有一个按钮可以自动从封面上裁剪边框，以防封面图像的边框很不美观。

同时编辑多本书的元数据

首先，通过按住Ctrl或Shift键并单击来选择要编辑的书籍。如果您选择了多本图书，点击：guilabel：`编辑元数据`按钮将会打开一个新的*批量*元数据编辑对话框。使用此对话框可以快速将一组图书的作者/出版商/评级/标签/系列等设置为相同的值。如果您刚刚导入了许多具有某些共同元数据的图书，则此功能尤其有用。此对话框非常强大，例如，它有一个搜索和替换选项卡，您可以使用该选项卡对元数据执行批量操作，甚至将元数据从一列复制到另一列。

普通的编辑元数据对话框会有下一个和上一个按钮，你可以通过这两个按钮逐次编辑多本书的元数据

搜索与替换
guilabel：批量元数据编辑`对话框允许您对所选图书执行任意强大的搜索和替换操作。默认情况下，它使用简单的文本搜索和替换，但也支持*正则表达式*。有关正则表达式的详细信息，请参阅：ref：`正则表达式指南。

如上所述，有两种搜索和替换模式：字符匹配和正则表达式。字符匹配将在您选择的`搜索字段`中查找您在`搜索‘框中键入的字符，并将这些字符替换为您在`替换为’框中键入的字符。该字段中每次出现的搜索字符都将被替换。例如，假设要搜索的字段包含`a bad cat`。如果搜索`a`替换为`HELLO`，则结果为`hello bHELLOd cHELLOt`。

如果您正在搜索的字段是类似于标记的“多个”字段，则每个标记都会被单独处理。例如，如果您的标签包含“Horror，Scary”，则搜索表达式“r，”将不会匹配任何内容，因为该表达式会先应用于“Horror”，然后应用于“Scary”。

如果您希望搜索忽略大小写差异，请取消选中`区分大小写`框。

您可以通过从`替换后应用函数‘框中选择一个函数，让Calibre更改结果的大小写(替换发生后的信息)。可用的操作包括：

- 小写–将字段中的所有字符都改为小写

- 大写–将字段中的所有字符都改为大写

- `标题大写‘–将结果中的每个单词大写。

您可以在`您的测试`框中输入文本，以检查搜索/替换是否正在执行您想要的操作。在大多数情况下，书籍测试箱就足够了，但可能有一个您想要检查的案例没有显示在这些框中。将该案例输入到`您的测试`中。

正则表达式模式与字符模式有一些不同，除了(当然)使用正则表达式之外。首先，函数应用于与搜索字符串匹配的字符串部分，而不是整个字段。第二个是函数应用于替换字符串，而不是整个字段。

第三，也是最重要的一点是，替换字符串可以通过使用反向引用来引用部分搜索字符串。反向引用是``\n``，其中n是引用搜索表达式中第n个带括号的组的整数。例如，给出与上面相同的示例，一只坏猫，一个搜索表达式`a(.)(.)`，一个替换表达式`a\2\1`，结果将是`一只猫坏`。有关反向引用的更多信息，请参阅：ref：正则表达式学习指南。

一个有用的模式是：假设您想要更改整个字段的大小写。要做到这一点，最简单的方法是使用字符模式，但让我们进一步假设您想要使用正则表达式模式。搜索表达式应为`(^.*$)`，替换表达式应为`\1`，并选择所需的大小写转换函数。

最后，在正则表达式模式下，您可以将值从一个字段复制到另一个字段。只需使源字段和目标字段不同即可。副本可以替换目标字段、添加到字段的前缀(添加到前面)或附加到字段(添加到末尾)。“使用逗号”复选框告知Calibre在预加和追加模式下在文本和目标字段之间添加逗号(或不加)。如果目的地是多个(例如，标签)，则不能取消选中此框。

搜索和替换是在应用其他选项卡中的所有其他元数据更改后完成的。这可能会导致一些混淆，因为测试框会在其他更改之前显示信息，但操作将在其他更改之后应用。如果您对将要发生的事情有任何疑问，请不要将搜索/替换与其他更改混为一谈。

批量下载元数据

如果你想同时下载多本书的元数据，右键点击编辑元数据按钮，选择下载元数据按钮。你可以选择只下载元数据，只下载封面，或者两者都下载

将额外的数据文件添加到图书
calibre可以存储与一本书相关联的任何数量的额外数据文件。这些可以是备用封面、补充材料等。它们不能直接查看或用作转换源。全文搜索引擎也没有为它们编制索引。要查看/添加/删除它们，请选择图书，右键单击“编辑元数据”按钮，然后选择“管理数据文件”。这将弹出一个窗口，您可以在其中对这些文件执行操作。或者，您可以右键单击“添加图书”按钮，然后选择：“将数据文件添加到所选图书记录中”以更快地添加数据文件。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 常见问题

常见问题

目录

- 电子书格式转换

- 设备集成

- 书库管理

- 杂项

电子书格式转换

目录

- calibre支持哪些格式的转换？

- 哪些格式是最好的转换源格式呢？

- 我转换了一个PDF文件，但输出结果有很多问题？

- 如果我需要转换的文件中含有非英文字符或者引用时该怎么办？

- 怎么处理 MOBI 文件中的目录？

- 如何按特定顺序转换一组HTML文件？

- 我用calibre 生成的EPUB是不是有效？

- 如何使用转换工具的一些高级功能？

calibre支持哪些格式的转换？

calibre支持多种格式之间的相互转换，其可以将如下所有的输入格式转换成所有的输出格式。

输入格式: AZW, AZW3, AZW4, CBZ, CBR, CB7, CBC, CHM, DJVU, DOCX, EPUB, FB2, FBZ, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ

输出格式: AZW3, EPUB, DOCX, FB2, HTMLZ, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

备注

PRC是一种通用格式，calibre支持含有TextRead和MOBIBook头部的PRC文件。PDB也是一种通用格式，calibre支持eReader、Plucker（仅支持输入）、PML和zTxt等PDB文件。对DJVU的支持仅限于转换含有内嵌文本的DJVU 文件，这些文件通常是由OCR软件生成的。MOBI书籍分为Mobi6和KF8两种格式，calibre对其均提供完整的支持。MOBI文件的拓展名通常为.azw或.azw3。由微软Word2007及其后续版本生成的DOCX文件也支持转换。

哪些格式是最好的转换源格式呢？

按照优先级由高到低排列如下：LIT, MOBI, AZW, EPUB, AZW3, FB2, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

我转换了一个PDF文件，但输出结果有很多问题？

PDF是一种比较不理想的转换源格式。转换PDF文件时你可能遇到的一些问题，详见： 转换PDF文档。

如果我需要转换的文件中含有非英文字符或者引用时该怎么办？

这个问题有两个方面：

1. 了解源文件的编码：calibre 尝试猜测源文件使用的字符编码，但通常这是不可能的，因此您需要告诉它使用什么编码。 这可以在 GUI 中通过转换对话框的“外观 -> 文本”部分中的“输入字符编码”字段来完成。 命令行工具有一个“ebook-convert-txt-input –input-encoding”选项。

2. 将 HTML 文件添加到 calibre 时，您可能需要告诉 calibre 文件的编码方式。为此，请转到“首选项->高级->插件->文件类型”并自定义“HTML 到 ZIP”插件，告诉它 您的 HTML 文件采用什么编码。现在，当您将 HTML 文件添加到 calibre 时，它们将被正确处理。 不同来源的 HTML 文件通常具有不同的编码，因此您可能必须重复更改此设置。 网络上许多文件的常见编码是“cp1252”，我建议您首先尝试一下。 请注意，转换 HTML 文件时，请将上面提到的输入编码设置留空。 这是因为“HTML转ZIP”插件会自动将 HTML 文件转换为标准编码 (UTF-8)。

怎么处理 MOBI 文件中的目录？

首先要意识到的是，大多数电子书都有两个目录。一种是传统的目录，就像你在纸质书中找到的目录ToC。此目录是主文档流的一部分，可以根据您的喜好进行样式设置。此目录称为“内容目录*”。

然后是元数据目录*。元数据目录是指不属于书籍文本的目录，通常由阅读器上的某个特殊按钮访问。例如，在calibre电子书阅读器中，您可以使用“显示目录”按钮来查看此目录。该目录不能由图书创建者设置样式。它的表现方式取决于阅读器程序。

在MOBI格式中，情况有点混乱。这是因为MOBI格式，在主流电子书格式中，*没有*对元数据目录的良好支持。一本MOBI书通过在书的末尾放置一个额外的内容目录来模拟元数据目录的存在。当你在Kindle上点击“转到目录”时，Kindle就会把你带到这个额外的目录内容中。

现在，很可能会在你看来，MOBI电子书有两个相同的目录。请记住，一个是字面意义上的目录，而另一个则是元数据目录，即使这两个可能具有完全相同的条目，看起来也一样。一个可以直接从Kindle的菜单进行访问，而另一个则不能。

转换为 MOBI 时，calibre 会检测输入文档中的*元数据目录*，并在输出 MOBI 文件中生成文件结束目录。 您可以通过 MOBI 输出设置中的选项将其关闭。 您还可以通过 MOBI 输出设置中的选项告诉 calibre 是将其放在书的开头还是结尾。 请记住，此目录在语义上是*元数据目录*，在 MOBI 以外的任何格式中，它*不能不是文本的一部分*。 它成为 MOBI 文本的一部分这一事实是由于 MOBI 的局限性而造成的意外。 如果您希望文档文本中的特定位置有一个目录，请手动创建一个。 因此，我们强烈建议您保留默认值，即元数据目录位于本书末尾。 另请注意，如果您禁用文件结尾目录的生成，则生成的 MOBI 文件可能无法在 Kindle 上正常运行，因为 Kindle 将元数据目录用于许多功能，包括翻页功能。

如果您在输入文档中有手动编辑的目录，您可以使用calibre中的目录检测选项自动从中生成元数据目录。有关如何使用这些选项的更多 详细信息，请参阅用户手册的转换部分。

最后，我鼓励你抛弃内容目录，在你的电子书中只有一个元数据目录。元数据目录将为阅读你的电子书的人提供更好的导航体验(除了在Kindle上，它们本质上与内容目录相同)。

备注

较新的AZW3格式对元数据目录有适当的支持。但是，如果您禁用文件结尾内联目录的生成，Kindle固件可能会出现故障。因此，建议您不要使用生成的目录。如果您创建的AZW3文件包含元数据目录，并且没有文件结尾生成的目录，则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文件添加到图形用户界面，并使用“转换”按钮创建您的电子书。您可以使用转换对话框中目录部分中的选项来控制如何生成目录。

备注

默认情况下，添加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以*广度优先*的顺序添加您的文件。要做到这一点，请转到“首选项->高级->插件->文件类型”并定制“Html转ZIP”插件。

我用calibre 生成的EPUB是不是有效？

calibre 不保证其制作的 EPUB 有效。 它唯一的保证是，如果您向其提供有效的 XHTML 1.1 + CSS 2.1，它将输出有效的 EPUB。 calibre 努力确保其生成的 EPUB 在各种设备上实际按预期工作，这一目标与生成有效的 EPUB 不相容，但对其绝大多数用户而言更为重要。 如果您需要一个始终生成有效 EPUB 的工具，那么 calibre 不适合您。 这意味着，如果您想将 calibre 制作的 EPUB 发送到使用 EPUB 有效性检查器的在线商店，您必须自己确保 EPUB 有效，calibre 不会为您做这件事 - 换句话说，您必须 feed 口径有效的 XHTML + CSS 作为输入文档。

如何使用转换工具的一些高级功能？

您可以通过在 GUI 中将鼠标悬停在转换器上或在终端上运行“ebook-convert dummy.html .epub -h”来获得有关转换器的任何单独功能的帮助。 一个好的起点是查看以下演示文件，该文件演示了一些高级功能“html-demo.zip <html-demo.zip>”。

设备集成

目录

- Calibre 支持哪些设备？

- 我如何才能帮助我的设备获得calibre支持？

- 我的设备没有被calibre检测到吗？

- 我的设备不是标准的或不寻常的。我可以做些什么来连接到它？

- 如何在iPad/iPhone/iPod Touch中使用Calibre？

- 如何在我的安卓手机/平板电脑或Kindle Fire上使用Calibre？

- 我可以使用Kindle或其他阅读设备中的网络浏览器访问我的calibre图书吗？

- 我不能使用Calibre发送电子邮件？

- 我的设备在Linux中以只读方式安装，因此Calibre无法连接到它？

- 为什么Calibre不支持Kindle上的 收藏或Nook上的货架？

- 当我尝试在我的Kobo Touch/Glo/等中使用Calibre时出现错误？

- 我发送到我的电子墨水Kindle上的书籍的封面会瞬间出现，然后被一个通用的封面取代？

- 我的MOBI文件的封面已经不再出现在适用于PC的Kindle/适用于Android/iPad的Kindle等等。

- 我用Calibre把一些书转移到我的Kindle上，它们没有出现吗？

Calibre 支持哪些设备？

Calibre可以直接连接到所有主要(和大多数次要的)电子书阅读设备、智能手机、平板电脑等。此外，使用“连接到文件夹”功能，您可以将其用于任何将自身导出为U盘的电子书阅读器。最后，您可以使用Calibre内容服务器无线连接到任何具有Web浏览器的设备。

我如何才能帮助我的设备获得calibre支持？

如果您的设备在操作系统中显示为USB磁盘，则将对其的支持添加到Calibre非常容易。我们只需要你提供一些信息：

- 您的设备支持的电子书格式的完整列表。

- 设备上有没有一个特殊的文件夹来放置所有的电子书文件？此外，设备是否会检测子文件夹中的文件？

- 我们还需要关于您的设备的信息，Calibre将自动收集这些信息。首先，如果您的设备支持SD卡，请插入SD卡。然后将您的设备连接到计算机。在Calibre中，转到“首选项->其他”，然后单击“调试设备检测”按钮。这将创建一些调试输出。将其复制到一个文件并重复该过程，这一次是在设备与计算机断开连接的情况下。

- 将上述两个输出和其他信息发送给我们，我们将为您的设备编写设备驱动程序。

一旦您向我们发送了特定操作系统的输出，该操作系统中对该设备的支持将出现在下一版本的Calibre中。要向我们发送输出，请打开错误报告并将输出附加到它。参见“如何报告错误<bugs>”。

我的设备没有被calibre检测到吗？

按下列步骤找出问题：

- 确保一次只将一台设备连接到您的计算机。不要同时拥有iPhone/iPad等支持另一种calibre的设备。

- 如果您连接的是Apple iDevice(iPad、iPod Touch、iPhone)，Apple不再允许第三方软件使用USB电缆连接到他们的设备。取而代之的是使用无线连接，通过Calibre内容服务器。

- 如果您连接的是Kindle Fire或其他Android设备，请阅读“android_usb”下的说明。

- 在MacOS上，如果你在将设备连接到Calibre时遇到权限错误，你可以通过查看以下内容来修复：“ 系统 首选项>安全和隐私>隐私>文件和文件夹”。

- 确保您运行的是最新版本的 calibre（当前为 7.10.0）。 最新版本始终可以从“calibre 网站 <download>”下载。 您可以通过查看主 calibre 窗口的底行来了解当前运行的 calibre 版本。

- 确保您的操作系统正在检测到该设备。也就是说，设备应该显示在Windows资源管理器(Windows)或Finder(MacOS)中。

- 在Calibre中，转到“首选项->忽略的设备”并检查您的设备是否未被忽略

- 如果上述所有步骤均失败，请转至“首选项 -> 其他”，然后在附加设备的情况下单击“调试设备检测”，并将输出作为票证发布在“calibre bug 跟踪器 <https://bugs.launchpad.net/calibre>上 ”_。

我的设备不是标准的或不寻常的。我可以做些什么来连接到它？

除了“连接/共享”按钮下的“连接到文件夹”功能外，Calibre还提供了一个``用户定义的``设备插件，可用于连接到操作系统中显示为磁盘驱动器的任何USB设备。注：在Windows上，设备必须有驱动器号才能使用Calibre。查看设备插件`` 首选项->插件->设备插件->用户定义的``和``首选项->其他->获取设置用户定义的设备的信息``了解更多信息。请注意，如果您使用的是内置口径插件通常检测到的设备的用户定义插件，则必须首先禁用内置插件，以便使用您的用户定义插件。

如何在iPad/iPhone/iPod Touch中使用Calibre？

从你的苹果设备上浏览你的calibre收藏有一个简单的方法，就是使用“服务器”，这样你的收藏就可以在网上获得。首先按calibre执行以下步骤

- 将 calibre 中的首选输出格式设置为 EPUB（可以在“首选项->界面->操作”下设置输出格式）

- 在“首选项 -> 转换 -> 常用选项 -> 页面设置”下，将输出配置文件设置为 iPad（这也适用于 iPhone/iPod）

- 选择您想要在 iDevice 上阅读的书籍并单击“转换”按钮，将其转换为 EPUB 格式。

- 单击“连接/共享”按钮打开内容服务器并保持 calibre 运行。 您还可以通过“首选项->共享->通过网络共享”告诉 calibre 自动启动内容服务器。

内容服务器允许您直接在 Safari 本身中阅读书籍。 此外，您的 iDevice 有许多应用程序可以连接到 calibre 内容服务器。 例子包括：Marvin、Mapleread 和 iBooks 本身。

使用内容服务器

启动Safari浏览器并输入运行Calibre服务器的计算机的IP地址和端口，如下所示：

http://192.168.1.2:8080/

将``192.168.1.2`替换为运行Calibre的计算机的本地IP地址。有关运行服务器和找到正确的IP地址的 详细信息，请参阅“服务器”。

你会在Safari中看到一个图书列表，点击任何一本书，你就可以选择下载它，或者在浏览器中阅读它。如果你选择下载，Safari会询问你是否想用iBooks打开它。

许多阅读应用程序支持通过其“OPDS <httpd://opds.io/>”_支持直接浏览 calibre 书库。 在此类应用程序中，您可以转到在线目录屏幕并添加 calibre 服务器的 IP 地址，以便在应用程序内浏览和下载 calibre书库中的书籍。

如何在我的安卓手机/平板电脑或Kindle Fire上使用Calibre？

您可以通过两种方式将 Android 设备连接到 calibre。 使用 USB 电缆——或者通过无线方式。 使用 Android 设备的第一步是在其上安装电子书阅读应用程序。 Android 有许多免费和付费电子书阅读应用程序：一些示例（排名不分先后）：“FBReader <https://play.google.com/store/apps/details?id=org.geometerplus.zlibrary.ui.android&hl=en>”_，“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（媒体传输模式）”之类的内容。 如果没有，请点击通知，然后将模式更改为媒体传输 (MTP)。 此时您可能需要重新启动 calibre 才能识别您的设备。 最后，每次 calibre 或操作系统实际尝试连接到设备时，您可能会在设备上看到一个弹出窗口，请求许可，然后点击“确定”。

备注

对于Kindle Fire 8或更新版本，当插入USB数据线时会出现一个图标，表明设备正在充电。点击该按钮，将设备切换到数据传输模式，然后启动Calibre，然后应该会检测到它。

OTA升级

Calibre有一个内置的Web服务器：“服务器”。它使您的calibre收集在网上可用。您可以使用简单的浏览器或专用应用程序在您的设备上浏览它。首先按calibre执行以下步骤：

- 将 calibre 中的“首选输出格式”设置为普通 Android 设备的 EPUB 或 Kindle 的 MOBI（可以在“首选项->界面->操作”下设置输出格式）

- 将您想要在设备上阅读的图书转换为EPUB/MOBI格式，方法是选择它们并单击“转换”按钮。

- 在 calibre 首选项 中打开“内容服务器”并保持 calibre 运行。

现在，在您的Android设备上，打开浏览器并浏览到

http://192.168.1.2:8080/

将``192.168.1.2`替换为运行Calibre的计算机的本地IP地址。有关运行服务器和找到正确的IP地址的 详细信息，请参阅“服务器”。

你现在可以浏览你的图书收藏，并将图书从Calibre下载到你的设备上，然后用你Android设备上的任何电子书阅读软件打开。

许多阅读应用程序支持通过其“OPDS <httpd://opds.io/>”_支持直接浏览 calibre 书库。 在此类应用程序中，您可以转到在线目录屏幕并添加 calibre 服务器的 IP 地址，以便在应用程序内浏览和下载 calibre书库中的书籍。

我可以使用Kindle或其他阅读设备中的网络浏览器访问我的calibre图书吗？

calibre 有一个*内容服务器*，可将 calibre 中的书籍导出为网页。 有关 详细信息，请参阅“服务器”。

某些设备，例如 Kindle (1/2/DX)，不允许您访问端口 8080（内容服务器运行的默认端口）。 在这种情况下，将 calibre Preferences 中的端口更改为 80。（在某些操作系统上，由于安全设置的原因，您可能无法在小于 1024 的端口号上运行服务器。在这种情况下，最简单的解决方案是调整 您的路由器将端口 80 上的请求转发到端口 8080）。

此外，一些设备没有足够高级的浏览器来运行内容服务器使用的类似APP的界面。对于这类设备，您只需在服务器URL上添加``/mobile`，就可以得到简化的、非JavaScript的界面。

我不能使用Calibre发送电子邮件？

由于电子邮件中存在大量垃圾邮件，发送电子邮件可能会很棘手，因为不同的邮件服务器使用不同的策略来阻止电子邮件。 最常见的问题是您是否以 calibre 直接发送电子邮件（没有邮件中继）。 许多服务器（例如亚马逊）会阻止并非来自知名中继的电子邮件。 在 calibre 中设置电子邮件发送的最可靠方法是执行以下操作：

- 在 GMX [https://www.gmx.com] 创建一个免费的 GMX 帐户 。

- 转到 calibre 中的“首选项->共享->通过电子邮件共享书籍”，然后单击“使用 GMX”按钮并填写所需信息。

- 在网站上登录您的 GMX 帐户 并启用 SMTP 发送（设置 -> POP3 和 IMAP -> 通过外部程序发送和接收电子邮件）

- 然后，Calibre将能够使用GMX发送邮件。

- 如果您正在向您的Kindle发送电子邮件，请记住更新您的Amazon Kindle页面上的电子邮件 首选项，以允许从您的GMX电子邮件地址发送电子邮件。还要注意的是，亚马逊不允许通过电子邮件发送AZW3和新风格(KF8)MOBI文件。最后，亚马逊最近开始向你的GMX账户发送确认电子邮件，你必须点击这些电子邮件才能真正交付图书。

即使在这样做之后，您也可能会遇到问题。问题的一个常见来源是，一些设计糟糕的杀毒程序阻止了Calibre打开连接发送电子邮件。尝试在您的防病毒程序中添加口径排除项。

备注

如果您使用 Microsoft/GMX 发送大量电子邮件，Microsoft/GMX 可能会禁用您的帐户。 因此，当使用这些服务发送邮件时， calibre 自动限制为每五分钟发送一本书。 如果您不介意冒 帐户被冻结的风险，可以通过转到 calibre 中的“首选项”->“高级”->“调整”来缩短此等待间隔。

备注

谷歌最近故意破坏了他们的电子邮件发送协议(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。似乎只有Kobo明白，生命太短暂了，不能在电子墨水屏幕上一个接一个地输入收藏😇

请注意，对于 Kindle，有一种方法可以通过 USB 操作 作品集，但它要求 Kindle *每次*与计算机断开连接时都重新启动，以便识别对集合的更改。 因此，任何 calibre 开发人员都不太可能有足够的动力来支持它。 然而，有一个 calibre 插件，允许您从 calibre 元数据在 Kindle 上创建作品集。 它可以从这里获取<https://www.mobileread.com/forums/showthread.php?t=244202>`_。

备注

亚马逊已经完全取消了在Kindle Touch和Kindle Fire等较新型号中操纵 收藏的功能，即使是上面的插件也变得毫无用处，除非你已经root Kindle并安装定制固件。

当我尝试在我的Kobo Touch/Glo/等中使用Calibre时出现错误？

Kobo 的固件有很多问题。 众所周知，连接到它会随机失败。 主板、USB 端口/电缆/集线器的某些组合可能会加剧这种失败的趋势。 如果您在使用 calibre 连接到触摸屏时遇到错误，请尝试以下操作，每个方法都已为*某些* calibre 用户解决了问题。

- 将Kobo直接连接到您的计算机，而不是通过USB集线器

- 在您的计算机上尝试不同的USB数据线和不同的USB端口

- 从Kobo注销并重新登录，这会导致它重建数据库，修复损坏的数据库错误。

- 尝试将Kobo Touch上的固件升级到最新版本

- 尝试重置Kobo(有时这会暂时解决问题，但它会再次出现，在这种情况下，您必须一次又一次地重置)

- 试着一次只放一两本书到Kobo上，不要在Kobo上放大量的 藏书

我发送到我的电子墨水Kindle上的书籍的封面会瞬间出现，然后被一个通用的封面取代？

发生这种情况是因为亚马逊的错误。 他们尝试从服务器下载该书的封面，当失败时，他们会用通用封面替换 calibre 创建的现有封面。 有关详细信息，请参阅“此论坛主题 <https://www.mobileread.com/forums/showthread.php?t=329945>”_。 从版本 4.17 开始，calibre 有一个解决方法，如果您在亚马逊销毁封面后将 Kindle 连接到 calibre，calibre 会自动恢复它们。 因此，要在 Kindle 上查看封面，您必须：

1. 把书送到有calibre的Kindle上

2. 断开Kindle的连接，等待亚马逊销毁封面

3. 将Kindle重新连接到Calibre

请注意，此解决方法仅适用于发送口径为4.17或更高版本的图书。或者，简单地把你的Kindle保持在飞行模式，你不会真的想让亚马逊知道你读过的每一本书。我鼓励您联系亚马逊客户支持，并大声抱怨此漏洞。也许亚马逊会听进去。

备注

如果解决方法对您无效，请确保您的Kindle固件版本至少为5.12.5，于2020年4月发布。

我的MOBI文件的封面已经不再出现在适用于PC的Kindle/适用于Android/iPad的Kindle等等。

这是由亚马逊软件中的错误引起的。 您可以通过转到“首选项->转换->输出选项->MOBI 输出”并设置“启用图书内容共享”选项来解决此问题。 如果您要重新转换以前转换的书籍，您还必须在该书籍的转换对话框中启用该选项（根据书籍转换设置保存并优先）。

请注意，这样做意味着生成的 MOBI 将显示在个人文档下，而不是 Kindle Fire 上的图书下，并且 Amazon Whispersync 将不起作用，但封面可以。 您可以选择哪个功能对您来说更重要。 我鼓励您联系亚马逊并要求他们修复此错误。

亚马逊软件的缺陷是，当你将一个MOBI文件放在Kindle上时，除非该文件被标记为个人文档，否则亚马逊会认为你是从它那里购买了这本书，并试图从它的服务器上为它下载封面缩略图。当下载失败时，它拒绝退回到MOBI文件中定义的封面。这很可能是亚马逊故意的，目的是试图迫使作者只能通过他们进行销售。换句话说，Kindle只显示标记为个人文档的书籍或直接从亚马逊购买的书籍的封面。

如果你使用USB连接将MOBI文件发送到拥有Calibre的电子墨水Kindle上，Calibre会通过上传封面缩略图本身来解决亚马逊的这个漏洞。但是，只有在使用USB连接并使用Calibre发送时，才有可能解决此问题。请注意，如果你使用电子邮件发送，亚马逊会自动将MOBI文件标记为个人文档，封面将起作用，但图书将显示在个人文档中。

我用Calibre把一些书转移到我的Kindle上，它们没有出现吗？

发送到Kindle的图书只有在被Kindle*索引后才会显示在Kindle上。这可能需要一些时间。如果书在一段时间后仍然没有出现，那么很可能是Kindle索引器崩溃了。有时，一本特定的书会导致索引器崩溃。不幸的是，亚马逊没有提供任何方法来推断哪本书导致了Kindle的崩溃。你唯一的办法是要么重置Kindle，要么使用Windows资源管理器(或你使用的任何文件管理器)从它的内存中删除所有文件，然后再一本接一本地将图书发送给它，直到你发现问题图书。一旦你找到了问题书，把它从Kindle上删除，然后进行MOBI到MOBI或MOBI到AZW3的转换，然后再把它寄回来。这很可能会解决这个问题。

书库管理

目录

- 图书文件存放在哪里？

- Calibre如何管理作者姓名和排序？

- 为什么 calibre 不允许我将书籍存储在我自己的文件夹结构中？

- 为什么 calibre 没有 foo 列？

- 我可以有一栏显示格式或 ISBN 吗？

- 如何将我的 calibre 数据从一台计算机转移到另一台计算机？

- calibre 的书籍列表是空白的！

- 我的 calibre 库在网络驱动器/NAS 上出现错误？

图书文件存放在哪里？

当你第一次运行Calibre时，它会要求你提供一个文件夹来存储你的书。每当您将图书添加到Calibre时，它都会将图书复制到该文件夹中。文件夹中的书籍按作者和标题很好地排列在子文件夹中。请注意，此文件夹的内容是按口径自动管理的，**请勿**手动将任何文件/文件夹添加到此文件夹，因为它们可能会被自动删除。如果要添加与特定图书相关联的文件，请使用”辑元数据”对话框的右上角区域来执行此操作。然后，Calibre会自动将文件放到正确的文件夹中，并在标题/作者发生变化时移动它。

关于图书的 元数据存储在库文件夹顶层的文件``metadata.db``中。该文件是一个SQLite数据库。在备份存储库时，请确保复制整个文件夹及其所有子文件夹。

库文件夹及其所有内容构成了所谓的calibre书库。您可以有多个这样的书库。要管理书库，请单击工具栏上的calibre图标。您可以轻松地创建新书库、删除/重命名现有书库以及在书库之间切换。

您可以在不同的书库之间复制或移动图书(如果您有多个图书馆设置)，方法是在图书上单击鼠标右键并选择“复制到书库”操作。

Calibre如何管理作者姓名和排序？

作者姓名很复杂，尤其是跨文化，请参阅“此注释 <https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en>”_ 了解某些复杂性。 calibre 有一个非常灵活的策略来管理作者姓名。 首先要明白的是，书籍和作者在口径上是独立的实体。 一本书可以有多个作者，一个作者也可以拥有多本书。 您可以通过编辑元数据对话框来管理书籍的作者。 您可以通过右键单击主 calibre 窗口左侧标签浏览器中的作者并选择“管理作者”来管理各个作者。 使用此对话框您可以更改作者的姓名以及该姓名的排序方式。 这将自动更改该作者所有书籍中的作者姓名。 当一本书有多个作者时，请使用 & 字符分隔他们的姓名。

现在来看作者姓名排序：

- 当将新作者添加到Calibre中时(每当添加新作者的图书时都会发生这种情况)，Calibre会自动计算图书和作者的排序字符串。

- 标签浏览器中的作者按**作者**的排序值进行排序。请记住，这与图书的作者排序字段不同。

- 默认情况下，此排序算法假定作者姓名采用``姓氏``格式，并生成``姓氏，名字``排序值。

- 您可以通过转到“首选项->高级->调整”并设置“author_sort_copy_method”调整来更改此算法。

- 您可以强制Calibre重新计算每个作者的作者排序值，方法是：右击任何作者并选择“管理作者”，然后按下“重新计算所有作者排序值”按钮。在将Author_Sort_Copy_方法调整设置为您想要的之后执行此操作。

- 您可以使用批量元数据编辑对话框强制Calibre重新计算所有图书的作者排序值(选择所有图书并单击编辑元数据，选中“自动设置作者排序”复选框，然后按确定)。

- 在重新计算图书的 作者排序值 时，Calibre使用每个单独作者的 作者排序值 。因此，在重新计算图书的 作者排序值 之前，请确保单个 作者的排序值 是正确的。

- 您可以通过在“首选项->高级->调整”中设置“categories_use_field_for_author_name”调整来控制标签浏览器是否使用作者姓名或排序值显示作者

请注意，您可以使用“管理作者”将单个作者的排序值设置为您想要的任何值。 当处理 calibre不正确的名字时，例如复杂的多部分名字，如 Miguel de Cervantes Saavedra，或处理亚洲名字，如 Sun Tzu时，这非常有用。

凭借所有这些灵活性，calibre 可以按照您喜欢的方式管理您的作者姓名。 例如，一种常见的请求是让 calibre 显示作者姓名 LN、FN。 为此，如果以下注释不适用于您，则：

- 如上所述，将“author_sort_copy_method”调整设置为“copy”。

- 重新启动Calibre。在执行其余步骤之前，不要更改任何图书元数据。

- 使用管理作者对话框将所有作者姓名更改为LN，FN。

- 更改所有作者后，按`重新计算所有作者排序值‘按钮。

- 按下OK，此时Calibre将更改您所有图书中的作者。这可能需要一段时间。

备注

从FN LN更改为LN，FN时，通常AUTHER_SORT中的值已经是LN，FN格式。如果这是您的情况，请执行以下操作：

- 如上所述，将“author_sort_copy_method”调整设置为“copy”。

- 重新启动Calibre。在执行其余步骤之前，不要更改任何图书元数据。

- 打开“管理作者”对话框。 按“将所有作者排序值复制到作者”按钮。

- 检查作者以确保您满意。 您仍然可以按“取消”放弃更改。 一旦按下“确定”，就无法撤消。

- 按下OK，此时Calibre将更改您所有图书中的作者。这可能需要一段时间。

为什么 calibre 不允许我将书籍存储在我自己的文件夹结构中？

calibre 图书馆管理功能的全部要点在于，它们提供了一个基于搜索和排序的界面来查找书籍，该界面比您为收藏想出的任何可能的文件夹方案“高效”得多。 事实上，一旦您习惯了使用 calibre 界面来查找、排序和浏览您的收藏，您将不再需要在磁盘上的文件中寻找书籍。 通过在作者 -> 标题 -> 图书文件自己的文件夹结构中管理图书，calibre 能够实现高水平的可靠性和标准化。 为了说明为什么基于搜索/标记的界面优于文件夹，请考虑以下内容。 假设您的藏书按照以下方案很好地分类到文件夹中：

Genre -> Author -> Series -> ReadStatus

现在，这使得找到例如艾萨克·阿西莫夫的《基础》系列中的所有科幻小说变得非常容易。 但假设您想查找所有未读的科幻小说。 使用此文件夹方案没有简单的方法来执行此操作，您需要一个如下所示的文件夹方案：

ReadStatus -> Genre -> Author -> Series

在 calibre 中，您可以使用标签来标记流派和阅读状态，然后只需使用简单的搜索查询，例如“tag:scifi 而不是 tag:read”。 calibre 甚至还有一个漂亮的图形界面，因此您无需学习其搜索语言，只需单击标签即可在搜索中包含或排除它们。

对于那些声称需要访问文件系统以便可以通过网络访问书籍的人来说，calibre 有一个出色的内容服务器，可以让您通过网络访问 calibre 图书馆。

如果您担心有一天 calibre 将停止开发，让您的所有书籍都被困在其文件夹结构中，请探索 calibre 中强大的“保存到磁盘”功能，该功能可让您将所有文件导出到基于以下内容的任意复杂性的文件夹结构中： 他们的元数据。

最后，每个标题文件夹末尾都有数字的原因是为了*稳健性*。 该数字是 calibre 数据库中图书记录的 ID 号。 该编号的存在使您可以拥有多个具有相同标题和作者姓名的记录。 如果数据库文件损坏，它也是允许 calibre 神奇地重新生成包含所有元数据的数据库的一部分。 鉴于 calibre 的使命是让您停止在文件名中存储元数据并停止使用文件系统来查找内容，因此 ID 号提供的增强的稳健性非常值得使用更丑陋的文件夹名称。

如果您仍然不相信，那么恐怕 calibre 不适合您。 在其他地方寻找您的图书编目需求。 我们很清楚，这不会改变。 请不要联系我们试图让我们改变这一点。

为什么 calibre 没有 foo 列？

calibre 旨在为最常用和最广泛使用的领域提供列。 此外，您可以添加任何您喜欢的列。 可以通过“首选项->界面->添加您自己的列”来添加列。 观看教程“UI Power Tips <demo#tutorials>”，了解如何创建自己的列，或阅读“此博文 <https://blog.calibre-ebook.com/calibre-custom-columns/>”_ 。

您还可以创建包含其他列元数据组合的“虚拟列”。 在添加列对话框中，使用“快速创建”链接轻松创建列以显示图书 ISBN 或格式。 您可以使用强大的 calibre 模板语言对列执行更多操作。 有关更多详细信息，请参阅“templatelangcalibre”。

我可以有一栏显示格式或 ISBN 吗？

是的你可以。 按照上面答案中的说明添加自定义列。

如何将我的 calibre 数据从一台计算机转移到另一台计算机？

您可以导出所有 calibre 数据（书籍、设置和插件），然后将其导入到另一台计算机上。 首先我们看看如何导出数据：

- 右键单击主 calibre 工具栏中的 calibre 图标，然后选择“导出/导入所有 calibre 数据”。 请注意，如果当前已连接设备，则此菜单选项将不可用 - 因此，请断开所有连接的设备。 然后单击标有“导出所有口径数据”的按钮。 您将看到所有 calibre 库的列表。 单击“确定”并在计算机上的某个位置选择一个空文件夹。 导出的数据将保存在该文件夹中。 只需将此文件夹复制到您的新计算机，然后按照以下说明导入数据即可。

- 在您的新计算机上安装 calibre 并运行“欢迎向导”，您在那里做什么并不重要，因为您将在下一步中导入旧设置。 您现在将拥有一个空 calibre，书库中只有“入门”指南。 再次右键单击口径按钮并选择“导出/导入所有 calibre数据”。 然后单击标有“导入以前导出的数据”的按钮。 选择包含您之前复制的导出数据的文件夹。 您现在将拥有可以导入的书库的列表。 逐一浏览列表，然后为每个书库选择新位置（位置只是计算机上某个位置的空文件夹）。 单击“确定”。 导入完成后，calibre 将重新启动，并包含所有旧的书库、设置和 calibre 插件。

备注

此导入/导出功能仅从 calibre 版本 2.47 开始可用。 如果您有旧版本的 calibre，或者遇到导入/导出问题，您可以手动复制 calibre 书库文件夹，如下一段所述。

只需将 calibre 书库文件夹从旧计算机复制到新计算机即可。 您可以通过单击工具栏中的 calibre 图标来了解书库文件夹是什么。 选择“切换/创建 calibre 书库”操作，您将看到当前 calibre 书库的路径。

现在在新计算机上，首次启动 calibre。 它将运行“欢迎向导”，询问您 calibre 书库的位置。 将其指向之前复制的文件夹。 如果您要转移到的计算机已经安装了 calibre，则“欢迎向导”将不会运行。 在这种情况下，右键单击工具栏中的 calibre 图标并将其指向新复制的文件夹。 现在，您的计算机上将有两个 calibre 书库，您可以通过单击工具栏上的 calibre 图标在它们之间切换。 以这种方式传输您的书库可以保留您的所有元数据、标签、自定义列等。

calibre 的书籍列表是空白的！

为了理解为什么会发生这种情况，您必须了解什么是 calibre 书库。 在最基本的层面上，calibre 书库只是一个文件夹。 每当您将一本图书添加到 calibre 时，该书的文件都会复制到此文件夹中（按作者和标题排列到子文件夹中）。 在 calibre书库文件夹的顶层，您将看到一个名为 metadata.db 的文件。 此文件是 calibre 存储 calibre 书库中*每本书的 元数据的位置，例如标题/作者/评级/标签等。 calibre 显示的书籍列表是通过读取这个 metadata.db 文件的内容来创建的。

calibre 显示空图书列表可能有两个原因：

- 您的 calibre 书库文件夹已更改位置。 如果它位于外部磁盘上并且该磁盘的驱动器号已更改，则可能会发生这种情况。 或者如果您不小心移动了该文件夹。 在这种情况下，calibre 无法找到其书库，因此会以空书库启动。 要解决此问题，请右键单击 calibre 工具栏中的 calibre 图标，然后选择切换/创建书库。 单击蓝色小图标选择 calibre 书库的新位置，然后单击“确定”。 如果您不知道新位置，请在计算机中搜索文件 “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 Drive 与 calibre 不兼容，如果您将 calibre 库放入 Google Drive，您将遭受数据丢失。 有关详细信息，请参阅“此线程 <https://www.mobileread.com/forums/showthread.php?t=205581>”_。

杂项

目录

- 亚马逊将停止通过电子邮件发送 MOBI 文件？

- 我想要 calibre 从我最喜欢的新闻网站下载新闻。

- 为什么叫 calibre？

- 为什么 calibre 在 macOS 上只显示我的部分字体？

- calibre 在 Windows 上无法启动？

- calibre 偶尔会冻结/崩溃？

- calibre 电子书阅读器和编辑书籍工具无法在 Windows 上运行？

- 在 Windows 上使用查看器或执行任何转换会导致权限被拒绝错误

- calibre 在 macOS 上无法启动/崩溃？

- 运行 calibre 电子书查看器时，我只能看到黑屏或白屏？

- 我下载了安装程序，但它不起作用？

- 我的防病毒程序声称 calibre 是病毒/木马？

- 如何备份 calibre？

- 如何使用购买的 calibre EPUB 图书（或者如何处理 .acsm 文件）？

- 我收到“权限被拒绝”错误？

- 我可以让评论元数据显示在我的阅读器上吗？

- 我如何才能使用我的HTTP代理？

- 我想要一些功能增加到calibre。我能做什么?

- Calibre 为什么没有自动更新？

- Calibre 授权协议是什么？

- 怎样在 U 盘上运行 Calibre？

- 如何在我自己的Linux服务器上运行新闻下载和内容服务器等功能？

亚马逊将停止通过电子邮件发送 MOBI 文件？

亚马逊“已宣布 <https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/>`__ 他们将停止 接受通过电子邮件发送到“@kindle.com”电子邮件地址的 MOBI 文件。 您可以指示 calibre 发送 EPUB 而不是 MOBI，方法是转到“首选项 -> 通过电子邮件共享图书”，然后从要发送到您的“@kindle.com”电子邮件地址的格式列表中删除 MOBI，然后添加 EPUB。

但请注意，亚马逊的 EPUB 接收存在很大缺陷，他们会拒绝许多在其他地方可用的 EPUB 文件。 在这种情况下，您可以尝试以下技巧：

1. 将 EPUB 文件转换为 MOBI

2. 然后将 MOBI 文件转换回 EPUB 并发送生成的 EPUB 文件

这将删除所有高级格式、嵌入字体等，但会大大增加亚马逊接受 EPUB 的机会。

备注

如果您之前使用电子邮件发送通过 calibre 下载的期刊，则最好通过 USB 线发送这些期刊或通过 Kindle 的内置浏览器从 calibre 内容服务器下载它们。 但是，如果您想继续使用电子邮件发送，可以尝试在“首选项”->“行为”中将输出格式更改为 EPUB，然后 calibre 将以 EPUB 格式下载新闻。 亚马逊是否会接受 EPUB 是另一个问题。

我想要 calibre 从我最喜欢的新闻网站下载新闻。

如果您相当精通计算机，您可以教 calibre 从您选择的任何网站下载新闻。 要了解如何执行此操作，请参阅“新闻”。

否则，您可以通过在“calibre新闻源 论坛 <https://www.mobileread.com/forums/forumdisplay.php?f=228>”中发帖来请求特定的新闻网站。

为什么叫 calibre？

任君选择：

- 电子书籍转换器和书库

- 高品质*calibre* 产品

- 向 SONY Librie 致敬，它是第一款基于电子墨水的电子书阅读器

- 我老婆选的 ;-)

calibre 发音为 cal-i-ber 不是 ca-li-bre。 如果您想知道，calibre 是英国/英联邦 calibre 的拼写。 作为印度人，这对我来说是自然的拼写。

为什么 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 因其他程序正在使用而无法打开文件的错误，请执行以下操作：

- 卸载calibre

- 重启电脑

- 重新安装calibre。但不要从安装向导启动 calibre。

- 暂时禁用您的防病毒程序（为了安全起见，请先断开与互联网的连接）

- 查看您为 calibre 书库选择的文件夹内部。 如果您看到名为metadata.db 的文件，请将其删除。

- 启动calibre

- 从现在开始你应该可以正常启动calibre了。

- 如果您在升级 calibre 后收到有关 Python 函数意外终止的错误，请首先卸载 calibre，然后删除文件夹（如果存在） “C:\Program Files\Calibre” 和 “C:\Program Files\Calibre2” 。 现在重新安装，应该没问题了。

- 如果您在初次运行 calibre 时在“欢迎向导”中遇到错误，请尝试选择 “C:\library”等文件夹作为 calibre 书库(如果路径包含非英文字符，calibre 有时会遇到库位置问题 ，或仅数字等）

- 尝试以管理员身份运行它（右键单击图标并选择“以管理员身份运行”）

如果仍然无法启动，请启动命令提示符（按 Windows 键和 R；然后在出现的“运行”对话框中键入“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 by Xfinity、Spybot *、*Killer Network Manager、Nahimic UI Interface、Acronis True Image。

- 您使用的是 Wacom 品牌的 USB 鼠标/平板电脑。 Wacom 驱动程序与 calibre 使用的图形工具包之间不兼容。 尝试使用非 Wacom 鼠标。

- 在某些 64 位版本的 Windows 上，存在阻止 64 位 calibre正常工作的安全软件/设置。 如果您使用 64 位版本的 calibre，请尝试切换到 32 位版本。

- 如果当您尝试从 calibre 电子书查看器复制文本时发生崩溃，则很可能是由您运行的某些剪贴板监视/管理应用程序引起的。 把它关掉，你应该没问题。

- 如果崩溃特别发生在您使用文件对话框时，例如单击“添加书籍”按钮或“保存到磁盘”按钮，则说明您的计算机上安装了某些已损坏的 Shell 扩展的软件。 已知的罪魁祸首包括：SpiderOak、odrivesync 和*Dell Backup and Recovery* 以及*NetDrive*。 如果你有其中之一，卸载它们就可以了。 您还可以使用“NirSoft Shell 扩展查看器 <https://www.nirsoft.net/utils/shexview.html>”_ 查看系统上安装了哪些 shell 扩展，如果您不想，可以单独禁用它们 卸载完整程序。 请记住在禁用 shell 扩展后使用“重新启动资源管理器”或重新启动计算机。

如果以上情况均不适用于您，那么您的计算机上还有其他程序正在干扰 calibre。 首先以安全模式重新启动计算机，以尽可能少地运行程序，然后查看崩溃是否仍然发生。 如果没有，那么您就知道是某个程序导致了问题。 最有可能的罪魁祸首是修改其他程序行为的程序，例如防病毒程序、设备驱动程序、RoboForm（自动表单填写应用程序）之类的东西或语音控制或屏幕阅读器等辅助技术。

找到罪魁祸首的唯一方法是逐一删除这些程序，看看是哪个程序导致了问题。 基本上，停止程序，运行 calibre，检查崩溃情况。 如果仍然发生，请停止另一个程序并重复。

calibre 电子书阅读器和编辑书籍工具无法在 Windows 上运行？

这两个程序使用硬件加速，因为它们嵌入了 Chrome 浏览器版本来渲染 HTML。 如果它们不起作用，那是因为与您系统的 GPU（图形）驱动程序不兼容。 首先尝试更新这些，然后重新启动。 如果这不能解决问题，您可以将“QTWEBENGINE_CHROMIUM_FLAGS”环境变量设置为值“–disable-gpu”以关闭硬件加速。 有关详细信息，请参阅“此页面 <https://doc.qt.io/qt-6/qtwebengine-debugging.html>”。

在 Windows 上使用查看器或执行任何转换会导致权限被拒绝错误

您计算机上的某些内容阻止 calibre 访问其自己的临时文件。 您的“Temp”文件夹的权限很可能不正确。 在Windows资源管理器中转到文件夹文件：C:\Users\USERNAME\AppData\Local，然后右键单击文件：`Temp`文件夹，选择`属性`并转到`安全`选项卡 。 确保您的用户帐户对此文件夹具有完全控制权。

一些用户报告说，在管理员命令提示符中运行以下命令修复了他们的权限。 要获取管理员命令提示符，请在开始菜单中搜索 cmd.exe，然后右键单击命令提示符条目并选择“以管理员身份运行”。 在命令提示符处键入以下命令并按 Enter：

icacls "%appdata%\..\Local\Temp" /reset /T

或者，您可以以管理员身份运行 calibre，但这样做会导致某些功能，例如拖放，无法工作。

最后，一些用户报告禁用 UAC 可以解决该问题。

calibre 在 macOS 上无法启动/崩溃？

macOS 上失败的常见原因之一是使用了与图形工具包 calibre 使用不兼容的辅助功能。 如果您打开了 VoiceOver，请尝试将其关闭。 另请转到“系统偏好设置”->“系统”->“通用访问”，然后关闭所有选项卡中启用辅助设备访问的设置。 另一个原因可能是某些第三方应用程序修改了系统行为，例如 Smart Scroll。

您可以通过运行“Console.app”来获取有关 calibre 未启动原因的调试输出。 调试输出将打印到其中。 如果调试输出包含如下行：

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

那么问题可能是字体缓存损坏。 您可以按照这些`说明 <https://www.macworld.com/article/1139383/fontcacheclear.html>`_ 清除缓存。 如果这不能解决问题，请在系统上的 ~/Library/Fonts 等位置查找损坏的字体文件。 在 macOS 中检查损坏字体的一个简单方法是启动“字体簿”应用程序，选择所有字体，然后在“文件”菜单中选择“验证字体”。

运行 calibre 电子书查看器时，我只能看到黑屏或白屏？

这是因为查看器用来渲染的 Qt WebEngine 与系统上的 GPU 驱动程序之间不兼容。 首先尝试升级 GPU 驱动程序。 如果这没有帮助，您可以尝试通过将环境变量“QTWEBENGINE_CHROMIUM_FLAGS”设置为值“–disable-gpu”来关闭 Qt WebEngine 中的硬件加速。 有关如何更改环境变量的信息，请参阅“customize_env_vars”。

我下载了安装程序，但它不起作用？

从 Internet 下载有时会导致下载损坏。 如果您下载的 calibre 安装程序无法打开，请尝试重新下载。 如果重新下载不起作用，请从“备用位置 <https://github.com/kovidgoyal/calibre/releases/latest>”下载。 如果安装程序仍然无法运行，则表明您计算机上的某些内容阻止其运行。

- 尝试暂时禁用防病毒程序（Microsoft Security Essentials、卡巴斯基、诺顿、迈克菲或其他程序）。 如果升级过程停滞不前，这很可能是罪魁祸首。

- 同样，如果安装程序失败/回滚并且您正在运行 Microsoft PowerToys，请退出它。

- 尝试重新启动计算机并运行注册表清理程序，例如“Wise 注册表清理程序 <https://www.wisecleaner.com>”。

- 尝试全新安装。 也就是说，卸载 calibre，删除 C:\Program Files\Calibre2 （或者您之前选择安装 calibre 的任何位置）。 然后重新安装calibre。 请注意，卸载不会影响您的书籍或设置。

- 尝试使用备用浏览器下载安装程序。 例如，如果您使用的是 Microsoft Edge，请尝试使用 Firefox 或 Chrome。

- 如果您在 Windows 上收到有关缺少 DLL 的错误，则很可能是临时文件夹的权限不正确。 在 Windows 资源管理器中转到文件夹“C:\Users\USERNAME\AppData\Local”，然后右键单击“Temp”文件夹并选择“属性”并转到“安全”选项卡。 确保您的用户帐户对此文件夹具有完全控制权。

如果您仍然无法让安装程序运行并且您使用的是 Windows，则可以使用“calibre portable install <download_portable>”，它不需要安装程序（它只是一个 ZIP 文件）。

我的防病毒程序声称 calibre 是病毒/木马？

首先要检查的是您是否从“官方网站 <download>”下载calibre。 确保您点击左侧的下载链接，而不是右侧的广告。 calibre 是一个非常流行的程序，不法分子试图建立提供该程序下载的网站来愚弄粗心的人。

如果您有官方下载并且您的防病毒程序仍然声称 calibre 是病毒，那么您的防病毒程序是错误的。 防病毒程序使用启发式方法，即“看起来可疑”的代码模式来检测病毒。 这很像种族定性。 calibre 是一个完全开源的产品。 您实际上可以自己浏览源代码（或雇人为您做）来验证它不是病毒。 请将错误标识报告给您购买防病毒软件的任何公司。 如果防病毒程序阻止您下载/安装 calibre，请暂时禁用它，安装 calibre，然后重新启用它。

如何备份 calibre？

最重要的备份是 calibre 书库文件夹，其中包含您的所有书籍和元数据。 这是您第一次运行 calibre 时为 calibre 书库选择的文件夹。 您可以通过单击主工具栏上的 calibre 图标来获取书库文件夹的路径。 您必须备份此完整文件夹及其所有文件和子文件夹。

您只需单击工具栏上的 calibre 图标并选择备份书库文件夹即可将 calibre 切换为使用备份书库文件夹。 备份的书库文件夹会备份您的自定义列和保存的搜索以及所有书籍和元数据。

如果要备份 calibre 配置/插件，则必须备份 config 文件夹。 您可以通过“首选项->其他”找到此配置文件夹。 请注意，恢复配置文件夹不受官方支持，但在大多数情况下应该有效。 只需将备份文件夹的内容复制到当前配置文件夹即可恢复。

如何使用购买的 calibre EPUB 图书（或者如何处理 .acsm 文件）？

大多数购买的 EPUB 图书都有“DRM <drm>”。 这可以防止 calibre 打开它们。 您仍然可以使用 calibre 存储它们并将其传输到您的电子书阅读器。 首先，您必须在具有 Adobe Digital Editions 的 Windows 计算机上授权您的阅读器。 完成此操作后，使用 calibre 传输的 EPUB 图书将在您的阅读器上正常工作。 当您从网站购买 epub 图书时，您将获得一个“.acsm”文件。 该文件应使用 Adobe Digital Editions 打开，然后它将下载实际的“.epub”电子书。 电子书文件将存储在“My Digital Editions”文件夹中，您可以从这里将其添加到 calibre。

我收到“权限被拒绝”错误？

由于许多可能的原因，可能会发生权限被拒绝错误，但这些原因都与 calibre 无关。

- 如果您使用启用了写保护的 SD 卡，您可能会收到权限被拒绝错误。

- 在MacOS上，如果你在将设备连接到Calibre时遇到权限错误，你可以通过查看以下内容来修复：“ 系统 首选项>安全和隐私>隐私>文件和文件夹”。

- 如果您或您使用的某些程序将相关文件的文件权限更改为只读。

- 如果设备上存在文件系统错误，导致操作系统以只读模式挂载文件系统或将特定文件标记为只读待恢复。

- 如果文件的所有者设置为您以外的用户。

- 如果您的文件已在其他程序中打开。

- 如果文件驻留在设备上，则可能已达到设备根目录中最多256个文件的限制。在这种情况下，您需要使用FAT32文件系统重新格式化错误消息中提到的设备/SD卡，或者从SD卡/设备内存中删除一些文件。

在恢复使用Calibre之前，您需要修复权限错误的根本原因。仔细阅读错误消息，查看它指向的文件，并修复该文件或其包含的文件夹的权限。

我可以让评论元数据显示在我的阅读器上吗？

大多数读者并不支持这一点。你应该向制造商投诉，如果有足够多的人投诉，情况就会改变。同时，您可以在转换过程中使用“将元数据作为页面插入图书开头”的选项，将元数据包括注释插入到电子书开头的“封套页面”中。该选项位于转换设置的“结构检测”部分。请注意，要使这一点生效，您必须对书进行“转换”。如果您的图书已采用不需要转换的格式，则可以从该格式转换为相同的格式。

另一种选择是创建一个电子书形式的目录，其中包含您的Calibre书库中所有图书的列表，以及它们的元数据。单击并按住“转换”按钮以访问目录创建工具。在你问之前，不，你不能让目录直接链接到你的读者上的书。

我如何才能使用我的HTTP代理？

默认情况下，calibre 使用操作系统中设置的任何代理设置。 有时这些是不正确的，例如，在 Windows 上，如果您不使用 Microsoft Edge，则代理设置可能不是最新的。 您可以通过设置“http_proxy”和“https_proxy”环境变量来告诉 calibre 使用特定的代理服务器。 该变量的格式为：http://username:password@servername 您应该要求网络管理员为您提供该变量的正确值。 请注意，calibre 仅支持 HTTP 代理，不支持 SOCKS 代理。 您可以在“首选项”->“其他”中查看 calibre 当前使用的代理。

我想要一些功能增加到calibre。我能做什么?

你有两个选择:

1. 通过破解 calibre 创建补丁并将其发送给我进行审查和包含。 参见“开发 <get-involved>”。

2. “打开一个 bug，请求功能 <bugs>”。 请记住，虽然您可能认为您的功能请求极其重要/必不可少，但 calibre 开发人员可能不会同意。 幸运的是，calibre 是开源的，这意味着您始终可以选择自己实现您的功能，或者雇用某人来为您实现。 此外，calibre 有一个全面的插件架构，因此您可以将您的功能开发为插件，请参阅“pluginstutorial”。

Calibre 为什么没有自动更新？

有很多原因：

- 无需每周更新。如果你对Calibre的工作方式感到满意，关闭更新通知，继续你的快乐之路。回头看看你是否想要一年左右更新一次。在更新通知本身上有一个用于关闭更新通知的复选框。

- calibre 下载目前使用“每月约 150TB 的带宽<动态/下载>”。 实施自动更新会大大增加成本，最终每月要花费数千美元，而这是必须有人支付的。

- 如果我实现一个对话框来下载更新并启动它，而不是像现在那样访问网站，这将节省最热情的calibre更新器*每周最多点击五次*。在能力发展方面，有更优先的事情要做。

- 如果你真的，真的讨厌每周下载Calibre，但仍然想要更新到最新，我鼓励你从源码运行，这使得更新变得微不足道。说明如下：“可在此处查看 <develop>”。

- 在Calibre论坛中，有第三方自动更新器，供Calibre用户使用<https://www.mobileread.com/forums/forumdisplay.php?f=238>`_.

Calibre 授权协议是什么？

calibre 根据 GNU 通用公共许可证 v3（开源许可证）获得许可。 这意味着只要您提供源代码，您就可以自由地重新分发 calibre。 因此，如果您想将 calibre 与您的产品一起放入 CD 中，则还必须将 calibre 源代码放入 CD 中。 源代码可“下载 <https://download.calibre-ebook.com>”_。 您可以随意使用 calibre 转换的结果。 如果不将软件开源，您就无法在软件中使用 calibre 的代码或库。 有关详细信息，请参阅“GNU GPL v3 <https://www.gnu.org/licenses/gpl.html>”_。

怎样在 U 盘上运行 Calibre？

便携版本的calibre在“这里<download_portable>”下载。

如何在我自己的Linux服务器上运行新闻下载和内容服务器等功能？

首先，您必须在您的Linux服务器上安装Calibre。如果您的服务器使用的是现代的Linux发行版，那么在其上安装Calibre应该没有问题。

备注
calibre 需要 GLIBC >= 2.31 和 libstdc++ >= 6.0.28。 如果您有较旧的服务器，则需要从源代码编译它们，或者使用需要 GLIBC >= 2.17 的 calibre 3.48 或需要 GLIBC >= 2.13 的 2.85.1 或仅需要 GLIBC >= 2.10 的 calibre 1.48。 此外，虽然 calibre 命令行实用程序不需要正在运行的 X 服务器，但其中一些实用程序确实需要在您的系统上安装 X 服务器库。 这是因为 Qt，它用于各种图像处理任务，并链接到这些库。 如果您收到有关某些 Qt 模块的 ImportError 错误，则可能缺少某些 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 用户手册的“cli”部分，以了解有关这些命令和其他命令的更多信息。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程

教程

在这里你能找到使用 calibre 更多高级功能的教程，例如 XPath 以及模版。

- 添加你喜欢的新闻站点
- 管理图书的子组，例如“体裁”
- XPath教程
- calibre 模板语言
- 关于在 calibre 中使用正则表达式的所有内容
- 编写自己的插件来扩展 calibre 的功能
- 电子书中的排版数学
- 创建AZW3 、 EPUB 、 MOBI目录
- 虚拟书库

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 管理图书的子组，例如“体裁”

管理图书的子组，例如“体裁”

有些人希望把书库里的书组织成子组，类似于子文件夹。最常见的原因是创建体裁层次结构，但还有许多其他原因。一位用户要求找到一种按学科和课程编号组织教科书的方法。另一个则希望根据礼物的主题和接收者来追踪礼物。本教程将在本文的其余部分使用流派示例。

在继续之前，请注意我们不是在谈论硬盘上的文件夹。子组不是文件文件夹。任何地方都不会复制书籍。calibre的书库文件结构不受影响。相反，我们提供了一种在一个calibre书库中组织和显示图书子组的方法。

- 配置

- 搜索

- 限制

- 有用的模板函数

通常提供的对子组例如体裁的要求是：

- 子组(例如，体裁)必须包含(指向)图书，而不是图书类别。这就是将子组与Calibre用户类别区分开来的原因。

- 一本书可以分成多个子组(体裁)。这将子组与物理文件夹区分开来。

- 子组(体裁)必须形成层次结构；子组可以包含子组。

标签给出了前两个。如果你给一本书贴上这种类型的标签，那么你可以使用标签浏览器或搜索来查找这种类型的书，这样你就可以得到第一类。许多书可以有相同的类型标签 ，为您提供第二类标签。问题是标签不满足第三个要求。它们不提供层次结构。

|sgtree | |体裁树搜索|calibre分级特性为您提供了第三种功能，即在“树”中查看体裁，并能够轻松地搜索体裁或子类型的书籍。例如，假设你的体裁结构与以下类似：

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

通过使用分级结构功能，您可以在“标签浏览器”中以树的形式查看这些流派，如屏幕图像所示。在本例中，最外层(体裁)是包含体裁的自定义列。包含子体裁的体裁旁边会出现一个小三角形。点击那个三角形将打开项目并显示子体裁，就像你在历史和科幻小说中看到的那样。

点击一个体裁可以搜索到该体裁的所有书籍或该体裁的子类。例如，点击“科幻小说”可以提供所有三种儿童体裁，“架空历史”，“军事”和“太空歌剧”。点击“架空历史”将给出这种体裁的书，忽略那些军事和太空歌剧体裁。当然，一本书可以有多种体裁。如果一本书同时有太空歌剧和军事流派，那么只要你点击其中一种，你就会看到那本书。下面将更详细地讨论搜索。

从图片中可以看到的另一件事是，军事这一体裁出现了两次，一次出现在历史下面，一次出现在科幻下面。因为体裁是分层次的，所以这是两个独立的体裁。一本书可以在其中一种中，也可以在另一种中，或者( 在这种情况下是怀疑的 )两者兼而有之。例如，温斯顿·丘吉尔 (Winston Churchill)的“第二次世界大战” (The Second World War)中的书可以放在“历史·军事” (History.Military)一体裁中。大卫·韦伯的“荣誉哈林顿”的书可能会出现在“科幻小说.军事”中，就这一点而言，也可能出现在“科幻小说.太空歌剧” (Science Fiction.Space Opera)中。

一旦存在一种体裁，即至少有一本书具有该体裁，您可以很容易地将其应用于其他图书，方法是将这些图书从书库视图拖到您希望这些图书具有的体裁上。您还可以在元数据编辑器中应用体裁；下面将详细介绍这一点。

配置

现在，您的问题可能是“所有这些设置是如何的？”有三个步骤：1)创建自定义列，2)告诉Calibre新的列将被视为一个层次结构，以及3)添加体裁。

您可以使用“首选项->添加你自己的列”以通常的方式创建自定义列。本例使用“#体裁”作为查找名称，使用“体裁”作为列标题。列类型是“逗号分隔的文本，就像标签浏览器中显示的标签一样。”

然后，在重新启动Calibre之后，您必须告诉Calibre该列将被视为层次结构。进入“ 首选项->外观->标签浏览器”，在“具有层次结构的分类”框中输入查找名称“#体裁”。按“应用”，设置完毕。

在这一点上，列中没有流派。我们只剩下最后一步：如何将一种体裁应用于一本书。一种体裁只有在至少一本书上出现时，它才能在calibre上存在。要第一次学习如何应用一种体裁，我们必须详细了解一本书的元数据中的体裁是什么样子。

“事物”的层次结构是通过创建一个由句点分隔的短语组成的项来构建的。继续体裁示例，这些项目将“历史.军事”, “神秘.吸血鬼“，”科幻.太空歌剧”等等。因此，要创建新体裁，请选择一本应该具有该体裁的书，编辑其元数据，并将新体裁输入到您创建的列中。如果你想给一本书分配一个新的体裁“漫画”和一个子体裁“超级英雄”，你需要为那本（漫画）书“编辑元数据”，选择“自定义元数据”选项卡，然后输入“漫画。超级英雄“如下所示（忽略其他自定义列）：

完成上述操作后，您将在“标签浏览器”中看到：

从现在开始，要将这一新体裁应用于一本书(假设是漫画书)，您可以将书拖到该体裁上，或者使用编辑元数据将其添加到书中，方法与上面完全相同。

备注

仅当标签浏览器设置为按名称对项目排序时，分层显示才有效。这是默认设置，单击”标签浏览器”底部的“设置”按钮即可查看。

搜索

最简单的搜索体裁的方法是使用”标签浏览器”，点击你想看到的体裁。点击一个有儿童的体裁，你会看到有这个体裁和所有儿童体裁的书。然而，这可能会带来一个问题。仅仅因为一个体裁有孩子并不意味着它本身就不是一个体裁。例如，一本书的体裁可以是“历史”，但不是”历史.军事”. 你如何搜索只有“历史”体裁的书？

“标签浏览器”搜索机制知道项是否有子项。如果是这样，单击该项将循环进行5次搜索，而不是常规的三次搜索。第一种是普通的绿色加号，它只向你展示那种体裁的书（例如历史）。第二个是双倍加号（如上图所示），它向您展示具有该类型和所有子类型（例如，历史和历史.军事). 第三种是正常的红色减号，它显示的是没有那种类型的书。第四个是一个双倍减号，它显示的是没有这种类型或子类型的书。第五个是回到开始，没有标记，意味着没有搜索。

限制

如果您搜索某一体裁，然后为其创建保存的搜索，您可以使用“限制为”框来创建具有该体裁图书的“虚拟书库”。如果要在该体裁内进行其他搜索或管理/更新该体裁图书的元数据，这将非常有用。继续我们的示例，您可以创建名为‘历史.日本’的已保存搜索，方法是：首先单击“标记浏览器”中的”日本历史”体裁，将搜索放入搜索字段，在已保存搜索字段中输入历史.日本，然后按下“保存搜索”按钮(右侧带有白色加号的绿色框)。

创建保存的搜索后，您可以将其用作限制。

有用的模板函数

您可能需要在模板中使用体裁信息，例如“保存到磁盘”或“发送到设备”。接下来的问题可能是“如何获得最外层的体裁名称？”提供了一个calibre模板函数subitems来简化这一过程。

例如，假设您要将最外层的体裁级别添加到“保存到磁盘”模板中，以创建流派文件夹，如“历史/聚集风暴-丘吉尔，温斯顿”中所示。为此，您必须提取层次结构的第一级，并将其与斜线一起添加到前面，以指示它应该成为一个文件夹。下面的模板实现了这一点：

{#genre:subitems(0,1)||/}{title} - {authors}

有关模板和“subitems”函数的更多信息，请参阅：“模板语言<templatelangcalibre>”。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- XPath教程

XPath教程

在本教程中，将对`XPath <https://en.wikipedia.org/wiki/XPath>`_进行简要介绍，这是一种查询语言，在cllibre中可用于选择`HTML<https://en.wikipedia.org/wiki/HTML>`_文档的任意部分。 XPath是一种广泛使用的标准，并且对其进行谷歌搜索将产生大量信息。 但是，本教程重点介绍如何将XPath用于与电子书相关的任务，例如在非结构化HTML文档中查找章节标题。

目录

- 按标记名选取

- 按属性选择

- 按标签内容选择

- 电子书样本

- XPath内置函数

按标记名选取

最简单的选择形式是按名称选择 标记。例如，假设您要选择文档中的所有``<h2>``标签。针对此的XPath查询简单地为：

//h:h2 (Selects all <h2> tags)

前缀`//` 表示“在文档的任何级别搜索”。现在假设您要搜索``<a>``标记内的``<span>`` 标记。这可以通过以下方式实现：

//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非常简短的电子书来演示XPath。</p>的使用”匹配，而不与其他任何“<p>” 标记匹配。上面示例中的“h:”前缀需要匹配XHTML标签。这是因为在内部，Calibre将所有内容表示为XHTML。在XHTML标签中，有一个*命名空间*，而且“h:”是HTML标签的命名空间前缀。

现在假设您要同时选择“<h1>”和“<h2>” 标记。为此，我们需要一个称为*谓词*的XPath结构。A：“谓词”只是一个用于选择标签的测试。测试可以是任意强大的，随着本教程的进行，您将看到更强大的示例。谓词是通过将测试表达式括在方括号中创建的：

//*[name()='h1' or name()='h2']

此XPath表达式中有几个新功能。第一个是通配符“*”的使用。它的意思是*匹配任何标签*。现在看一下测试表达式“name()=’h1’或name()=’h2’”。“名称()”是一个*内置函数*的例子。 它只是评估标签的名称。 因此通过使用它，我们可以选择名称为“h1”或“h2”的标签。注意：“名称()”函数忽略命名空间，因此不需要“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内置函数”_对属性值执行更复杂的匹配。

按标签内容选择

使用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内置函数

名称()

当前标记的名称。

包含（）

如果s1包含s2，contains(s1, s2) 返回 true

re:test()

如果字符串`src`与正则表达式`pattern`匹配，则``re:test(src, pattern, flags)``返回true。一个特别有用的标志是``i``，它使匹配不区分大小写。有关正则表达式语法的入门知识，请参见“正则表达式语法<https://docs.python.org/library/re.html>”_。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- calibre 模板语言

calibre 模板语言
calibre模板语言是calibre特定的语言，用于在calibre中执行任务，例如指定文件路径、格式化值和计算用户指定列的值。示例:

- 在将文件从calibre书库保存到磁盘或电子书阅读器时，指定文件夹结构和文件名。

- 定义规则以将图标和颜色添加到calibre图书列表中。

- 定义包含来自其他列的数据的“虚拟列”。

- 高级书库搜索。

- 高级元数据搜索和替换。

该语言是围绕“模板”的概念构建的，它指定要使用的图书元数据、元数据的计算以及如何格式化。

基本模板

基本模板由一个或多个“模板表达式”组成。 “模板表达式”由大括号 (“{}”) 中的文本和名称组成，并由正在处理的书籍中的相应元数据替换。 例如，calibre 中用于将书籍保存到设备的默认模板有 4 个“模板表达式”：

{author_sort}/{title}/{title} - {authors}

对于“艾萨克·阿西莫夫”所著的《基础》一书，将变为：

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

斜杠不是“模板表达式”，因为它们位于“{}”之间。 此类文本保留在其出现的位置。 例如，如果模板是：

{author_sort} Some Important Text {title}/{title} - {authors}

那么对于“The Foundation”，模板会生成：

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

If the metadata for the field for a given book is not defined then the field in the template is replaced by the empty string (''). For example, consider the following template:

{author_sort}/{series}/{title} {series_index}

If Asimov’s book “Second Foundation” is in the series “Foundation” then the template produces:

Asimov, Isaac/Foundation/Second Foundation 3

If a series has not been entered for the book then the template produces:

Asimov, Isaac/Second Foundation

The template processor automatically removes multiple slashes and leading or trailing spaces.

高级格式

In addition to metadata substitution, templates can conditionally include additional text and control how substituted data is formatted.

Conditionally including text

有时，您希望仅当字段不为空时文本才出现在输出中。 常见的情况是“series”和“series_index”，其中您不需要任何内容或用连字符分隔两个值。 calibre 使用特殊的“模板表达式”语法来处理这种情况。

For example and using the above Foundation example, assume you want the template to produce Foundation - 3 - Second Foundation. This template produces that output:

{series} - {series_index} - {title}

但是，如果一本书没有系列，模板将生成“- - 标题”，这可能不是您想要的。 一般来说，人们希望结果是没有无关连字符的标题。 您可以使用以下模板语法来完成此操作：

{field:|prefix_text|suffix_text}

这个“模板表达式”表示，如果“field”的值为“XXXX”，那么结果将为“prefix_textXXXXXsuffix_text”。 如果“field”为空（没有值），那么结果将是空字符串（什么也没有），因为前缀和后缀被忽略。 前缀和后缀可以包含空格。

Do not use subtemplates (`{ … }`) or functions (see below) in the prefix or the suffix.

Using this syntax, we can solve the above no-series problem with the template:

{series}{series_index:| - | - }{title}

仅当该书具有系列索引时才会包含连字符，而该索引仅当它具有系列时才包含。 再次继续 Foundation 示例，模板将生成“Foundation - 1 - Second Foundation”。

Notes:

- You must include the colon after the lookup name if you are using a prefix or a suffix.

- You must either use either no or both | characters. Using one, as in {field:| - }, is not allowed.

- It is OK to provide no text for either the prefix or the suffix, such as in {series:|| - }. The template {title:||} is the same as {title}.

Formatting

Suppose you want the series_index to be formatted as three digits with leading zeros. This does the trick:

{series_index:0>3s} - Three digits with leading zeros

For trailing zeros, use:

{series_index:0<3s} - Three digits with trailing zeros

如果您使用带有小数值的系列索引，例如 1.1，您可能希望小数点对齐。 例如，您可能希望索引 1 和 2.5 显示为 01.00 和 02.50，以便它们在进行词法排序的设备上正确排序。 为此，请使用：

{series_index:0>5.2f} - Five characters consisting of two digits with leading zeros, a decimal point, then 2 digits after the decimal point.

If you want only the first two letters of the data, use:

{author_sort:.2} - Only the first two letters of the author sort name

大部分 calibre 模板语言格式都来自 Python。 有关这些高级格式化操作的语法的更多详细信息，请参阅“Python 文档 <https://docs.python.org/3/library/string.html#formatstrings>”_。

Using templates to define custom columns

模板可用于显示 calibre 元数据中没有的信息，或以不同于 calibre 正常格式的方式显示元数据。 例如，您可能想要显示“ISBN”，这是 calibre 不显示的字段。 您可以通过创建类型为“从其他列构建的列”（以下称为“复合列”）的自定义列来完成此操作，并提供一个模板来生成显示的文本。 该列将显示评估模板的结果。 例如，要显示 ISBN，请创建列并在模板框中输入“{identifiers:select(isbn)}”。 要显示包含两个系列自定义列的值（以逗号分隔）的列，请使用``{#series1:||,}{#series2}``。

Composite columns can use any template option, including formatting.

注意：您无法编辑复合列中显示的数据。 相反，您可以编辑源列。 如果您编辑复合列，例如通过双击它，calibre 将打开模板进行编辑，而不是基础数据。

Templates and plugboards

插件板用于在发送到设备和保存到磁盘操作期间更改写入书籍的元数据。 插件板允许您指定一个模板来提供要写入书籍元数据的数据。 您可以使用插件板修改以下字段：作者、author_sort、语言、出版商、标签、书名、title_sort。 此功能可以帮助想要在设备上使用图书中的不同元数据的人们解决排序或显示问题。

创建插接板时，您可以指定插接板要使用的格式和设备。 提供了一个特殊的设备“save_to_disk”，在保存格式时使用（而不是将它们发送到设备）。 选择格式和设备后，您可以选择要更改的元数据字段，并提供模板来提供新值。 这些模板“连接”到其目标字段，因此称为“插件板”。 您当然可以在这些模板中使用复合列。

Plugboards are quite flexible and can be written in Single Function Mode, Template Program Mode, General Program Mode, or Python Template mode.

当插件板可能适用时（内容服务器、保存到磁盘或发送到设备），calibre 会搜索定义的插件板，为给定的格式和设备选择正确的插件板。 例如，要为发送到 ANDROID 设备的 EPUB 图书找到合适的插件板，calibre 使用以下搜索顺序搜索插件板：

- a plugboard with an exact match on format and device, e.g., EPUB and ANDROID

- a plugboard with an exact match on format and the special any device choice, e.g., EPUB and any device

- a plugboard with the special any format choice and an exact match on device, e.g., any format and ANDROID

- a plugboard with any format and any device

标签和作者字段有特殊处理，因为这两个字段都可以容纳多个项目。 一本书可以有多个标签和多个作者。 当您指定要更改这两个字段之一时，将检查模板的结果以查看是否存在多个项目。 对于标签，只要 calibre 找到逗号，结果就会被分割。 例如，如果模板生成值“Thriller, Horror”，则结果将是两个标签“Thriller”和“Horror”。 无法在标签中间放置逗号。

作者也会发生同样的情况，但使用不同的字符进行剪切，即“&”（与号）而不是逗号。 例如，如果模板生成值“Blogs, Joe&Posts, Susan”，则该书最终将有两个作者：“Blogs, Joe”和“Posts, Susan”。 如果模板生成值“博客，乔；帖子，苏珊”，那么这本书将有一位作者的名字相当奇怪。

当将书籍保存到磁盘或写入设备时，插件板会影响写入书中的元数据。 插件板不会影响“保存到磁盘”和“发送到设备”用来创建文件名的元数据。 相反，文件名是使用在适当的首选项窗口中输入的模板构建的。

Using functions in templates - Single Function Mode

假设您想要以大写形式显示某个字段的值，而该字段通常为标题大写形式。 您可以使用“模板函数”来完成此操作。 例如，要以大写形式显示标题，请使用“uppercase”函数，如“{title:uppercase()}”。 要以标题大小写显示它，请使用``{title:titlecase()}``。

函数进入模板的格式部分，在 : 之后和第一个 | 之前，或者如果没有使用前缀/后缀，则在结束的 } 之前。 如果您同时具有格式和函数引用，则该函数位于第二个 : 后面。 函数返回模板中指定的列的值，经过适当修改。

The syntax for using functions is one of:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

函数名称后面必须始终跟有左括号和右括号。 有些函数需要额外的值（参数），这些值位于括号内。 参数之间用逗号分隔。 文字逗号（逗号作为文本，而不是参数分隔符）前面必须有反斜杠 (\) 。 最后一个（或唯一的）参数不能包含文本右括号。

Functions are evaluated before format specifications and the prefix/suffix. See further down for an example of using both a format and a function.

重要：如果您有编程经验，请注意“单函数模式”中的语法不是您所期望的。 字符串不加引号，并且空格很重要。 所有参数都被视为常量； 没有表达式。

Do not use subtemplates (`{ … }`) as function arguments. Instead, use Template Program Mode and General Program Mode.

Some functions require regular expressions. In the template language regular expression matching is case-insensitive.

在下面的函数文档中，符号“[something]*”意味着“something”可以重复零次或多次。 符号“[something]+”表示“something”重复一次或多次（必须至少存在一次）。

The functions intended for use in Single Function Mode are:

capitalize() – returns the value with the first letter upper case and the rest lower case.

contains(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 no match.

count(separator) – 将值解释为由``separator`` 分隔的项目列表，并返回列表中的项目数。 大多数列表使用逗号作为分隔符，但“作者”使用与号（&）。 示例：{tags:count(,)}、{authors:count(&)}。 别名：“count()”、“list_count()”

format_number(template) – 将值解释为数字，并使用 Python 格式模板，例如 {0:5.2f} 或 {0:,d} 或 ` ${0:5,.2f}`。 格式化模板必须以“{0:”开头并以“}”结尾，如上面的示例所示。 例外：如果格式模板仅包含格式，则可以省略前导“{0:”和尾随“}”。 有关更多示例，请参阅模板语言和“Python 文档 <https://docs.python.org/3/library/string.html#formatstrings>`_”。 如果格式化失败，则返回空字符串。

human_readable() – expects the value to be a number and returns a string representing that number in KB, MB, GB, etc.

ifempty(text if empty) – if the value is not empty then return the value of the field, otherwise return text if empty.

in_list(separator, [pattern,found_val,]* not_found_val) – 将值解释为由``separator`` 分隔的项目列表，并对照列表中的每个项目检查``pattern``。 如果“pattern”与某个项目匹配，则返回“found_val”，否则返回“not_found_val”。 pattern 和 found_value 对可以根据需要重复多次，允许根据项目的值返回不同的值。 按顺序检查模式，并返回第一个匹配项。

language_strings(localize) – 返回 语言代码 [https://www.loc.gov/standards/iso639-2/php/code_list.php] 的`语言名称 <https:// www.loc.gov/standards/iso639-2/php/code_list.php>`_ 作为值传入。 示例：“{语言：语言字符串()}”。 如果“localize”为零，则返回英文字符串。 如果“localize”不为零，则返回当前语言环境的字符串。 Lang_codes 是一个以逗号分隔的列表。

list_item(index,separator) – 将值解释为由``separator`` 分隔的项目列表，返回第 ‘index’ 项。 第一项的编号为零。 最后一项的索引为“-1”，如“list_item(-1,separator)”中所示。 如果该项目不在列表中，则返回空字符串。

lookup([pattern, key, ]* else_key) – 将按顺序对照值检查模式。 如果模式匹配，则返回由“key”命名的字段的值。 如果没有模式匹配，则返回由“else_key”命名的字段的值。 请参阅``switch``（如下）。

lowercase() – returns the value of the field in lower case.

rating_to_stars(use_half_stars) – Returns the rating 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.

re(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 Python regular expressions [https://docs.python.org/3/library/re.html].

select(key) – 将值解释为以逗号分隔的项目列表，每个项目的形式为``id:value`` （calibre程序``identifier`` 格式）。 该函数查找 id 等于 key 的第一对并返回相应的值。 如果没有 id 匹配，则该函数返回空字符串。

shorten(left chars, middle text, right chars) – 返回值的缩短版本，由值开头的 left chars 字符组成，后跟 middle text， 后跟值末尾的“right chars”字符。 “左字符”和“右字符”必须是非负整数。 示例：假设您要显示的标题长度最多为 15 个字符。 执行此操作的一个模板是“{title:shorten(9,-,5)}”。 对于标题为“艾芬豪时代的古代英国法律”的书，结果将是“Ancient E-anhoe”：标题的前 9 个字符，“-”，然后是最后 5 个字符。 如果值的长度小于“左字符”+“右字符”+“中间文本”的长度，则该值将按原样返回。 例如，标题“The Dome”不会改变。

str_in_list(separator, [ string,found_val, ]+ not_found_val) – 将值解释为由``separator`` 分隔的项目列表，然后将``string`` 与列表中的每个值进行比较。 “string”不是正则表达式。 如果“string”等于任何项（忽略大小写），则返回相应的“found_val”。 如果“string”包含“separators”，那么它也被视为一个列表，并且每个子值都会被检查。 “string”和“found_value”对可以根据需要重复多次，允许根据字符串的值返回不同的值。 如果没有任何字符串匹配，则返回“not_found_value”。 按顺序检查字符串。 返回第一个匹配项。

subitems(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（start_index，end_index，separator） – 将值解释为由``separator``分隔的项目列表，返回由``start_index``到``end_index``的项目组成的新列表 。 第一项的编号为零。 如果索引为负数，则从列表末尾开始计数。 作为一种特殊情况，假设 end_index 为零是列表的长度。

Examples assuming that the tags column (which is comma-separated) contains “A, B ,C”:

- {tags:sublist(0,1,\,)} returns “A”

- {tags:sublist(-1,0,\,)} returns “C”

- {tags:sublist(0,-1,\,)} returns “A, B”

swap_around_articles(separator) – 返回文章移至末尾的值。 该值可以是一个列表，在这种情况下，列表中的每个项目都会被处理。 如果该值是一个列表，那么您必须提供“分隔符”。 如果未提供“separator”，则该值将被视为单个值，而不是列表。 “articles”是 calibre 用于生成“title_sort”的内容。

swap_around_comma() – 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.

switch([pattern, value,]+ else_value) – 对于每个``pattern, value`` 对，检查该值是否与正则表达式``pattern`` 匹配，如果匹配则返回关联的``value’ ``。 如果没有“pattern”匹配，则返回“else_value”。 您可以根据需要拥有任意数量的“模式、值”对。 返回第一个匹配项。

test(text if not empty, text if empty) – return text if not empty if the value is not empty, otherwise return text if empty.

titlecase() – returns the value of the field in title case.

transliterate() – Return a string in a latin alphabet formed by approximating the sound of the words in the source field. For example, if the source field is Фёдор Миха́йлович Достоевский this function returns Fiodor Mikhailovich Dostoievskii.

uppercase() – returns the value of the field in upper case.

Using functions and formatting in the same template

假设您有一个整数自定义列“#myint”，您希望显示前导零，如“003”。 实现此目的的一种方法是使用“0>3s”的格式。 但是，默认情况下，如果数字（整数或浮点数）等于零，则该值将显示为空字符串，因此零值将生成空字符串，而不是“000”。 如果您想查看“000”值，则可以使用格式字符串和“ifempty”函数将空值更改回零。 模板将是：

{#myint:0>3s:ifempty(0)}

Note that you can use the prefix and suffix as well. If you want the number to appear as [003] or [000], then use the template:

{#myint:0>3s:ifempty(0)|[|]}

General Program Mode

General Program Mode (GPM) replaces template expressions with a program written in the template language. The syntax of the language is defined by the following grammar:

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' | '==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#' 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

Notes:

- a top_expression always has a value. The value of an expression_list is the value of the last top_expression in the list. For example, the value of the expression list 1;2;'foobar';3 is 3.

- In a logical context, any non-empty value is True

- In a logical context, the empty value is False

- Strings and numbers can be used interchangeably. For example, 10 and '10' are the same thing.

- Comments are lines starting with a ‘#’ character. Comments beginning later in a line are not supported.

Operator precedence

The operator precedence (order of evaluation) from highest (evaluated first) to lowest (evaluated last) is:

- Function calls, constants, parenthesized expressions, statement expressions, assignment expressions, field references.

- Unary plus (+) and minus (-). These operators evaluate right to left.

These and all the other arithmetic operators return integers if the expression results in a fractional part equal to zero. For example, if an expression returns 3.0 it is changed to 3.

- Multiply (*) and divide (/). These operators are associative and evaluate left to right. Use parentheses if you want to change the order of evaluation.

- Add (+) and subtract (-). These operators are associative and evaluate left to right.

- Numeric and string comparisons. These operators return '1' if the comparison succeeds, otherwise the empty string (''). Comparisons are not associative: a < b < c is a syntax error.

- String concatenation (&). The & operator returns a string formed by concatenating the left-hand and right-hand expressions. Example: 'aaa' & 'bbb' returns 'aaabbb'. The operator is associative and evaluates left to right.

- Unary logical not (!). This operator returns '1' if the expression is False (evaluates to the empty string), otherwise ''.

- Logical and (&&). This operator returns ‘1’ if both the left-hand and right-hand expressions are True, or the empty string '' if either is False. It is associative, evaluates left to right, and does short-circuiting [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html].

- 逻辑或 (||)。 如果左侧表达式或右侧表达式为 True，则此运算符返回 '1'；如果均为 False，则返回 ''。 它是关联的，从左到右求值，并进行“短路”<https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html>`_。 它是一个“包含或”，如果左侧和右侧表达式都为 True，则返回“’1””。

Field references

A field_reference evaluates to the value of the metadata field named by lookup name that follows the $ or $$. Using $ is equivalent to using the field() function. Using $$ is equivalent to using the raw_field function. Examples:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

If expressions

If 表达式首先评估``condition``。 如果“condition”为 True（非空值），则评估“then”子句中的“expression_list”。 如果为 False，则评估“elif”或“else”子句中的“expression_list”。 elif 和 else 部分是可选的。 单词“if”、“then”、“elif”、“else”和“fi”被保留； 您不能将它们用作标识符名称。 您可以在任何有意义的地方放置换行符和空格。 “condition”是“top_expression”，而不是“expression_list”； 不允许使用分号。 “表达式列表”是以分号分隔的“top_表达式”序列。 if 表达式返回已计算的 expression_list 中最后一个 top_expression 的结果，如果没有计算表达式列表，则返回空字符串。

Examples:

* 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)

Nested if example:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

As said above, an if produces a value. This means that all the following are equivalent:

* 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

As a last example, this program returns the value of the series column if the book has a series, otherwise the value of the title column:

program: field(if field('series') then 'series' else 'title' fi)

For expressions
for 表达式迭代一系列值，一次处理一个值。 “list_expression” 必须计算为元数据字段“查找名称”，例如“tags” 或“#genre”，或者计算为值列表。 `range() 函数 1`（见下文）生成一个数字列表。 如果结果是有效的“查找名称”，则将获取字段的值并使用为该字段类型指定的分隔符。 如果结果不是有效的查找名称，则假定它是值列表。 除非提供可选关键字“separator”，否则假定列表以逗号分隔，在这种情况下，列表值必须由评估“separator_expr”的结果分隔。 如果列表是由“range()”生成的，则不能使用分隔符。 列表中的每个值都分配给指定的变量，然后计算“表达式列表”。 您可以使用“break”跳出循环，并使用“continue”跳到循环的开头进行下一次迭代。

Example: This template removes the first hierarchical name for each value in Genre (#genre), constructing a list with the new names:

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 的“批量编辑元数据 -> 搜索和替换”中使用此模板，并将“搜索”设置为“模板”，以剥离层次结构的第一级并将结果值分配给类型。

注意：在这种情况下，模板中的最后一行“new_tags”并不是绝对必要的，因为“for”返回表达式列表中最后一个 top_expression 的值。 赋值的值是其表达式的值，因此“for”语句的值就是分配给“new_tags”的值。

Function definition

If you have code in a template that repeats then you can put that code into a local function. The def keyword starts the definition. It is followed by the function name, the argument list, then the code in the function. The function definition ends with the fed keyword.

参数是立场性的。 调用函数时，提供的参数会从左到右与定义的参数进行匹配，并将参数的值分配给参数。 提供的参数多于定义的参数是错误的。 参数可以有默认值，例如“a = 25”。 如果没有为该参数提供参数，则使用默认值，否则该参数将设置为空字符串。

The return statement can be used in a local function.

A function must be defined before it can be used.

Example: This template computes an approximate duration in years, months, and days from a number of days. The function to_plural() formats the computed values. Note that the example also uses the & operator:

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')

Relational operators

Relational operators return '1' if the comparison is true, otherwise the empty string (‘’).

There are two forms of relational operators: string comparisons and numeric comparisons.

字符串比较使用词法顺序进行不区分大小写的字符串比较。 支持的字符串比较运算符有 ==、!=、<、<=、>、>=、in ` 和 inlist。 对于“in”运算符，左侧表达式的结果被解释为正则表达式模式。 如果左侧正则表达式的值与右侧表达式的值匹配，则“in”运算符为 True。 如果左侧正则表达式与右侧列表中的任何一项匹配，其中列表中的项目以逗号分隔，则“inlist”运算符为 true。 匹配不区分大小写。

The numeric comparison operators are ==#, !=#, <#, <=#, >#, >=#. The left and right expressions must evaluate to numeric values with two exceptions: both the string value “None” (undefined field) and the empty string evaluate to the value zero.

例如：

- program: field('series') == 'foo' returns '1' if the book’s series is ‘foo’, otherwise ''.

- program: 'f.o' in field('series') returns '1' if the book’s series matches the regular expression f.o (e.g., foo, Off Onyx, etc.), otherwise ''.

- program: 'science' inlist field('#genre') returns '1' if any of the book’s genres match the regular expression science, e.g., Science, History of Science, Science Fiction etc., otherwise ''.

- program: '^science$' inlist field('#genre') returns '1' if any of the book’s genres exactly match the regular expression ^science$, e.g., Science. The genres History of Science and Science Fiction don’t match. If there isn’t a match then returns ''.

- program: if field('series') != 'foo' then 'bar' else 'mumble' fi returns 'bar' if the book’s series is not foo. Otherwise it returns '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)$' in 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 > operator does a lexical comparison.

- program: if 11 ># 2 then 'yes' else 'no' fi returns 'yes' because the ># operator does a numeric comparison.

Additional available functions

The following functions are available in addition to those described in Single Function Mode.

In GPM the functions described in Single Function Mode all require an additional first parameter specifying the value to operate upon. All parameters are expression_lists (see the grammar above).

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.

and(value [, value]*) – 如果所有值都不为空，则返回字符串“1”，否则返回空字符串。 您可以拥有任意多个值。 在大多数情况下，您可以使用“&&”运算符来代替此函数。 不将“and”替换为“&&”的原因之一是短路是否会因副作用而改变结果。 例如，“and(a=’’,b=5)”将始终执行两个赋值操作，而“&&”运算符不会执行第二个操作。

assign(id, val) – assigns val to id, then returns val. id must be an identifier, not an expression. In most cases you can use the = operator instead of this function.

approximate_formats() – 返回与书籍关联的以逗号分隔的格式列表。 尽管可能是正确的，但不能保证该列表是正确的。 可以使用模板``{:’approximate_formats()’}`` 在模板程序模式（见下文）中调用此函数和其他零参数函数。 请注意，生成的格式名称始终为大写，如 EPUB 中那样。 “approximate_formats()”函数比下面讨论的“formats_…”函数要快得多。

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.

作者与其链接值由“val_separator”字符串分隔，不添加空格。 “author:linkvalue” 对由 “pair_separator” 字符串参数分隔，不添加空格。 您可以选择不出现在作者姓名或链接中的分隔符字符串。 即使作者链接为空，也会包含作者。

author_sorts(val_separator) – 返回一个字符串，其中包含书籍作者的作者排序值列表。 排序是作者元数据信息中的排序（与书籍中的author_sort不同）。 返回的列表的形式为``author sort 1`` val_separator author sort 2 等，不添加空格。 此列表中的作者排序值与书籍作者的顺序相同。 如果您想要“val_separator”周围有空格，则将它们包含在“val_separator”字符串中。

book_count(query, use_vl) – 返回通过搜索 query 找到的书籍数量。 如果“use_vl”为“0”（零），则虚拟库将被忽略。 此函数及其同伴“book_values()”在模板搜索中特别有用，支持组合多本书信息的搜索，例如查找仅包含一本书的系列。 除非将“allow_template_database_functions_in_composites”调整设置为 True，否则它不能在复合列中使用。 它只能在 GUI 中使用。

For example this template search uses this function and its companion to find all series with only one book:

1. Define a stored template (using Preferences → Advanced → Template functions) named series_only_one_book (the name is arbitrary). The template is:

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, '')

模板第一次运行时（检查的第一本书）它将数据库查找的结果存储在名为“vals”的“global”模板变量中。 这些结果用于检查后续书籍，而无需重新查找。

2. Use the stored template in a template search:

template:"program: series_only_one_book()#@#:n:1"

Using a stored template instead of putting the template into the search eliminates problems caused by the requirement to escape quotes in search expressions.

book_values(column, query, sep, use_vl) – 返回列 column （查找名称）中包含的唯一值的列表，以 sep 分隔，在找到的书中 通过搜索“查询”。 如果“use_vl”为“0”（零），则虚拟库将被忽略。 该函数及其同伴“book_count()”在模板搜索中特别有用，支持组合多本书信息的搜索，例如查找仅包含一本书的系列。 除非将“allow_template_database_functions_in_composites”调整设置为 True，否则它不能在复合列中使用。 它只能在 GUI 中使用。

booksize() – returns the value of the calibre ‘size’ field. Returns ‘’ if there are no formats.

check_yes_no(field_name, is_undefined, is_false, is_true) – 检查由查找名称 field_name 命名的是/否字段的值是否是参数指定的值之一，返回 ' yes' 如果找到匹配项，否则返回空字符串。 将参数“is_undefined”、“is_false”或“is_true”设置为 1（数字）以检查该条件，否则将其设置为 0。示例：
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.

ceiling(x) – returns the smallest integer greater than or equal to x. Throws an exception if x is not a number.

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.

cmp(x, y, lt, eq, gt) – compares x and y after converting both to numbers. Returns lt if x <# y, eq if x ==# y, otherwise gt. This function can usually be replaced with one of the numeric compare operators (==#, <#, >#, etc).

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.

connected_device_uuid(storage_location_key) – 如果设备已连接，则返回设备 uuid（唯一 ID），否则返回空字符串。 设备上的每个存储位置都有不同的 uuid。 storage_location_key 位置名称为``’main’、’carda’`` 和``’cardb’``。 此功能仅在 GUI 中有效。

current_library_name() – return the last name on the path to the current calibre library.

current_library_path() – return the full path to the current calibre library.

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.

date_arithmetic(date, calc_spec, fmt) – 使用 calc_spec 从 date 计算新日期。 返回根据可选“fmt”格式化的新日期：如果未提供，则结果将采用 ISO 格式。 calc_spec 是由一对“vW”（“valueWhat”）连接而成的字符串，其中“v”是可能为负数，W 是以下字母之一：

- 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.

Example: '1s3d-1m' will add 1 second, add 3 days, and subtract 1 minute from date.

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.

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.

eval(string) – 将字符串作为程序求值，传递局部变量。 这允许使用模板处理器从局部变量构造复杂的结果。 在“模板程序模式 <template_mode>”中，由于在评估模板之前解释“{”和“}”字符，因此必须对“{”字符使用“[[”，对“}”使用“]]” 特点。 它们会自动转换。 另请注意，使用“模板程序模式 <template_mode>”时，不能在此函数的参数中使用前缀和后缀（“|prefix|suffix”语法）。

extra_file_size(file_name) – 返回书籍的``data/`` 文件夹中额外文件``file_name`` 的大小，如果存在，否则返回``-1``。 另请参见函数“has_extra_files()”、“extra_file_names()”和“extra_file_modtime()”。 该功能只能在 GUI 中使用。

extra_file_modtime(file_name, format_string) – 返回书籍的``data/`` 文件夹中额外文件``file_name`` 的修改时间，如果存在，否则返回``-1``。 modtime 根据“format_string”进行格式化（有关详细信息，请参阅“format_date()”）。 如果“format_string”是空字符串，则返回 modtime 作为自纪元以来的浮点数秒数。 另请参见函数“has_extra_files()”、“extra_file_names()”和“extra_file_size()”。 纪元取决于操作系统。 该功能只能在 GUI 中使用。

extra_file_names(sep [,pattern]) 返回本书的 data/ 文件夹中以 sep 分隔的额外文件列表。 如果提供了可选参数“pattern”，一个正则表达式，则列表将被过滤到与“pattern”匹配的文件。 模式匹配不区分大小写。 另请参见函数“has_extra_files()”、“extra_file_modtime()”和“extra_file_size()”。 该功能只能在 GUI 中使用。

field(lookup_name) – returns the value of the metadata field with lookup name lookup_name.

field_exists(field_name) – checks if a field (column) with the lookup name field_name exists, returning '1' if so and the empty string if not.

finish_formatting(val, fmt, prefix, suffix) – 将格式、前缀和后缀应用于值，其方式与在诸如 {series_index:05.2f|' 等模板中执行的方式相同 - |- }。 提供此功能是为了轻松地将复杂的单功能或模板程序模式模板转换为“GPM”模板。 例如，以下程序产生与上述模板相同的输出：

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Another example: for the template {series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} use:

program:
strcat(
re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
field('title')
)

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:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

returns "large". The same example with a first value of 16 returns "giant".

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.

floor(x) – returns the largest integer less than or equal to x. Throws an exception if x is not a number.

format_date(val, format_string) – format the value, which must be a date string, using the format_string, returning a string. The formatting codes are:

- 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 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: from_number:MMM dd yyyy

You might get unexpected results if the date you are formatting contains localized month names, which can happen if you changed the date format tweaks to contain MMMM. In this case, instead of using the field() function as in:

format_date(field('pubdate'), 'yyyy')

use the raw_field() function as in:

format_date(raw_field('pubdate'), 'yyyy')

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')

formats_modtimes(date_format_string) – 返回以冒号分隔的项目的逗号分隔列表 FMT:DATE 表示书籍格式的修改时间。 “date_format_string”参数指定日期的格式。 有关详细信息，请参阅“format_date()”函数。 您可以使用“select”函数来获取特定格式的修改时间。 请注意，格式名称始终为大写，如 EPUB 中那样。

formats_paths() – 返回以冒号分隔的项目的逗号分隔列表 FMT:PATH 给出书籍格式的完整路径。 您可以使用 select 函数来获取特定格式的路径。 请注意，格式名称始终为大写，如 EPUB 中那样。

formats_sizes() – return a comma-separated list of colon-separated FMT:SIZE items giving the sizes in bytes of the formats of a book. You can use the select function to get the size for a specific format. Note that format names are always uppercase, as in EPUB.

fractional_part(x) – returns the value after the decimal point. For example, fractional_part(3.14) returns 0.14. Throws an exception if x is not a number.

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:

- The following returns the link attached to the tag Fiction:

get_link('tags', 'Fiction')

- This template makes a list of the links for all the tags associated with a book in the form 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

has_cover() – return 'Yes' if the book has a cover, otherwise the empty string.

has_extra_files([pattern]) – 返回额外文件的计数，否则返回 ‘’ （空字符串）。 如果提供了可选参数“pattern”（正则表达式），则在对文件进行计数之前，列表将被过滤为与“pattern”匹配的文件。 模式匹配不区分大小写。 另请参见函数“extra_file_names()”、“extra_file_size()”和“extra_file_modtime()”。 该功能只能在 GUI 中使用。

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 ，则如果存在匹配则返回 identifier:value 对，否则返回空字符串 ('')。

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.

language_codes(lang_strings) – 返回 lang_strings 中传递的语言名称的`语言代码 <https://www.loc.gov/standards/iso639-2/php/code_list.php>`_。 字符串必须采用当前区域设置的语言。 Lang_strings 是一个以逗号分隔的列表。

list_contains(value,separator, [pattern,found_val,]* not_found_val) –（in_list 的别名）将值解释为由``separator`` 分隔的项目列表，评估 `` 针对列表中的每个值的pattern``。 如果“pattern”匹配任何值，则返回“found_val”，否则返回“not_found_val”。 “pattern” 和 “found_value” 可以根据需要重复多次，允许根据搜索返回不同的值。 按顺序检查模式。 返回第一个匹配项。 别名：in_list()、list_contains()

list_count(value, separator) – interprets value as a list of items separated by separator, returning the count of items in the list. Aliases: count(), list_count()

list_count_matching(list, pattern, separator) – interprets list 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()

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.

list_equals(list1, sep1, list2, sep2, yes_val, no_val) – 如果``list1`` 和``list2` 包含相同的项目，则返回``yes_val``，否则返回``no_val``。 通过使用适当的分隔符（“sep1”或“sep2”）分割每个列表来确定项目。 列表中项目的顺序不相关。 比较不区分大小写。

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.

list_join(with_separator,list1,separator1 [,list2,separator2]*) – 返回通过在项目之间使用``with_separator``连接源列表（``list1``等）中的项目而创建的列表 在结果列表中。 每个源“list[123…]”中的项目由关联的“separator[123…]”分隔。 列表可以包含零值。 它可以是像“publisher”这样的单值字段，实际上是一个单项列表。 使用不区分大小写的比较删除重复项。 项目按照它们在源列表中出现的顺序返回。 如果列表中的项目仅字母大小写不同，则使用最后一个。 所有分隔符都可以是多个字符。

例如:

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', ',')

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:

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

list_re(src_list,separator, include_re, opt_replace) – 首先使用``separator`` 字符将``src_list`` 分隔成项目来构造一个列表。 对于列表中的每个项目，检查它是否与“include_re”匹配。 如果是，则将其添加到要返回的列表中。 如果“opt_replace”不是空字符串，则在将项目添加到返回的列表之前应用替换。

list_re_group(src_list,separator, include_re, search_re [, template_for_group]*) – 与 list_re 类似，但替换不是可选的。 它在进行替换时使用“re_group(item, search_re, template …)”。

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.

list_sort(list, direction, separator) – return list sorted using a case-insensitive lexical sort. If direction is zero, list is sorted ascending, otherwise descending. The list items are separated by separator, as are the items in the returned list.

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.

例如:

list_split('one:two:foo', ':', 'var')

is equivalent to:

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

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()

mod(x, y) – returns the floor of the remainder of x / y. Throws an exception if either x or y is not a number.

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.

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.

ondevice() – return the string 'Yes' if ondevice is set, otherwise return the empty string.

or(value [, value]*) – 如果任何值不为空，则返回字符串 '1'，否则返回空字符串。 您可以拥有任意多个值。 该函数通常可以用“||”运算符替换。 无法更换的一个原因是短路会因副作用而改变结果。

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.

range(start, stop, step, limit) – 返回通过在参数 start、stop 和 step 指定的范围内循环生成的数字列表，最大长度为 limit。 产生的第一个值是“start”。 后续值``next_v = current_v + step``。 假设“step”为正，则“next_v < stop”时循环继续，否则当“next_v > stop”时继续。 如果“start”未通过测试，则会生成一个空列表：如果“step”为正，则“start >= stop”。 “limit” 设置列表的最大长度，默认值为 1000。参数“start”、“step” 和“limit” 是可选的。 使用一个参数调用“range()”指定“stop”。 两个参数指定“start”和“stop”。 三个参数指定“start”、“stop”和“step”。 四个参数指定“start”、“stop”、“step”和“limit”。 例子：

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)

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).

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.

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, 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()}", "{$}")'}

round(x) – returns the nearest integer to x. Throws an exception if x is not a number.

series_sort() – returns the series sort value.

strcat(a [, b]*) – can take any number of arguments. Returns a string formed by concatenating all the arguments.

strcat_max(max, string1 [, prefix2, string2]*) – 返回通过连接参数形成的字符串。 返回值被初始化为“string1”。 只要生成的字符串长度小于“max”，由“prefix, string”对组成的字符串就会添加到值的末尾。 前缀可以为空。 即使“string1”比“max”长，也会返回“string1”。 您可以根据需要传递任意数量的“前缀、字符串”对。

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.)

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.

strlen(value) – Returns the length of the string value.

substr(str, start, end) – 返回``str`` 的第``start``’ 个字符到第``end``’ 个字符。 “str” 中的第一个字符是第零个字符。 如果“end”为负数，则表示从右边数起有很多字符。 如果“end”为零，则表示最后一个字符。 例如， substr('12345', 1, 0) 返回 '2345'，而 substr('12345', 1, -1) 返回 '234' 。

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.

switch_if([test_expression, value_expression,]+ else_expression) – 对于每个 test_expression, value_expression 对，检查 test_expression 是否为 True（非空），如果是则返回 ` 值_表达式`。 如果没有“test_expression”为 True，则返回“else_expression”的结果。 您可以根据需要拥有任意多个“test_expression, value_expression”对。

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 ISO [https://en.wikipedia.org/wiki/ISO_8601] date/time format.

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.

to_hex(val) – returns the string val encoded in hex. This is useful when constructing calibre URLs.

urls_from_identifiers(identifiers, sort_results) – 给定一个以逗号分隔的“identifiers”列表，其中“identifier”是一对以冒号分隔的值（“id_name:id_value”），返回一个 从标识符生成的以逗号分隔的 HTML URL 列表。 如果 sort_results 为“0”（字符或数字），则列表不排序，否则按标识符名称的字母顺序排序。 URL 的生成方式与“图书详细信息”中显示的内置标识符列相同。

More complex programs in template expressions - Template Program Mode

“模板程序模式”(“TPM”) 是“通用程序模式 <general_mode>”和“单功能模式 <single_mode>”的混合。 “TPM”与单函数模式的不同之处在于它允许编写引用其他元数据字段的模板表达式、使用嵌套函数、修改变量和进行算术。 它与“通用程序模式”的不同之处在于，模板包含在“{”和“}”字符之间，并且不以单词“program:”开头。 模板的程序部分是通用程序模式表达式列表。

示例：假设您希望模板显示一本书的系列，如果有，否则显示自定义字段 #genre 的值。 您无法在“单函数模式 <single_mode>”中执行此操作，因为您无法引用模板表达式中的另一个元数据字段。 在“TPM”中，您可以，如以下表达式所示：

{#series:'ifempty($, field('#genre'))'}

The example shows several things:

- TPM is used if the expression begins with :' and ends with '}. Anything else is assumed to be in Single Function Mode.

- the variable $ stands for the field named in the template: the expression is operating upon, #series in this case.

- functions must be given all their arguments. There is no default value. For example, the standard built-in functions must be given an additional initial parameter indicating the source field.

- white space is ignored and can be used anywhere within the expression.

- constant strings are enclosed in matching quotes, either ' or ".

All the functions listed under Single Function Mode and General Program Mode can be used in TPM.

在“TPM”中，在字符串文字中使用“{”和“}”字符可能会导致错误或意外结果，因为它们会混淆模板处理器。 它尝试将它们视为模板表达式边界，而不是字符。 在某些情况下，但不是所有情况下，您可以将 { 替换为 [[ ，将 } 替换为 ]] 。 一般来说，如果您的程序包含“{”和“}”字符，那么您应该使用“通用程序模式”。

与“通用程序模式”一样，对于“单函数模式 <single_mode>”下记录的函数，除了记录的参数之外，您还必须提供函数要执行的值作为第一个参数。 在“TPM”中，您可以使用“$”来访问模板表达式的“查找名称”指定的值。

Python Template Mode

Python 模板模式 (PTM) 允许您使用本机 python 和 calibre API <https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre> 编写模板 _。 数据库 API 将是最常用的； 进一步的讨论超出了本手册的范围。 PTM 模板速度更快，可以执行更复杂的操作，但您必须知道如何使用 calibre API 在 python 中编写代码。

A PTM template begins with:

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'

You can add the above text to your template using the context menu, usually accessed with a right click. The comments are not significant and can be removed. You must use python indenting.

The context object supports str(context) that returns a string of the context’s contents, and context.attributes that returns a list of the attribute names in the context.

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))

The output in Book details looks like this:

保存的模板

“通用程序模式 <general_mode>”和“Python 模板模式 <python_mode>”都支持保存模板并从另一个模板调用这些模板，就像调用存储的函数一样。 您可以使用“首选项->高级->模板功能”保存模板。 该对话框中提供了更多信息。 您可以像调用函数一样调用模板，并根据需要传递位置参数。 参数可以是任何表达式。 调用模板的示例，假设存储的模板名为“foo”：

- foo() – call the template passing no arguments.

- foo(a, b) call the template passing the values of the two variables a and b.

- foo(if field('series') then field('series_index') else 0 fi) – if the book has a series then pass the series_index, otherwise pass the value 0.

在 GPM 中，您可以使用“arguments”函数检索在调用存储模板时传递的参数。 它声明并初始化局部变量，即有效的参数。 变量是位置变量； 他们在同一位置获取调用中给出的参数值。 如果调用中未提供相应的参数，则“arguments”会为该变量分配提供的默认值。 如果没有默认值，则该变量将设置为空字符串。 例如，以下“arguments”函数声明 2 个变量“key”、“alternate”：

arguments(key, alternate='series')

Examples, again assuming the stored template is named foo:

- foo('#myseries') – argument key is assigned the value 'myseries' and the argument alternate is assigned the default value 'series'.

- foo('series', '#genre') the variable key is assigned the value 'series' and the variable alternate is assigned the value '#genre'.

- foo() – the variable key is assigned the empty string and the variable alternate is assigned the value 'series'.

In PTM the arguments are passed in the arguments parameter, which is a list of strings. There isn’t any way to specify default values. You must check the length of the arguments list to be sure that the number of arguments is what you expect.

测试存储模板的一种简单方法是使用“模板测试器”对话框。 为了便于访问，请在“首选项->高级->键盘快捷键->模板测试器”中为其提供键盘快捷键。 为“存储的模板”对话框提供快捷方式将有助于在测试器和编辑存储的模板的源代码之间更快地切换。

Providing additional information to templates

A developer can choose to pass additional information to the template processor, such as application-specific book metadata or information about what the processor is being asked to do. A template can access this information and use it during the evaluation.

Developer: how to pass additional information

附加信息是一个包含“变量名：变量值”对的 Python 字典，其中值必须是字符串。 模板可以访问字典，创建名为“variable_name”的模板局部变量，其中包含值“variable_value”。 用户无法更改名称，因此最好使用不会与其他模板局部变量冲突的名称，例如在名称前添加下划线。

This dictionary is passed to the template processor (the formatter) using the named parameter global_vars=your_dict. The full method signature is:

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

Template writer: how to access the additional information

You access the additional information (the globals dictionary) in a template using the template function:

globals(id[=expression] [, id[=expression]]*)

其中“id”是任何合法的变量名称。 该函数检查开发者提供的附加信息中是否包含该名称。 如果是，则该函数将提供的值分配给具有该名称的模板局部变量。 如果附加信息中没有该名称，并且提供了“表达式”，则计算“表达式”并将结果分配给局部变量。 如果既没有提供值也没有提供表达式，函数会将空字符串 ('') 分配给局部变量。

A template can set a value in the globals dictionary using the template function:

set_globals(id[=expression] [, id[=expression]]*)

This function sets the globals dictionary key:value pair id:value where value is the value of the template local variable id. If that local variable doesn’t exist then value is set to the result of evaluating expression.

Notes on the difference between modes

The three program modes, Single Function Mode (SFM), Template Program Mode (TPM), and General Program Mode (GPM), work differently. SFM is intended to be ‘simple’ so it hides a lot of programming language bits.

Differences:

- In SFM the value of the column is always passed as an ‘invisible’ first argument to a function included in the template.

- SFM doesn’t support the difference between variables and strings; all values are strings.

- The following SFM template returns either the series name or the string “no series”:

{series:ifempty(no series)}

The equivalent template in TPM is

{series:'ifempty($, 'no series')'}

The equivalent template in GPM is:

program: ifempty(field('series'), 'no series')

The first argument to ifempty is the value of the field series. The second argument is the string no series. In SFM the first argument, the value of the field, is automatically passed (the invisible argument).

- Several template functions, for example booksize() and current_library_name(), take no arguments. Because of the ‘invisible argument’ you cannot use these functions in SFM.

- Nested functions, where a function calls another function to compute an argument, cannot be used in SFM. For example this template, intended to return the first 5 characters of the series value uppercased, won’t work in SFM:

{series:uppercase(substr(0,5))}

- TPM and GPM support nested functions. The above template in TPM would be:

{series:'uppercase(substr($, 0,5))'}

In GPM it would be:

program: uppercase(substr(field('series'), 0,5))

- 如上面“模板程序模式 1”部分所述，在“TPM”字符串文字中使用“{”和“}”字符可能会导致错误或意外结果，因为它们会混淆模板处理器。 它尝试将它们视为模板边界，而不是字符。 在某些情况下，但不是所有情况下，您可以将 { 替换为 [[ ，将 } 替换为 ]] 。 一般来说，如果您的程序包含“{”和“}”字符，那么您应该使用“通用程序模式”。

User-defined Python template functions

您可以将自己的 Python 函数添加到模板处理器。 这些函数可以在三种模板编程模式中的任何一种中使用。 通过转到“首选项 -> 高级 -> 模板函数”来添加这些函数。 该对话框中显示了说明。

Special notes for save/send templates

当模板用于“保存到磁盘”或“发送到设备”模板时，会应用特殊处理。 字段的值被清理，用下划线替换文件系统特有的字符，包括斜杠。 这意味着字段文本不能用于创建文件夹。 但是，前缀或后缀字符串中的斜杠不会更改，因此这些字符串中的斜杠将导致创建文件夹。 因此，您可以创建可变深度的文件夹结构。

For example, assume we want the folder structure series/series_index - title, with the caveat that if series does not exist, then the title should be in the top folder. The template to do this is:

{series:||/}{series_index:|| - }{title}

The slash and the hyphen appear only if series is not empty.

查找功能让我们可以进行更高级的处理。 例如，假设一本书有一个系列，那么我们需要文件夹结构“系列/系列索引 - title.fmt”。 如果这本书没有系列，那么我们需要文件夹结构“genre/author_sort/title.fmt”。 如果这本书没有类型，那么我们想使用“未知”。 我们想要两条完全不同的路径，具体取决于系列的值。

To accomplish this, we:

1. Create a composite field (give it lookup name #aa) containing {series}/{series_index} - {title}. If the series is not empty, then this template will produce series/series_index - title.

2. Create a composite field (give it lookup name #bb) containing {#genre:ifempty(Unknown)}/{author_sort}/{title}. This template produces genre/author_sort/title, where an empty genre is replaced with Unknown.

3. Set the save template to {series:lookup(.,#aa,#bb)}. This template chooses composite field #aa if series is not empty and composite field #bb if series is empty. We therefore have two completely different save paths, depending on whether or not series is empty.

Tips

- Use the Template Tester to test templates. Add the tester to the context menu for books in the library and/or give it a keyboard shortcut.

- Templates can use other templates by referencing composite columns built with the desired template. Alternatively, you can use Stored Templates.

- In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template {}. This template will always evaluate to an empty string.

- The technique described above to show numbers even if they have a zero value works with the standard field series_index.

Function reference

Reference for all built-in template language functions

Arithmetic

- add(x [, y]*)
- ceiling(x)
- divide(x, y)
- floor(x)
- fractional_part(x)
- multiply(x [, y]*)
- round(x)
- subtract(x, y)

Boolean

- and(value[, value]*)
- not(value)
- or(value [, value]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)
- days_between(date1, date2)
- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, format_string)

- format_date_field(field_name, format_string)

- format_number(v, template)
- human_readable(v)
- rating_to_stars(value, use_half_stars)

Get values from metadata

- annotation_count()
- approximate_formats()
- author_links(val_separator, pair_separator)
- author_sorts(val_separator)
- booksize()
- connected_device_name(storage_location)
- connected_device_uuid(storage_location)
- current_library_name()
- current_library_path()
- current_virtual_library_name()
- field(lookup_name)
- formats_modtimes(date_format)
- formats_paths()
- formats_sizes()
- has_cover()
- is_marked()
- language_codes(lang_strings)
- language_strings(lang_codes, localize)
- ondevice()
- raw_field(lookup_name [, optional_default])
- raw_list(lookup_name, separator)
- series_sort()
- user_categories()
- virtual_libraries()

If-then-else

- contains(val, pattern, text if match, text if not match)

- field_exists(field_name)
- ifempty(val, text if empty)
- test(val, text if not empty, text if empty)

Iterating over values

- first_non_empty(value[, value]*)
- lookup(val, [pattern, field,]+ else_field)
- switch(val, [pattern, value,]+ else_value)

- switch_if([test_expression, value_expression,]+ else_expression)

List lookup

- identifier_in_list(val, id_name [,found_val, not_found_val])

- in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

- list_item(val, index, separator)

- select(val, key)

- str_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(val, separator)

- list_count_matching(list, pattern, separator)

- list_difference(list1, list2, separator)

- list_equals(list1, sep1, list2, sep2, yes_val, no_val)

- list_intersection(list1, list2, separator)

- list_join(with_separator, list1, separator1 [, list2, separator2]*)

- list_re(src_list, separator, include_re, opt_replace)

- list_re_group(src_list, separator, include_re, search_re[, group_1_template]+)

- list_remove_duplicates(list, separator)
- list_sort(list, direction, separator)
- list_union(list1, list2, separator)
- range(start, stop, step, limit)
- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, separator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(template)

- template(x)

Relational

- first_matching_cmp(val, [cmp1, result1,]+, else_result)

- strcmp(x, y, lt, eq, gt)

- strcmpcase(x, y, lt, eq, gt)

String case changes

- capitalize(val)
- lowercase(val)
- titlecase(val)
- uppercase(val)

String manipulation

- character(character_name)

- re(val, pattern, replacement)

- re_group(val, pattern [, template_for_group]*)

- shorten(val, left chars, middle text, right chars)

- strcat(a [, b]*)

- strcat_max(max, string1 [, prefix2, string2]*)

- strlen(a)
- substr(str, start, end)
- swap_around_articles(val, separator)
- swap_around_comma(val)
- to_hex(val)
- transliterate(a)

Template database functions

- book_values(column, query, sep, use_vl)
- extra_file_modtime(file_name, format_spec)
- extra_file_names(sep [, pattern])
- extra_file_size(file_name)
- get_link(field_name, field_value)

- get_note(field_name, field_value, plain_text)

- has_extra_files([pattern])

- has_note(field_name, field_value)

other

- set_globals(id[=expression] [, id[=expression]]*)

API of the Metadata objects

- Metadata

- STANDARD_METADATA_FIELDS

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 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.

Arithmetic

- add(x [, y]*)

- ceiling(x)

- divide(x, y)

- floor(x)

- fractional_part(x)

- multiply(x [, y]*)

- round(x)

- subtract(x, y)

Boolean

- and(value[, value]*)

- not(value)

- or(value [, value]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)

- days_between(date1, date2)

- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, format_string)

- format_date_field(field_name, format_string)

- format_number(v, template)

- human_readable(v)

- rating_to_stars(value, use_half_stars)

Get values from metadata

- annotation_count()

- approximate_formats()

- author_links(val_separator, pair_separator)

- author_sorts(val_separator)

- booksize()

- connected_device_name(storage_location)

- connected_device_uuid(storage_location)

- current_library_name()

- current_library_path()

- current_virtual_library_name()

- field(lookup_name)

- formats_modtimes(date_format)

- formats_paths()

- formats_sizes()

- has_cover()

- is_marked()

- language_codes(lang_strings)

- language_strings(lang_codes, localize)

- ondevice()

- raw_field(lookup_name [, optional_default])

- raw_list(lookup_name, separator)

- series_sort()

- user_categories()

- virtual_libraries()

If-then-else

- contains(val, pattern, text if match, text if not match)

- field_exists(field_name)

- ifempty(val, text if empty)

- test(val, text if not empty, text if empty)

Iterating over values

- first_non_empty(value[, value]*)

- lookup(val, [pattern, field,]+ else_field)

- switch(val, [pattern, value,]+ else_value)

- switch_if([test_expression, value_expression,]+ else_expression)

List lookup

- identifier_in_list(val, id_name [,found_val, not_found_val])

- in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

- list_item(val, index, separator)

- select(val, key)

- str_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(val, separator)

- list_count_matching(list, pattern, separator)

- list_difference(list1, list2, separator)

- list_equals(list1, sep1, list2, sep2, yes_val, no_val)

- list_intersection(list1, list2, separator)

- list_join(with_separator, list1, separator1 [, list2, separator2]*)

- list_re(src_list, separator, include_re, opt_replace)

- list_re_group(src_list, separator, include_re, search_re[, group_1_template]+)

- list_remove_duplicates(list, separator)

- list_sort(list, direction, separator)

- list_union(list1, list2, separator)

- range(start, stop, step, limit)

- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, separator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(template)

- template(x)

Relational

- first_matching_cmp(val, [cmp1, result1,]+, else_result)

- strcmp(x, y, lt, eq, gt)

- strcmpcase(x, y, lt, eq, gt)

String case changes

- capitalize(val)

- lowercase(val)

- titlecase(val)

- uppercase(val)

String manipulation

- character(character_name)

- re(val, pattern, replacement)

- re_group(val, pattern [, template_for_group]*)

- shorten(val, left chars, middle text, right chars)

- strcat(a [, b]*)

- strcat_max(max, string1 [, prefix2, string2]*)

- strlen(a)

- substr(str, start, end)

- swap_around_articles(val, separator)

- swap_around_comma(val)

- to_hex(val)

- transliterate(a)

Template database functions

- book_values(column, query, sep, use_vl)

- extra_file_modtime(file_name, format_spec)

- extra_file_names(sep [, pattern])

- extra_file_size(file_name)

- get_link(field_name, field_value)

- get_note(field_name, field_value, plain_text)

- has_extra_files([pattern])

- has_note(field_name, field_value)

other

- set_globals(id[=expression] [, id[=expression]]*)

API of the Metadata objects

Arithmetic

add(x [, y]*)

class calibre.utils.formatter_functions.BuiltinAdd[源代码]

add(x [, y]*) –返回其参数的和。如果有参数不是数字，则抛出异常。此函数通常可以用+ 运算符替换。

ceiling(x)

class calibre.utils.formatter_functions.BuiltinCeiling[源代码]

ceiling(x) – 返回大于或等于x的最小整数。如果x不是数字，则引发异常。

divide(x, y)

class calibre.utils.formatter_functions.BuiltinDivide[源代码]

divide(x, y) – 返回x / y。如果x或y都不是数字，则抛出异常。此函数通常可以用 / 运算符替换。

floor(x)

class calibre.utils.formatter_functions.BuiltinFloor[源代码]

floor(x) – 返回小于或等于x的最大整数。如果x不是数字，则引发异常。

fractional_part(x)

class calibre.utils.formatter_functions.BuiltinFractionalPart[源代码]

fractional_part(x) – 返回小数点后的值。例如，fractional_part（3.14）返回0.14。如果x不是数字，则引发异常。

multiply(x [, y]*)

class calibre.utils.formatter_functions.BuiltinMultiply[源代码]

multiply(x [, y]*) –返回其参数的乘积。如果任何参数都不是数字，则抛出异常。此函数通常可以用*运算符替换。

round(x)

class calibre.utils.formatter_functions.BuiltinRound[源代码]

round(x) – 返回最接近x的整数。如果x不是数字，则引发异常。

subtract(x, y)

class calibre.utils.formatter_functions.BuiltinSubtract[源代码]

subtract(x, y) – 返回x - y。如果x或y不是数字，则抛出异常。此函数通常可以用-运算符替换。

Boolean

and(value[, value]*)

class calibre.utils.formatter_functions.BuiltinAnd[源代码]

and(value[, value]*) – 如果所有的值不为空则返回字符串 “1”, 否则返回空字符串. 这个函数和test或first_non_empty一起运行. 你可以拥有多个值。在许多情况下，&&运算符可以替换此函数。

not(value)

class calibre.utils.formatter_functions.BuiltinNot[源代码]

not(value) – 如果值为空，则返回字符串“1”，否则返回空字符串。此函数与test或first_non_empty配合使用效果很好。在许多情况下!运算符可以替代此函数。

or(value [, value]*)

class calibre.utils.formatter_functions.BuiltinOr[源代码]

or(value [, value]*) – 如果值为空，则返回字符串“1”，否则返回空字符串。此函数与test或first_non_empty配合使用效果很好。您可以有任意数量的值。在许多情况下，||运算符可以替换此函数。

Date functions

date_arithmetic(date, calc_spec, fmt)

class calibre.utils.formatter_functions.BuiltinDateArithmetic[源代码]

date_arithmetic(date, calc_spec, fmt) –使用“calc_spec”从“date”计算新日期。返回根据可选“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天，并从“date”中减去1分钟。

days_between(date1, date2)

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格式。

Formatting values

finish_formatting(val, fmt, prefix, suffix)

class calibre.utils.formatter_functions.BuiltinFinishFormatting[源代码]

finish_formatting(val, fmt, prefix, suffix) – 将格式、前缀和后缀应用于一个值，就像在模板中使用`{Series_index：05.2f-x-}‘作用一样。例如，以下程序产生与上述模板相同的输出：程序：finish_formatting(field(“series_index”), “05.2f”, “ - “, “ - “)

format_date(val, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDate[源代码]

format_date(val, format_string) – 使用format_string格式化该值（必须为日期），并返回一个字符串。格式代码为： d：以带前导零（1到31）的数字表示的天 dd：以带前导零（01到31）的数字表示的天 ddd：本地化的缩写日名（例如，“ 周一”到“ 周日” ”）。 dddd：本地化的长名称（例如，“星期一”至“星期日”）。 M：月份，不带前导零（1到12）的数字。 MM：月份的数字，前导零（01到12） MMM：本地化月份的缩写名称（例如，“1月”到“ 12月”）。 MMMM：本地化的长月份名称（例如，“ 一月”到“ 十二月”）。 yy：年份为两位数字（00至99）。 yyyy：年份为四位数。 h：无前导0的小时（0至11或0至23，取决于am / pm） hh：无前导0的小时（00至11或00至23，取决于am / pm） m：分钟无前导0（0到59） mm：无前导0（00到59）的分钟 s：无前导0（0到59）的秒 ss：有前导0（00到59）的秒 ap：使用12个小时的时钟代替24个小时的时钟，用“ ap”替换为am或pm AP的本地化字符串：使用12个小时的时钟而不是24小时时钟，用“ AP”替换为AM或PM iso：带时间和时区的日期。 to_number必须是唯一的格式：日期为浮点数 from_number[:fmt]：如果存在，则使用fmt格式化时间戳，否则为iso

format_date_field(field_name, format_string)

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_number(v, template)

class calibre.utils.formatter_functions.BuiltinFormatNumber[源代码]

format_number(v, template) –使用Python格式模板（例如“ {0：5.2f}”或“ {0：，d}”或“ $ {0：5，.2f}”）格式化数字v。 模板的field_name部分必须为0（zero）（即以上示例中的“ {0：”）。 有关更多示例，请参见模板语言和Python文档。 如果模板只包含一个格式，则可以省略开头的“ {0：”和结尾的“}”。 如果无法格式化，则返回空字符串。

human_readable(v)

class calibre.utils.formatter_functions.BuiltinHumanReadable[源代码]

human_readable(v) –返回代表数字v的字符串，以KB、MB、GB等为单位。

rating_to_stars(value, use_half_stars)

class calibre.utils.formatter_functions.BuiltinRatingToStars[源代码]

rating_to_stars(value, use_half_stars) – 以星形字符字符串形式返回评级。该值是一个介于0和5之间的数字。如果希望支持非整数评级的自定义评级栏目使用半星字符，请将use_half_stars设置为1，例如2.5。

Get values from metadata

annotation_count()

class calibre.utils.formatter_functions.BuiltinAnnotationCount[源代码]

annotation_count() —返回属于前书籍的所有类型的标注总数。此功能仅在GUI中工作。

approximate_formats()

class calibre.utils.formatter_functions.BuiltinApproximateFormats[源代码]

approximate_formats() – 返回一个逗号分隔的格式列表，其中一个点与书籍关联。不能保证这个列表是正确的，尽管它可能是正确的。这个函数可以在模板程序模式下调用，使用模板 “{:’approximate_formats()’}”。请注意，如在EPUB中格式名称总是大写的。这个函数只在图形界面中工作。如果要在保存到硬盘或发送到设备模板中使用这些值，则要自定义一个“基于其它栏目创建栏目”，使用该栏目的模板中的函数，并在保存/发送模板中使用该栏目的值

author_links(val_separator, pair_separator)

class calibre.utils.formatter_functions.BuiltinAuthorLinks[源代码]

author_links(val_separator, pair_separator) – 返回一个字符串，该字符串包含作者列表以及该作者的链接值，格式为author1 val_separator auth1link pair_separator auth2 val_separator auth2link等。作者与其链接值之间由val_separator字符串分隔，不添加空格。作者：链接值对由Pair_Separator字符串参数分隔，不添加空格。您可以选择作者名称或链接中不出现的分隔符字符串。即使作者链接为空，也会包含作者。

author_sorts(val_separator)

class calibre.utils.formatter_functions.BuiltinAuthorSorts[源代码]

author_sorts(val_separator) – 返回一个字符串，其中包含书籍作者的排序作者值列表。排序是author元数据中的排序(不同于书籍中的author_sort)。返回的列表的格式为 排序作者1 分隔符 排序作者2等。此列表中的排序作者值与书籍作者的顺序相同。如果您希望val_paricator周围有空格，则将它们包括在分隔符字符串中

booksize()

class calibre.utils.formatter_functions.BuiltinBooksize[源代码]

booksize() – 返回字段的大小。这个函数只在图形界面中工作。如果要在保存到硬盘或发送到设备模板中使用这些值，则必须定制“基于其它栏目创建栏目”，使用该栏目的模板中的函数，并在保存/发送模板中使用该栏目的值

connected_device_name(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[源代码]

connected_device_name(storage_location) – 如果设备已连接，则返回设备名称，否则返回空字符串。设备上的每个存储位置可以有不同的名称。位置名称为’main’、’carda’和’cardb’。此函数仅在GUI中工作。

connected_device_uuid(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[源代码]

connected_device_uuid(storage_location) – 如果设备已连接，则返回设备uuid（唯一id），否则返回空字符串。设备上的每个存储位置可以有不同的uuid。位置名称为’main’、’carda’和’cardb’。此函数仅在GUI中工作。

current_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[源代码]

current_library_name() – 返回当前 calibre 书库路径上的最后一个名称。这个函数可以在模板程序模式下被调用，使用模板”{:’current_library_name()’}”。

current_library_path()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[源代码]

current_library_path() – 返回当前 calibre 书库的路径。这个函数可以在模板程序模式下被调用，使用模板”{:’current_library_path()’}”。

current_virtual_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[源代码]

current_virtual_library_name() –返回当前虚拟书库的名称（如果有），否则返回空字符串。书库名称大小写敏感。示例：”program: current_virtual_library_name()”.

field(lookup_name)

class calibre.utils.formatter_functions.BuiltinField[源代码]

field(lookup_name) – 通过 lookup_name 返回元数据字段名称

formats_modtimes(date_format)

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[源代码]

formats_modtimes(date_format) – 返回逗号分隔的项目列表，这些项目以冒号分隔，表示书籍格式的修改时间。date_format参数指定如何将日期格式化。有关详情，请参阅format_date函数。您可以使用select函数获取特定格式的修改时间。请注意，格式名称始终为大写，就像在EPUB中一样。

formats_paths()

class calibre.utils.formatter_functions.BuiltinFormatsPaths[源代码]

formats_paths() – 返回逗号分隔的项目列表，这些项目以冒号分隔，表示书籍格式的完整路径。您可以使用SELECT函数获取特定格式的路径。请注意，格式名称始终为大写，就像在EPUB中一样。

formats_sizes()

class calibre.utils.formatter_functions.BuiltinFormatsSizes[源代码]

formats_sizes() – 返回以逗号分隔的项目列表，这些项目以冒号分隔，表示书籍格式的大小(以字节为单位)。您可以使用SELECT函数获取特定格式的大小。请注意，格式名称始终为大写，就像在EPUB中一样。

has_cover()

class calibre.utils.formatter_functions.BuiltinHasCover[源代码]

has_cover() –如果书有封面，则返回“是”，否则返回空字符串

is_marked()

class calibre.utils.formatter_functions.BuiltinIsMarked[源代码]

is_marked() –检查书籍在calibre中是否“标记”。如果是，则返回标记的值，“true”或逗号分隔的标记名称列表。如果书籍未标记，则返回 ‘’ 。

language_codes(lang_strings)

class calibre.utils.formatter_functions.BuiltinLanguageCodes[源代码]

language_codes(lang_strings) – 返回lang_strings中传递的字符串的语言代码。字符串必须使用当前区域设置的语言。Lang_strings是逗号分隔的列表。

language_strings(lang_codes, localize)

class calibre.utils.formatter_functions.BuiltinLanguageStrings[源代码]

language_strings(lang_codes, localize) – 返回lang_codes中传递的语言代码的字符串。如果localize为零，则返回英文字符串。如果localize不为零，则以当前区域设置的语言返回字符串。Lang_codes是逗号分隔的列表。

ondevice()

class calibre.utils.formatter_functions.BuiltinOndevice[源代码]

ondevice() – 如果设置了设备则返回“是”，否则返回空字符串。这个函数只在图形界面中工作。如果要在保存到硬盘或发送到设备模板中使用这些值，则必须定制“基于其它栏目创建栏目”，使用该栏目的模板中的函数，并在保存/发送模板中使用该栏目的值

raw_field(lookup_name [, optional_default])

class calibre.utils.formatter_functions.BuiltinRawField[源代码]

raw_field(lookup_name [, optional_default]) –返回由lookup_name命名的元数据字段，不应用任何格式。如果字段未定义 (‘None’)，则计算并返回可选的第二个参数’default’。

raw_list(lookup_name, separator)

class calibre.utils.formatter_functions.BuiltinRawList[源代码]

raw_list(lookup_name, separator) – 返回按 lookup_name命名的元数据列表，不应用任何格式或排序，并用分隔符分隔每项。

series_sort()

class calibre.utils.formatter_functions.BuiltinSeriesSort[源代码]

series_sort() – 返回丛书排序值

user_categories()

class calibre.utils.formatter_functions.BuiltinUserCategories[源代码]

user_categories() – 返回由逗号分隔的包含这本书’自定义分类’的列表。这个函数只在图形界面中工作。如果要在保存到硬盘或发送到设备模板中使用这些值，则需自定义一个“基于其它栏目创建栏目”，使用该栏目的模板中的函数，并在保存/发送模板中使用该栏目的值

virtual_libraries()

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[源代码]

virtual_libraries() – 返回包含这本书的以逗号分隔的虚拟书库列表。此功能仅在GUI中有效。如果要在保存到硬盘或发送到设备模板中使用这些值，则必须自定义“基于其它栏目创建栏目”，使用该栏目模板中的函数，并在保存/发送模板中使用该栏目的值

If-then-else

contains(val, pattern, text if match, text if not match)

class calibre.utils.formatter_functions.BuiltinContains[源代码]

contains(val, pattern, text if match, text if not match) – 检查val是否包含正则表达式“ pattern”的匹配项。如果找到匹配项则返回”文本，如果匹配”，否则，返回”文本，如果不匹配”

field_exists(field_name)

class calibre.utils.formatter_functions.BuiltinFieldExists[源代码]

field_exists(field_name) – 检查是否一个字段 (栏目) 名称为 field_name 存在,存在返回 ‘1’ 否则返回 ‘’.

ifempty(val, text if empty)

class calibre.utils.formatter_functions.BuiltinIfempty[源代码]

ifempty(val, text if empty) –如果val非空值则返回val，否则返回文本。

test(val, text if not empty, text if empty)

class calibre.utils.formatter_functions.BuiltinTest[源代码]

test(val, text if not empty, text if empty) – 如果值不为空则返回“文本不为空”，否则返回“空文本”

Iterating over values

first_non_empty(value[, value]*)

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[源代码]

first_non_empty(value[, value]*) – 返回第一个非空的值。如果所有值都为空，则返回空值。如果需要，您可以拥有任意多个值。

lookup(val, [pattern, field,]+ else_field)

class calibre.utils.formatter_functions.BuiltinLookup[源代码]

lookup(val, [pattern, field,]+ else_field) – 与switch类似，只不过参数是字段(元数据)名称，而不是文本。将获取并使用相应字段的值。请注意，因为复合栏目是字段，所以您可以在一个复合字段中使用此函数来使用其他一些复合字段的值。这在构建可变保存路径时非常有用

switch(val, [pattern, value,]+ else_value)

class calibre.utils.formatter_functions.BuiltinSwitch[源代码]

switch(val, [pattern, value,]+ else_value) – 对于每个“pattern，value”配对，检查“val”是否与正则表达式“pattern”匹配，如果匹配，则返回该“value”。如果没有匹配的规则，则返回“else_value”。如果需要，可以有任意多个“pattern，value”配对

switch_if([test_expression, value_expression,]+ else_expression)

class calibre.utils.formatter_functions.BuiltinSwitchIf[源代码]

switch_if([test_expression, value_expression,]+ else_expression) – 对于每个“test_expression，value_expression”对，检查test_express是否为True（非空），如果为True，则返回value_express的结果。如果没有test_expression为True，则返回else_expression的结果。您可以有任意数量的“test_expression，value_expression”对。

List lookup

identifier_in_list(val, id_name [,found_val, not_found_val])

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，则如果存在匹配则返回identifier:value对，否则返回空字符串。

in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

class calibre.utils.formatter_functions.BuiltinInList[源代码]

in_list(val, separator, [ pattern, found_val, ]+ not_found_val) –将val视为由分隔符分隔的项目列表，如果规则与任何列表值匹配，则返回found_val。如果规则与列表值不匹配，则返回not_found_val。规则和found_value对可以根据需要重复多次。该规则为按顺序检查。返回第一个匹配的found_val。别名： in_list(), list_contains()

list_item(val, index, separator)

class calibre.utils.formatter_functions.BuiltinListitem[源代码]

list_item(val, index, separator) – 将该值解释为由`分隔符`分隔的项目列表，返回第`index`项。第一个项目是零号。最后一项可以使用`List_Item(-1，分隔符)`返回。如果该项不在列表中，则返回空值。分隔符的含义与COUNT函数中的相同。

select(val, key)

class calibre.utils.formatter_functions.BuiltinSelect[源代码]

select(val, key) –将值解释为以逗号分隔的项目列表，其中项目为”id:value”。找到id等于 key的配对,并返回相应的值。如果未找到匹配项，则返回空字符串。

str_in_list(val, separator, [string, found_val, ]+ not_found_val)

class calibre.utils.formatter_functions.BuiltinStrInList[源代码]

str_in_list(val, separator, [string, found_val, ]+ not_found_val)–将val视为由分隔符分隔的项目列表，如果字符串与任何列表值匹配，则返回found_val。如果字符串与列表值不匹配，则返回not_found_val。比较完全匹配（不包含），不区分大小写。字符串和found_value对可以根据需要重复多次。该规则为按顺序检查。返回第一个匹配的found_val。

List manipulation

count(val, separator)

class calibre.utils.formatter_functions.BuiltinCount[源代码]

count(val, separator) –将该值解释为由`分隔符`分隔的项列表，返回列表中的项数。大多数列表使用逗号作为分隔符，但作者使用&号。示例： {tags:count(,)}, {authors:count(&)}。别名: count(), list_count()

list_count_matching(list, pattern, separator)

class calibre.utils.formatter_functions.BuiltinListCountMatching[源代码]

list_count_matching(list, pattern, separator) –将“list”解释为由分隔符separator分隔的项目列表，返回列表中与正则表达式“pattern”匹配的项目数。别名： list_count_matching(), count_matching()

list_difference(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListDifference[源代码]

list_difference(list1, list2, separator) – 使用不区分大小写的比较从list1中删除在list2中找到的任何项，从而返回一个列表。list1和list2中的项用分隔符分隔，返回的列表中的项也是如此。

list_equals(list1, sep1, list2, sep2, yes_val, no_val)

class calibre.utils.formatter_functions.BuiltinListEquals[源代码]

list_equals(list1, sep1, list2, sep2, yes_val, no_val) – 如果list1和list2包含相同的项，则返回yes_val，否则返回no_val。这些项目是通过使用适当的分隔符(Sep1或Sep2)拆分每个列表来确定的。列表中项目的顺序无关紧要。该比较不区分大小写。

list_intersection(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListIntersection[源代码]

list_intersection(list1, list2, separator) – 使用不区分大小写的比较从list1中删除在list2中找不到的任何项，从而返回一个列表。list1和list2中的项用分隔符分隔，返回的列表中的项也是如此。

list_join(with_separator, list1, separator1 [, list2, separator2]*)

class calibre.utils.formatter_functions.BuiltinListJoin[源代码]

list_join(with_separator, list1, separator1 [, list2, separator2]*) –返回通过使用with_separaator连接源列表lists (list1, 等) 中的结果列表中的项而生成的列表。每个源列表list[123…] 中的项目由关联的分隔符separator[123…]分隔。列表可以不包含值。它可以是一个类似于出版商这样的单一值的字段，实际上是一个单项列表。使用不区分大小写的比较来删除重复项。项目将按照它们在源列表中出现的顺序返回。如果列表中的项目仅字母大小写不同，则使用最后一个。所有分隔符可以是多个字符。例子：

program:

list_join(‘#@#’, $authors, ‘&’, $tags, ‘,’)

您可以对先前调用list_join的结果使用list_join，如下所示：

program:

a = list_join(‘#@#’, $authors, ‘&’, $tags, ‘,’); b = list_join(‘#@#’, a, ‘#@#’, $#genre, ‘,’, $#people, ‘&’)

可以使用表达式生成列表。例如，假设您想要作者和#genre（类型）的项目，但将genre改为单词“Genre:”，后跟该类型的第一个字母，即，类型“Fiction”变为“Genre: F”。以下操作将实现这一点：

program:

list_join(‘#@#’, $authors, ‘&’, list_re($#genre, ‘,’, ‘^(.).*$’, ‘Genre: 1’), ‘,’)

list_re(src_list, separator, include_re, opt_replace)

class calibre.utils.formatter_functions.BuiltinListRe[源代码]

list_re(src_list, separator, include_re, opt_replace) – 通过首先使用分隔符将src_list分成多个项来构造列表。对于列表中的每一项，检查它是否与include_re匹配。如果是，则将其添加到要返回的列表中。如果opt_place不是空字符串，在将项目添加到返回列表之前先应用替换。

list_re_group(src_list, separator, include_re, search_re[, group_1_template]+)

class calibre.utils.formatter_functions.BuiltinListReGroup[源代码]

list_re_group(src_list, separator, include_re, search_re[, group_1_template]+) – 除替换项之外的类似列表list_re不是可选的。当对结果列表进行替换时，它使用re_group(list_item，search_re，group_template，…)。

list_remove_duplicates(list, separator)

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[源代码]

list_remove_duplicates(list, separator) –返回通过删除源列表中的重复项而生成的列表。如果项目仅大小写不同，则返回最后一个项目。源列表中的项和返回列表中的项目都用分隔符分隔。

list_sort(list, direction, separator)

class calibre.utils.formatter_functions.BuiltinListSort[源代码]

list_sort(list, direction, separator) – 返回使用不区分大小写的排序的列表。如果排序方向为零，则列表按升序排序，否则按降序排序。列表项与返回列表中的项一样，用分隔符分隔。

list_union(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListUnion[源代码]

list_union(list1, list2, separator) – 通过合并list1和list2中的项，在不区分大小写的比较后，删除其中重复项，返回一个列表。如果项的大小写不同，则使用list1中的项。list1和list2中的项用分隔符分隔，返回的列表中的项也是如此。别名: list_union(), merge_lists()

range(start, stop, step, limit)

class calibre.utils.formatter_functions.BuiltinRange[源代码]

range(start, stop, step, limit) –返回通过在参数start、stop和step指定的范围内循环生成的数字列表，最大长度为limit。产生的第一个值是“start”。后续值next_v为current_v+step。step为正。当next_v＜stop时，则循环继续，否则当next_v>stop时停止。生成一个空列表，如果启动未通过测试：start>=stop 。此时step为正。 limit设置列表的最大长度，默认值为1000。参数start、step和limit是可选的。调用 range() 同时带有一个指定参数stop或两个指定参数start和stop或三个指定参数start、stop和step或四个指定参数start、stop、step和limit。示例： 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).

subitems(val, start_index, end_index)

class calibre.utils.formatter_functions.BuiltinSubitems[源代码]

subitems(val, start_index, end_index) – 此函数用于拆分诸如题材等类型的项目列表。它将该值解释为逗号分隔的项列表，其中每个项都是句点分隔的列表。返回一个新列表，该列表首先找到所有用句点分隔的项目，然后将每个这样的项目从start_index提取到end_index组件，然后将结果一起返回。句点分隔列表中的第一个组件的索引为零。如果索引为负数，则从列表末尾开始计数。作为一种特殊情况，假定end_index为零是列表的长度。使用基本模板模式并假定#genre值为“ A.B.C”的示例： {#genre:subitems(0,1)} 返回结果 “A”。 {#genre:subitems(0,2)} 返回结果 “A.B”。{#genre:subitems(1,0)} 返回结果 “B.C”。假设#genre值为”A.B.C, D.E.F”， {#genre:subitems(0,1)}返回“A，D”。{#genre:subitems(0,2)}返回“A.B，D.E”

sublist(val, start_index, end_index, separator)

class calibre.utils.formatter_functions.BuiltinSublist[源代码]

sublist(val, start_index, end_index, separator) –将该值解释为以`分隔符`分隔的项目列表，将从`start_index`生成的新列表返回给`end_index`项目。第一个项目是零号。如果索引为负数，则从列表末尾开始计数。作为一种特殊情况，假定end_index为零是列表的长度。示例：使用基本模板模式，并假定标签栏目（用逗号分隔）包含“ A，B，C”，{tags：sublist（0,1，\，）}返回“ A”。 {tags:sublist(0,1,\,)}返回“ C”。

{tags:sublist(-1,0,\,)} 返回“ A，B”。

Other

arguments(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinArguments[源代码]

arguments(id[=expression] [, id[=expression]]*) –在保存的模板中用于检索调用中传递的参数。它同时声明和初始化局部变量，实际上是参数。变量是位置变量；它们在同一位置获得调用中给定的参数值。如果调用中未提供相应的参数，则参数将为该变量分配所提供的默认值。如果没有默认值，则将变量设置为空字符串。

assign(id, val)

class calibre.utils.formatter_functions.BuiltinAssign[源代码]

assign(id, val) – 将val分配给id，然后返回的val. id必须是标识符，而不是表达式。此函数通常可以用=运算符替换。

globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinGlobals[源代码]

globals(id[=expression] [, id[=expression]]*)–检索可以传递到格式化程序中的“全局变量”。它使用传入的全局变量的名称来声明和初始化局部变量。如果传入的全局变量中没有提供相应的变量，那么它将为该变量分配所提供的默认值。如果没有默认值，则将变量设置为空字符串。

print(a[, b]*)

class calibre.utils.formatter_functions.BuiltinPrint[源代码]

print(a[, b]*) – 将参数打印到标准输出。除非从命令行（calibre debug-g）启动calibre，否则无法显示结果。

Recursion

eval(template)

class calibre.utils.formatter_functions.BuiltinEval[源代码]

eval(template) – 评估模板，传递局部变量（那些“赋值”给的变量）而不是书籍元数据。这允许使用模板处理器从局部变量构造复杂的结果。由于{ and }字符是特殊字符，因此必须使用[[表示{字符，and]]表示}字符；它们会自动转换。还要注意，在使用模板程序模式时，不能在此函数的参数中使用前缀和后缀（`| prefix | suffix`语法）。

template(x)

class calibre.utils.formatter_functions.BuiltinTemplate[源代码]

template(x) – x作为模板进行评估。评估是在其自己的上下文中完成的，这意味着变量不会在调用方和模板评估之间共享。因为{ and }字符是特殊的，所以必须使用[[表示{字符，使用]]表示}字符； 它们会自动转换。例如，template (‘[[title_sort]]’)将评估模板{title_sort} 并返回其值。 还要注意，在使用模板程序模式时，不能在此函数的参数中使用前缀和后缀（|prefix|suffix 语法）。

Relational

first_matching_cmp(val, [cmp1, result1,]+, else_result)

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[源代码]

first_matching_cmp(val, [cmp1, result1,]+, else_result) – 依次比较“ val <cmpN”，如第一次比较满足（即val小于cmpN）则返回“resultN”。 如果没有一次比较满足，则返回else_result。 示例： first_matching_cmp(10,5,”small”,10,”middle”,15,”large”,”giant”) 返回”large”。 相同例子中如果第一个值为16，则返回”giant”。

strcmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmp[源代码]

strcmp(x, y, lt, eq, gt) – 作为字符串比较 x 和 y，不区分大小写。当 x < y 时返回 lt，x = y 时返回 eq，否则返回 gt。在许多情况下，词法比较运算符（>、<、==等）可以替换此函数。

strcmpcase(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmpcase[源代码]

strcmpcase(x, y, lt, eq, gt) –将x和y作为字符串进行区分大小写敏感的比较。如果x<y，返回lt。如果x==y，则返回eq。否则返回gt。注意：这不是calibre使用的默认行为，例如，在词法比较运算符 (==, >, <, 等.)中。此函数可能导致意外结果，最好尽可能使用 strcmp() 。

String case changes

capitalize(val)

class calibre.utils.formatter_functions.BuiltinCapitalize[源代码]

capitalize(val) – 返回句首大写的 val

lowercase(val)

class calibre.utils.formatter_functions.BuiltinLowercase[源代码]

lowercase(val) – 返回小写的 val

titlecase(val)

class calibre.utils.formatter_functions.BuiltinTitlecase[源代码]

titlecase(val) – 返回词首大写的 val

uppercase(val)

class calibre.utils.formatter_functions.BuiltinUppercase[源代码]

uppercase(val) – 返回大写的 val

String manipulation

character(character_name)

class calibre.utils.formatter_functions.BuiltinCharacter[源代码]

character(character_name) –返回由character_name命名的字符。例如，字符（’newline’）返回换行符（’n’）。支持的字符名为’newline’, ‘return’, ‘tab’, 和 ‘backslash’.。

re(val, pattern, replacement)

class calibre.utils.formatter_functions.BuiltinRe[源代码]

re(val, pattern, replacement) –应用正则表达式后返回val。所有`pattern`实例都将以`replacement`替换。与所有calibre中的使用相同，这些是与Python兼容的正则表达式。

re_group(val, pattern [, template_for_group]*)

class calibre.utils.formatter_functions.BuiltinReGroup[源代码]

re_group(val, pattern [, template_for_group]*) – 返回一个字符串，该字符串是通过将正则表达式应用于val，并将每个匹配的实例替换为通过将每个匹配的组替换为相应模板返回的值计算的字符串而生成的字符串。 该组的原始匹配值可用$表示。 在模板程序模式下，与模板和eval函数一样，您可以使用[[ for { and ]] for }.下面的示例表示在模板程序模式下查找具有多个单词的序列，并将第一个单词大写： {series:’re_group($, “(S* )(.*)”, “[[$:uppercase()]]”, “[[$]]”)’}

shorten(val, left chars, middle text, right chars)

class calibre.utils.formatter_functions.BuiltinShorten[源代码]

shorten(val, left chars, middle text, right chars) – 返回val的缩写版本，由val开头的“左字符”字符组成，后跟“中间文本”，然后是字符串末尾的“右字符”字符。左字符`和`右字符`必须是整数。例如，假设书名为”Ancient English Laws in the Times of Ivanhoe”，您希望它最多只能容纳15个字符。如果使用{title：Shorten(9，-，5)}，则结果为`Ancient E-nhoe。如果该字段的长度小于左字符+右字符+`中间文本`的长度，则该字段将被原封不动地使用。例如，标题‘The Dome`不会改变。

strcat(a [, b]*)

class calibre.utils.formatter_functions.BuiltinStrcat[源代码]

strcat(a [, b]*) –可以接受任意数量的参数。返回通过串联所有参数形成的字符串

strcat_max(max, string1 [, prefix2, string2]*)

class calibre.utils.formatter_functions.BuiltinStrcatMax[源代码]

strcat_max(max, string1 [, prefix2, string2]*) – 返回通过串联参数形成的字符串。返回的值初始化为string1。只要得到的字符串长度小于’max’，`Prefix，string`字符串对就会添加到值的末尾。即使String1比max长，也会返回String1。可以根据需要传递任意多个“prefix，string”对。

strlen(a)

class calibre.utils.formatter_functions.BuiltinStrlen[源代码]

strlen(a) – 返回作为参数传递的字符串的长度

substr(str, start, end)

class calibre.utils.formatter_functions.BuiltinSubstr[源代码]

substr(str, start, end) – 返回str的开始字符到结束字符。str中的第一个字符是第零个字符。如果end为负数，则表示许多字符从右边开始计数。如果end为零，则表示最后一个字符。例如，substr（’12345’，1，0）返回’2345’，substr（’12345’，1，-1）返回’234’。

swap_around_articles(val, separator)

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[源代码]

swap_around_articles(val, separator) –返回的val与冠词一起移到末尾。该值可以是一个列表，在这种情况下，将处理列表的每个成员。如果值是列表，则必须提供列表值分隔符。如果没有提供分隔符，则将该值视为单个值，而不是列表。

swap_around_comma(val)

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[源代码]

swap_around_comma(val) – 如果给定值的形式为“B，A”，则返回“A、B”。这对于将’姓、名’格式的名称转换为’名、姓’非常有用。如果没有逗号，则函数返回未更改的val

to_hex(val)

class calibre.utils.formatter_functions.BuiltinToHex[源代码]

to_hex(val) –返回以十六进制编码的字符串。这在构造calibre URL时很有用。

transliterate(a)

class calibre.utils.formatter_functions.BuiltinTransliterate[源代码]

transliterate(a) – 返回一个拉丁字母表中的字符串，该字符串是通过近似源字符串中的单词的声音而形成的。例如，如果源是”Фёдор Миха́йлович Достоевский” ，则函数返回 “Fiodor Mikhailovich Dostoievskii”。

Template database functions

book_values(column, query, sep, use_vl)

class calibre.utils.formatter_functions.BuiltinBookValues[源代码]

book_values(column, query, sep, use_vl) –返回通过搜索“query”找到的书籍中“column”栏目中包含的值列表，用“sep”分隔。如果use_vl为0（零），则忽略虚拟书库。此功能只能在GUI中使用。

extra_file_modtime(file_name, format_spec)

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[源代码]

extra_file_modtime(file_name, format_spec) —-返回书籍 ‘data/’文件夹中额外文件’file_name’ 的修改时间（如果存在），否则为-1.0。modtime根据’format_string’进行格式化（请参阅format_date()）。如果’format_string’为空，则返回modtime格式为自epoch以来的浮点数格式的秒数。epoch的设定值取决于操作系统。此功能只能在GUI中使用。

extra_file_names(sep [, pattern])

class calibre.utils.formatter_functions.BuiltinExtraFileNames[源代码]

extra_file_names(sep [, pattern]) – 回以分隔符-分隔的书籍的 ‘data/’文件夹中额外文件的列表。如果提供了可选参数“pattern”（一个正则表达式），会先筛选出与规则匹配的文件的列表。规则匹配不区分大小写。此功能只能在GUI中使用。

extra_file_size(file_name)

class calibre.utils.formatter_functions.BuiltinExtraFileSize[源代码]

extra_file_size(file_name) –返回书籍的 ‘data/’ 文件夹中的额外文件 ‘file_name’的大小（以字节为单位），否则为-1。此函数只能在GUI中使用。

get_link(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinGetLink[源代码]

get_link(field_name, field_value) – 取值为‘field_value’的字段‘field_name’的链接。如果没有附加链接，则返回’’。示例：get_link(‘tags’, ‘Fiction’) 返回附加到标签’Fiction’的链接。

get_note(field_name, field_value, plain_text)

class calibre.utils.formatter_functions.BuiltinGetNote[源代码]

get_note(field_name, field_value, plain_text) – 取取值为’field_value’的字段’field_name’的注释。如果’plain_text’为空，则返回注释的的HTML。如果’plain_text’非空，则返回注释的纯文本。如果注释不存在，则在两种情况下都返回 ‘’。示例：get_note(‘tags’, ‘Fiction’, ‘’) 返回附加到标签’Fiction’的注释的HTML。

has_extra_files([pattern])

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[源代码]

has_extra_files([pattern]) –返回额外文件的计数，否则为”(空字符串)。如果提供了可选参数“pattern”（正则表达式），则在对文件进行计数之前，会先筛选出与规则匹配的文件的列表。规则匹配不区分大小写。此功能只能在GUI中使用。

has_note(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinHasNote[源代码]

has_note(field_name, field_value) – 如果在’field_name’字段中’field_value’的值附有注释，则返回 ‘1’ ，否则返回’’ 。例如：has_note(‘tags’, ‘Fiction’) ，如果标签’fiction’带有附加注释，则返回’1’，否则返回’’。

other

set_globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinSetGlobals[源代码]

set_globals(id[=expression] [, id[=expression]]*) –设置可以传递到格式化程序中的“全局变量”。全局变量被赋予传入的id的名称。除非提供了表达式，否则将使用id的值。

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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 关于在 calibre 中使用正则表达式的所有内容

关于在 calibre 中使用正则表达式的所有内容

正则表达式是 calibre 中许多地方使用的功能，用于对电子书内容和元数据执行复杂的操作。 本教程简要介绍了如何开始在 calibre 中使用正则表达式。

目录

- 首先，一句警告和一句勇气

- calibre 中哪里可以使用正则表达式？

- 正则表达式到底是什么？

- 介意解释一下吗？

- 听起来还不错。 下一步是什么？

- 嘿，整洁！这开始有意义了！

- 嗯，这些特殊字符非常整齐，但是如果我想匹配一个点或一个问号怎么办？

- 那么，最有用的集合是什么？

- 但是如果我想要匹配一些不同的字符串，事情会变得复杂吗？

- 你错过了。。。

- 一开始，你说有办法让正则表达式不区分大小写？

- 我想我现在开始理解这些正则表达式了……我如何在 calibre 中使用它们？

- 转换

- 添加书籍

- 批量编辑元数据

- 快速参考

- 荣誉

首先，一句警告和一句勇气

这不可避免地会有些技术性——毕竟，正则表达式是一种用于完成技术工作的技术工具。 我将不得不使用一些看起来复杂或令人费解的术语和概念。 我将尝试尽可能清楚地解释这些概念，但实际上根本无法不使用它们。 话虽如此，不要因为任何行话而气馁，因为我已经尝试解释所有新内容。虽然正则表达式本身可能看起来像是一种神秘的黑魔法（或者，更平淡地说，是一串随机的繁琐字母和符号），但我保证它们并不那么复杂。 即使那些非常了解正则表达式的人在阅读更复杂的表达式时也会遇到困难，但是编写它们并不那么困难 - 您可以逐步构建表达式。 那么，迈出一步，跟我一起进入兔子洞吧。

calibre 中哪里可以使用正则表达式？

calibre 有几个地方使用了正则表达式。 转换选项中有搜索和替换，导入设置中的文件名元数据检测以及批量编辑图书元数据时的搜索和替换。 calibre 图书编辑器还可以在其搜索和替换功能中使用正则表达式。 最后，您可以在搜索 calibre 图书列表以及在 calibre 电子书阅读器内搜索时使用正则表达式。

正则表达式到底是什么？

正则表达式是一种描述字符串集的方法。 单个正则表达式可以“匹配”多个不同的字符串。 这就是正则表达式如此强大的原因——它们是描述大量潜在变体的简洁方式。

备注

我在这里使用的字符串是在编程语言中使用的意义上的：由一个或多个字符组成的字符串，字符包括实际字符、数字、标点符号和所谓的空白（换行符、制表符等）。 请注意，一般情况下，大写和小写字符不被视为相同，因此“a”与“A”是不同的字符，依此类推。 在 calibre 中，正则表达式在搜索栏中不区分大小写，但在转换选项中则不区分大小写。 有一种方法可以使每个正则表达式不区分大小写，但我们稍后会讨论。 它变得很复杂，因为正则表达式允许其匹配的字符串存在变化，因此一个表达式可以匹配多个字符串，这就是人们费心使用它们的原因。 稍后会详细介绍。

介意解释一下吗？

嗯，这就是我们在这里的原因。 首先，这是正则表达式中最重要的概念：字符串本身就是一个与自身匹配的正则表达式。 也就是说，如果我想使用正则表达式来匹配字符串“Hello, World!”，则使用的正则表达式将是“Hello, World!”。 是的，确实就是这么简单。 不过，您会注意到，这 仅 匹配确切的字符串 "Hello, World!"，而不是例如 “你好，世界！”或“你好，世界！”或任何其他类似的变体。

听起来还不错。 下一步是什么？

接下来是真正好东西的开始。 还记得我说过正则表达式可以匹配多个字符串吗？ 这就是事情变得有点复杂的地方。 假设，作为一个更实际的练习，您想要转换的电子书有一个讨厌的页脚来计算页数，例如“第 5 页，共 423 页”。 显然页码将从 1 增加到 423，因此您必须匹配 423 个不同的字符串，对吧？ 实际上是错误的：正则表达式允许您定义匹配的字符集：要定义一个集合，您可以将想要包含在该集合中的所有字符放入方括号中。因此，例如，集合“[abc]”将匹配字符“a”、“b”或“c”。 集合将始终仅匹配集合中的一个字符。 它们“理解”字符范围，也就是说，如果您想匹配所有小写字符，您可以使用集合“[a-z]”来表示小写和大写字符，您可以使用“[a-zA” -Z]``等等。明白了吗？ 因此，显然，使用表达式“Page [0-9] of 423”，您可以匹配前 9 页，从而将所需的表达式减少到三个：第二个表达式“Page [0-9]” [0-9] of 423`` 将匹配所有两位数页码，我相信您可以猜到第三个表达式会是什么样子。 好，去吧。 写下来。

嘿，整洁！这开始有意义了！

我本来希望你会这么说。 但请做好准备，现在情况会变得更好！ 我们刚刚看到，使用集合，我们可以一次匹配多个字符之一。 但您甚至可以重复一个字符或集合，将处理上述页码示例所需的表达式数量减少到一个。 是的，一个！ 兴奋的？ 你应该！ 它的工作原理是这样的：一些所谓的特殊字符，“+”，“？” 和“*”，重复它们前面的单个元素。 （元素意味着单个字符、字符集、转义序列或组（稍后我们将了解最后两个）——简而言之，正则表达式中的任何单个实体）。 这些字符称为通配符或量词。更准确地说，“？” 匹配前面元素的 0 或 1，“*”匹配前面元素的 0 或更多，“+”匹配前面元素的 1 或更多。 几个例子：表达式 a? 将匹配 “” （这是空字符串，在这种情况下严格来说没有用）或 “a”，表达式 a* 将匹配 “”, “ a”、”aa” 或任意数量的连续 a，最后，表达式 a+ 将匹配 “a”、”aa” 或任意数量的连续 a（注意：它不会 匹配空字符串！）。对于集合来说也是如此：表达式“[0-9]+”将匹配*每个整数*！ 我知道您在想什么，您是对的：如果您在上述匹配页码的情况下使用它，那不是可以匹配所有页码的单个表达式吗？ 是的，表达式“第 [0-9]+ 页，共 423 页”将匹配该书中的每个页码！

备注

关于这些量词的注释：它们通常会尝试匹配尽可能多的文本，因此使用它们时要小心。 这就是所谓的“贪婪行为”——我相信你明白为什么。 例如，当您尝试匹配标签时，就会出现问题。例如，考虑字符串 "<p class="calibre2"> Title here </p>" ，假设您想要匹配开始标签（第一对尖括号之间的部分，稍后会详细介绍标签）。 您可能认为表达式 <p.*> 会匹配该标签，但实际上，它匹配整个字符串！ （字符“.”是另一个特殊字符。它匹配*除*换行符之外的任何内容，因此，基本上，表达式“.*”将匹配您能想到的任何单行）。相反，尝试使用 <p.*?> 这使得量词 "*" 非贪婪。 正如预期的那样，该表达式仅匹配第一个开始标记。 实际上还有另一种方法可以实现此目的：表达式 <p[^>]*> 将匹配相同的开始 标记 - 您将在下一节之后看到原因。 请注意，编写正则表达式的方法通常不止一种。

嗯，这些特殊字符非常整齐，但是如果我想匹配一个点或一个问号怎么办？

您当然可以这样做：只需在任何特殊字符前面放置一个反斜杠，它就会被解释为文字字符，没有任何特殊含义。 这对反斜杠后跟一个字符称为转义序列，将反斜杠放在特殊字符前面的行为称为转义该字符。 转义序列被解释为单个元素。 当然，有些转义序列的作用不仅仅是转义特殊字符，例如“”t“表示制表符。 稍后我们将讨论一些转义序列。 哦，顺便说一下，关于那些特殊字符：将我们在本介绍中讨论的任何字符视为具有某些特殊功能，因此如果您想要字面字符，则需要对其进行转义。

那么，最有用的集合是什么？

就知道你会问。 一些有用的集合是“[0-9]”匹配单个数字，“[a-z]”匹配单个小写字母，“[A-Z]”匹配单个大写字母，“[a-zA” -Z]`` 匹配单个字母，[a-zA-Z0-9] 匹配单个字母或数字。 您还可以使用转义序列作为简写：

\d

相当于``[0-9]``

\w

相当于``[a-zA-Z0-9_]``

\s

相当于任何空白

备注

“空白”是一个术语，指的是任何不会被打印的内容。 这些字符包括空格、制表符、换行符、换页符、回车符、不间断空格等。

备注

如果启用了搜索不区分大小写的设置，则大写和小写集可能会同时匹配大写和小写。 例如，可以在 calibre 本身的“首选项”->“搜索”以及 calibre“电子书阅读器”以及 calibre“编辑书籍”工具的搜索面板中找到此类设置。

作为集合的最后一个注释，您还可以将集合定义为集合中的任何字符*但*字符。 您可以通过将字符 "^" 作为*集合中的第一个字符*来实现这一点。 因此，[^a] 将匹配除“a”之外的任何字符。 这就是所谓的补充集合。 我们之前看到的那些转义序列简写也可以补充：“\D”``表示任何非数字字符，因此相当于``[^0-9]。 您猜对了，可以使用相应的大写字母而不是小写字母来补充其他简写。 因此，回到上一节中的示例 <p[^>]*> ，现在您可以看到它使用的字符集尝试匹配除右尖括号之外的任何字符。

但是如果我想要匹配一些不同的字符串，事情会变得复杂吗？

不要害怕，生活仍然美好而轻松。 考虑以下示例：您要转换的书籍的每个奇数页上都写有“标题”，而每个偶数页上都写有“作者”。 印刷版看起来很棒，对吧？ 但在电子书中，这很烦人。 您可以将整个表达式分组在普通括号中，并且字符 "|" 将让您匹配*其右侧的表达式*或*其左侧的表达式。 将它们结合起来就完成了。 对你来说太快了吗？ 好的，首先，我们将奇数页和偶数页的表达式分组，从而将“(标题)( 作者 )”作为我们需要的两个表达式。 现在我们通过使用竖线（“|”``称为竖线字符）使事情变得更简单：如果您使用表达式``（标题 | 作者），您将获得“Title”的匹配项 ”（在奇数页上），或者匹配“作者”（在偶数页上）。 嗯，这不是很容易吗？

当然，您也可以使用竖线而不使用分组括号。 还记得我说过量词会重复它们前面的元素吗？ 嗯，竖线的工作方式有点不同：表达式“ Title | 作者”也将匹配字符串“Title”或字符串“作者”，就像上面使用分组的示例一样。 竖线在其前后的整个表达式之间进行选择。 因此，如果您想匹配字符串“Calibre”和“calibre”，并且只想在大写和小写“c”之间进行选择，则必须使用表达式“(c|C)alibre”， 其中分组确保仅选择“c”。 如果你要使用“c|Calibre”，你会得到字符串“c”或字符串“Calibre”的匹配，这不是我们想要的。 简而言之：如果有疑问，请与竖线一起使用分组。

你错过了。。。

…等一下，还有最后一件非常巧妙的事情，你可以与团体一起做。 如果您之前有一个匹配的组，则可以稍后在表达式中使用对该组的引用：组的编号从 1 开始，您可以通过转义要引用的组的编号来引用它们，因此是第五组 将被引用为``5``。 因此，如果您在字符串“Test Test”中搜索``([^]+) 1``，您将匹配整个字符串！

一开始，你说有办法让正则表达式不区分大小写？

是的，我知道了，谢谢你的关注和提醒。 您可以使用称为标志的东西来告诉 calibre 您希望如何处理某些事情。 您可以通过使用特殊构造“(?flags go here)”在表达式中包含标志，显然，您可以将“flags go here”替换为您想要的特定标志。 为了忽略大小写，标志是“i”，因此您可以在表达式中包含“(?i)”。 因此，(?i)test 将匹配“Test”、“tEst”、“TEst”以及您能想到的任何大小写变体。

另一个有用的标志可以让点匹配任何字符，*包括*换行符，标志“s”。 如果您想在表达式中使用多个标志，只需将它们放在同一个语句中即可：(?is) 将忽略大小写并使点全部匹配。 无论您首先声明哪个标志，(?si) 都等同于上面的。

我想我现在开始理解这些正则表达式了……我如何在 calibre 中使用它们？

转换

让我们从转换设置开始，这真的很简洁。 在“搜索和替换”部分中，您可以输入一个regexp（正则表达式的缩写）来描述转换过程中将被替换的字符串。 最巧妙的部分是向导。 单击向导人员，您可以预览转换过程中口径“看到”的内容。 向下滚动到要删除的字符串，选择并复制它，将其粘贴到窗口顶部的正则表达式字段中。 如果存在可变部分，例如页码等，请使用集合和量词来覆盖这些部分，并且在使用时，请记住转义特殊字符（如果有）。 点击标有“测试”的按钮，口径会突出显示如果您使用正则表达式它将替换的部分。 一旦您满意，请点击“确定”并进行转换。 如果您的转化源具有类似以下示例的标签，请务必小心：

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*Generates+bys+ABCs+Ambers+LIT.*?</b>`` 带有量词的 \s 包含在此处，而不是像字符串中那样显式使用空格 捕获可能出现的字符串的任何变化。 请记住检查要删除的口径，以确保在测试新表达式时不会删除任何想要保留的部分。 如果您只检查一次出现的情况，则可能会错过文本中其他地方的不匹配之处。 另请注意，如果您不小心删除了比您实际想要的更多或更少的标签，calibre 会在删除后尝试修复损坏的代码。

添加书籍

您可以使用正则表达式的另一件事是从文件名中提取元数据。 您可以在设置的“添加图书”部分找到此功能。 这里有一个特殊功能：您可以使用元数据字段的字段名称，例如“(?P<title>)”将表明 calibre 使用这部分字符串作为书名。 窗口中列出了允许的字段名称，以及另一个不错的测试字段。举个例子：假设您要导入一大堆名为“古典文本：但丁·阿利吉耶里的神曲.mobi”的文件。 （显然，这已经在您的书库中，因为我们都喜欢古典意大利诗歌）或“科幻史诗：艾萨克·阿西莫夫的基础三部曲.epub”。 这显然是一个 calibre 不会从中提取任何有意义的数据的命名方案 - 它提取元数据的标准表达式是``(?P<title>.+) - (?P<author>[^_]+)``。此处适用的正则表达式为“[a-zA-Z]+: (?P<title>.+) by (?P<author>.+)”。 请注意，在元数据字段的组内，您需要使用表达式来描述字段实际匹配的内容。 另请注意，在使用 calibre 提供的测试字段时，您需要将文件扩展名添加到测试文件名中，否则即使使用工作表达式，您也根本不会获得任何匹配项。

批量编辑元数据

最后一部分是元数据字段中的正则表达式“搜索和替换”。 您可以通过选择书库中的多本书并使用批量元数据编辑来访问此内容。 使用最后一个功能时要非常小心，因为它可能会对您的书库造成**非常糟糕的事情**！ 使用测试字段仔细检查您的表达式是否符合您的要求，并且仅标记您真正想要更改的书籍！ 在正则表达式搜索模式下，您可以在一个字段中搜索，用某些内容替换文本，甚至可以将结果写入另一字段。一个实际的例子：假设您的书库包含弗兰克·赫伯特的《沙丘》系列书籍，以时尚“沙丘 1 - 沙丘”、“沙丘 2 - 沙丘弥赛亚”等命名。 现在你想让《沙丘》进入系列领域。 您可以通过在标题字段中搜索``(.*?) d+ - .*`` 并在系列字段中将其替换为``1`` 来做到这一点。看看我在那里做了什么？ 这是对您要替换系列字段的第一组的引用。 现在您已经完成了该系列的准备，您只需再次搜索``.*？ -`` 在标题字段中，并再次在标题字段中将其替换为 "" （空字符串），这样您的元数据就干净整洁了。是不是很棒？ 顺便说一句，您也可以附加或添加到该字段，而不是替换整个字段，因此，如果您“希望”在书名前面添加系列信息，您也可以这样做。 您现在无疑已经注意到，有一个标记为“区分大小写”的复选框，因此您不必在此处使用标志来选择行为。

好了，关于正则表达式的简短介绍就到此结束了。 希望我能向您展示足够的内容，至少让您入门并让您能够继续自己学习 - 一个很好的起点是“正则表达式的 Python 文档”<https://docs.python.org/library/re.html>`_.

不过，最后一句警告是：正则表达式很强大，但也很容易出错。 calibre 提供了非常好的测试可能性，可以查看您的表达式是否按照您的预期运行。 使用它们。 尽量不要搬起石头砸自己的脚。 （上帝，我喜欢这个表达……）。 但是，如果您不顾警告，受伤了您的脚（或任何其他身体部位），请尝试从中吸取教训。

快速参考

- 正则表达式语法速查

- 字符种类
- 缩写字符集合
- 量词
- 贪婪
- 交替
- 不包括在内
- 锚
- 组
- Lookarounds
- Recursion
- 特殊字符
- 元字符
- Modes

荣誉

感谢您提供提示、更正等帮助：

- ldolse

- kovidgoyal

- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 关于在 calibre 中使用正则表达式的所有内容 »
- 正则表达式语法速查

正则表达式语法速查

这份清单总结了最常用/最难记住的正则表达式引擎部件，这些部件可在Calibre的大多数部件中使用。

目录

- 字符种类

- 缩写字符集合

- 量词

- 贪婪

- 交替

- 不包括在内

- 锚

- 组

- Lookarounds

- Recursion

- 特殊字符

- 元字符

- Modes

字符种类

字符集合可以用来简洁地表示不同的字符组合。

例如：

表达式

种类

[a-z]

小写字母，不包括带重音符号的字母和连字字母

[a-z0-9]

a 到 z 的小写字母或 0 到 9 的数字

[A-Za-z-]

大写和小写字母，或短横“-”。要在集合中包括短横，必须把它放在开头或结尾，从而不会和指定字符范围的短横混淆。

[^0-9]

除数字之外的任何字符。 位于类开头的插入符号 (^) 排除该类的字符（补集类）

[[a-z]--[aeiouy]]

小写元音字母。字符集合可以包含另一个字符集合。-- 符号表示排除它之后的集合

[\w--[\d_]]

所有字母（包括带重音符号的外语字母）。 可以在类内部使用缩写类

例如:

<[^<>]+> to select an HTML tag

缩写字符集合

表达式

种类

\d

一个数字（ 与``[0-9]``相同）

\D

任何非数字字符（与 ``[^0-9]``相同）

\w

一种字母数字字符（[a-zA-Z0-9]`），包括带重音符号和连字的字符

\W

任何“非单词”字符

\s

空格、不间断空格、制表符、返回行

\S

任何“非空白”字符

.

除换行符以外的任何字符。使用“.all”复选框或``(?s)``正则表达式修饰符来包含换行符。

量词

量词

量词前表达式出现次数

?

表达式出现0或1次。与``{0,1}``相同

+

表达式出现1次或多次。与``{1,}``相同

*

表达式出现0次、1次或更多次。与``{0,}``相同

{n}

该表达式恰好出现n次

{min,max}

包含的最小值和最大值之间的出现次数

{min,}

包含的最小值和无穷大之间的出现次数

{,max}

介于0和包含的最大值之间的出现次数

贪婪

默认情况下，对于量词，正则表达式引擎是贪婪的：它尽可能多地扩展选择。起初，这往往会引起意外。“？”跟随一个量词使其变懒。避免将两者放在同一个表达式中，结果可能是不可预测的。

小心嵌套的量词，例如模式“（a*）*”，因为它会成倍地增加处理时间。

交替

正则表达式中的“|”字符是逻辑“OR”。这意味着前面的表达式或下面的表达式都可以匹配。

不包括在内

方法 1
pattern_to_exclude(*SKIP)(*FAIL)|pattern_to_select

例如：

"Blabla"(*SKIP)(*FAIL)|Blabla

选择Blabla，在字符串Blabla或“Blabla or Blabla”中，但不在“Blabla”中。

方法 2
pattern_to_exclude\K|(pattern_to_select)

"Blabla"\K|(Blabla)

选择Blabla，在字符串Blabla或“Blabla or Blabla”中，但不在“Blabla”中。

锚

锚点是匹配字符串中的逻辑位置而不是字符的一种方式。文本处理中最有用的锚点是：

\b

指定单词边界，即从空格字符到非空格字符的转换。例如，您可以使用“bssurd”来匹配“the surd”，但不能使用“荒谬”。

^

匹配行首(在多行模式下，这是默认设置)

$

匹配行尾(在多行模式下，这是默认设置)

\K

将所选内容的起始位置重置为其在阵列中的位置。一些正则表达式引擎（但不是calibre）不允许可变长度的lookbacking，尤其是使用量词。当您可以将“K”与这些引擎一起使用时，它还允许您通过编写可变长度的正查找表来消除这一限制。

组

(表达式)

捕获组，它存储所选内容，并可稍后在*搜索*或*替换*模式中用“n”调用，其中“n”是捕获组的序列号（按读取顺序从1开始）

(?:表达式)

未捕获所选内容的组

(?>表达式)

Atomic Group：一旦满足表达式，正则表达式引擎就会通过，如果模式的其余部分失败，它将不会回溯以尝试与表达式的其他组合。原子组不会捕获。

(?|表达式)

分支重置组：表达式中包含的替换项的分支共享相同的组号

(?<name>表达式)

名为“name”的组。稍后可以在*search*模式中用“（？P=name）”和在*replace*中用“g <name>”调用所选内容。两个不同的组可以使用相同的名称。

Lookarounds

Lookaround

Meaning

?=

正向前视(放置在选定内容之后)

?!

反向前视(放在选择之后)

?<=

正向后视(放在所选内容之前)

?<!

反向后视(放在选定内容之前)

Lookahead和lookbehinds不消耗字符，它们为零长度，不捕获。它们是原子组：只要断言得到满足，正则表达式引擎就会通过，如果模式的其余部分失败，它就不会在环视中回溯以尝试其他组合。

在字符串中查找多个匹配时，在每次匹配尝试的起始位置，后行断言可以检查当前位置之前的字符。因此，在字符串123上，模式“（？<=d）d”（一个数字前面有一个数字）理论上应该选择2和3。另一方面，“dKd”只能选择2，因为第一次选择后的起始位置正好在3之前，并且没有足够的数字进行第二次匹配。类似地，“d（d）”只捕获2。在calibre的regexp引擎实践中，正向后行断言的行为与理论相反，只选择2。

组可以放置在环视区内，但捕获很少有用。然而，如果它是有用的，那么在后行断言中使用量词时就必须非常小心：与没有后行断言相关的贪婪可能会带来令人惊讶的结果。出于这个原因，当你在一个正向后行断言的捕获组中有一个量词（或者更糟的是，几个）时，使用“K”而不是正向后行断言。

负向先行断言示例：

(?![^<>{}]*[>}])

放置在图案末尾可防止在文件中嵌入的 标记或样式中进行选择。

只要可能，最好“固定”环视，以减少获得结果所需的步骤数。

Recursion

表达式

Meaning

(?R)

递归匹配整个模式

(?1)

编号捕获组的唯一模式的递归，此处为组1

递归是在召唤自己。这对于平衡查询非常有用，例如可以包含嵌入的带引号的字符串的引号字符串。因此，如果在处理双引号之间的字符串的过程中，我们遇到双引号之间的新字符串的开头，那么我们知道如何做，我们称之为自己。然后我们就有了类似这样的模式：

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

要在双引号之间选择字符串而不在嵌入的字符串上停止，请执行以下操作：

“((?>[^“”]+|(?R))*[^“”]+)”

该模板还可用于修改可嵌入的标签对，例如“<div>”标签。

特殊字符

表达式

字符

\t

tabulation

\n

换行符

\x20

（可间断的）空格

\xa0

不间断空格

元字符

元字符是那些对正则表达式引擎具有特殊意义的字符。其中，十二必须在前加一个转义符，即反斜杠（\），才能失去其特殊含义，重新成为正则字符：

^ . [ ] $ ( ) * + ? | \

其他七个元字符不需要以反斜杠开头(但可以没有任何其他后果)：

{ } ! < > = :

如果在类中使用特殊字符（位于括号``[]``之间），它们将失去状态。括号和短划线在类中具有特殊的地位。在类之外，短划线是一个简单的文字，右括号仍然是一个元字符。

斜杠（/）和数字符号（或哈希字符）（#）不是元字符，它们不需要转义。

在一些工具中，如带有Python引擎的regex101.com，双引号具有分隔符的特殊状态，必须转义，或者更改选项。优秀编辑的情况并非如此。

Modes

(?s)

使点 (.) 也匹配换行符

(?m)

使“^”和“$”锚与行的开始和结束匹配，而不是与整个字符串的开始和结尾匹配。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 编写自己的插件来扩展 calibre 的功能

编写自己的插件来扩展 calibre 的功能

Calibre 是以模块化的形式设计的，其所有功能几乎都以插件的形式实现。这些插件用作于文本转换、新闻（或称之为诀窍）下载，和用户界面的各种组件，来连接到不同的设备，或在将文件添加到Calibre时处理他们等等。您可以在“选项→高级→插件”中完整地得到在Cailbre里已经内建的插件清单。

在此，我们将会教您如何创造您自己的插件，给Calibre 添加新的功能。

目录

- Calibre 插件的剖析

- 用户界面插件

- __init__.py

- ui.py

- main.py

- 从插件 ZIP 文件获取资源

- 启用插件的用户配置

- 编辑书籍插件

- main.py

- 将翻译添加到您的插件中

- 插件 API

- 调试插件

- 更多插件示例

- 与他人分享您的插件

备注

此功能仅适用于0.8.60或更高的版本。

Calibre 插件的剖析

一个Calibre 插件的组成是非常简单的，其只是一个包含了一些Python 代码与一些其他资源文件如插件所需的贴图文件的ZIP 文件。现在，让我们直入主题，看看一个简单的例子。

假设您已安装了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/`.中。

您可以从 helloworld_plugin.zip 下载 Hello World 插件。

每次您使用 calibre 转换书籍时，都会调用插件的 run 方法，并将转换后的书籍的出版商设置为“Hello World”。 这是一个简单的插件，让我们继续看一个更复杂的示例，该示例实际上向用户界面添加了一个组件。

用户界面插件

该插件将分布在几个文件中（以保持代码整洁）。 它将向您展示如何从插件 ZIP 文件获取资源（图像或数据文件）、允许用户配置您的插件、如何在 calibre 用户界面中创建元素以及如何访问和查询 calibre 中的图书数据库。

您可以从“interface_demo_plugin.zip”下载此插件

首先要注意的是，这个ZIP文件中有很多文件，解释如下，特别注意``plugin-import-name-interface_demo.txt``。

plugin-import-name-interface_demo.txt

一个空文本文件，用于启用多文件插件魔法。 该文件必须存在于使用多个 .py 文件的所有插件中。 它应该为空，并且文件名必须采用以下形式：plugin-import-name-**some_name**.txt。 该文件的存在允许您使用如下语句从 ZIP 文件内的 .py 文件导入代码：

from calibre_plugins.some_name.some_module import some_object

前缀“calibre_plugins”必须始终存在。 some_name 来自空文本文件的文件名。 some_module 指的是 ZIP 文件中的 some_module.py 文件。 请注意，此导入与常规 Python 导入一样强大。 您可以在 ZIP 文件内创建 .py 模块的包和子包，就像通常一样（通过在每个子文件夹中定义 __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 插件不应在 __init__.py 中加载任何 GUI 库。 ual_plugin 字段可以为您完成此操作，告诉 calibre 实际插件可以在 ZIP 存档内的另一个文件中找到，该文件只会在 GUI 上下文中加载。

请记住，要使其正常工作，您的插件 ZIP 文件中必须有一个 plugin-import-name-some_name.txt 文件，如上所述。

还有几种方法可以启用插件的用户配置。 这些将在下面讨论。

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 dont 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

实现界面插件演示对话框的实际逻辑。

from qt.core import QDialog, QLabel, QMessageBox, QPushButton, QVBoxLayout 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 dont 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', 'Updated the metadata in the files of %d book(s)'%len(ids), 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 文件内文件的路径列表来调用此函数。 例如，要访问 ZIP 文件中图像文件夹中的文件“icon.png”，您可以使用：“images/icon.png”。 始终使用正斜杠作为路径分隔符，即使在 Windows 上也是如此。 当您传入单个名称时，该函数将返回该文件的原始字节，如果在 ZIP 文件中找不到该名称，则返回 None。 如果您传入多个名称，那么它会返回一个将名称映射到字节的字典。 如果未找到名称，则该名称不会出现在返回的字典中。

get_icons(name_or_list_of_names, plugin_name=’’)

get_resources() 的包装器，用于根据 get_resources 返回的原始字节创建 QIcon 对象。 如果在 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 qt.core import QHBoxLayout, QLabel, QLineEdit, QWidget

# 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 dont 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

您可以看到 main.py 中使用了“prefs”对象：

self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

编辑书籍插件

现在让我们稍微改变一下，看看创建一个插件来向 calibre 书籍编辑器添加工具。 该插件可在此处获取：editor_demo_plugin.zip。

对于所有插件来说，第一步是创建导入名称空 txt 文件，如“上面 <import_name_txt>”所述。 我们将文件命名为“plugin-import-name-editor_plugin_demo.txt”。

现在我们创建强制性的 __init__.py 文件，其中包含有关插件的元数据 - 它的名称、作者、版本等。

from calibre.customize import EditBookToolPlugin

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)

单个编辑器插件可以提供多个工具，每个工具对应于工具栏中的单个按钮和编辑器中“插件”菜单中的条目。 如果该工具有多个相关操作，这些可以有子菜单。

这些工具必须全部在插件的“main.py”文件中定义。 每个工具都是一个继承自“calibre.gui2.tweak_book.plugin.Tool”类的类。 让我们看一下演示插件中的“main.py”，源代码有大量注释，应该是不言自明的。 阅读 calibre.gui2.tweak_book.plugin.Tool 类的 API 文档了解更多详细信息。

main.py

在这里，我们将看到一个工具的定义，该工具将书中的所有字体大小乘以用户提供的数字。 该工具演示了开发自己的插件时需要的各种重要概念，因此您应该仔细阅读（大量注释的）源代码。

import re 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 from css_parser.css import CSSRule from qt.core import QAction, QInputDialog 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 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)) 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``。 我们看到它定义了一个工具，名为*放大字体*。 该工具将要求用户提供一个数字，并将书中所有字体大小乘以该数字。

第一件重要的事情是工具名称，您必须将其设置为一些相对唯一的字符串，因为它将用作该工具的密钥。

下一个重要的入口点是“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/图像文件的集合，并且具有执行许多有用操作的便捷方法。 容器对象和可以在插件代码中重用的各种有用的实用程序函数记录在“polish_api”中。

将翻译添加到您的插件中

您可以翻译插件中的所有用户界面字符串，并以为主 calibre 用户界面设置的任何语言显示。

第一步是检查插件的源代码，并通过将所有用户可见的字符串括在 _() 中，将它们标记为可翻译。 例如：

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”文件夹中。

最后一步是简单地调用插件 .py 文件顶部的函数“load_translations()”。 出于性能原因，您应该只在那些实际具有可翻译字符串的 .py 文件中调用此函数。 因此，在典型的用户界面插件中，您可以在“ui.py”的顶部调用它，而不是“__init__.py”。

您可以通过在 “首选项->界面->外观” 下更改 calibre 中的用户界面语言或通过设置 CALIBRE_OVERRIDE_LANG 环境变量集运行 calibre 来测试插件的翻译。 例如：

CALIBRE_OVERRIDE_LANG=de

将“de”替换为您要测试的语言的语言代码。

对于复数翻译，请使用“ngettext()”函数而不是“_()”。 例如：

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

插件 API

正如您在上面可能已经注意到的，calibre 中的插件是一个类。 calibre 中不同类型的插件有不同的类。 每个类的详细信息，包括所有插件的基类，可以在“插件”中找到。

您的插件几乎肯定会使用 calibre 中的代码。 要了解如何在 calibre 代码库中查找各种功能，请阅读有关 calibre“code_layout”的部分。

调试插件

第一步，也是最重要的一步是在调试模式下运行 calibre。 您可以使用以下命令从命令行执行此操作：

calibre-debug -g

或者在 calibre 中右键单击“首选项”按钮或使用“Ctrl+Shift+R”键盘快捷键。

从命令行运行时，调试输出将打印到控制台，从 calibre 内运行时，输出将打印到 txt 文件。

您可以在插件代码中的任何位置插入打印语句，它们将以调试模式输出。 请记住，这是 Python，除了打印语句之外，您实际上不需要任何其他东西来调试；）我仅使用这种调试技术开发了所有 calibre。

您可以使用以下命令行快速测试对插件的更改：

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

这将关闭正在运行的 calibre，等待关闭完成，然后更新 calibre 中的插件并重新启动 calibre。

更多插件示例

您可以在“此处 <https://www.mobileread.com/forums/showthread.php?t=118764>”找到许多复杂的 calibre 插件列表。

与他人分享您的插件

如果您想与 calibre 的其他用户分享您创建的插件，请将您的插件发布到“calibre 插件论坛”的新主题中 <https://www.mobileread.com/forums/forumdisplay.php?f=237 >`_。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 电子书中的排版数学

电子书中的排版数学

Calibre电子书阅读器能够显示嵌入在电子书(EPUB和HTML文件)中的数学。您可以直接使用TeX、MathML或AsciiMath来排版数学。Calibre电子书阅读器使用优秀的`MathJax<https://www.mathjax.org>`_书库来实现这一点。这是一个简短的教程，介绍如何使用数学制作电子书，并与优秀的电子书阅读器配合使用。

带有数学公式的简单 HTML 文件

你可以直接在简单 HTML 文件中的段落里穿插数学公式，这样 calibre 电子书阅读器就能正确排版数学公式。在如下的例子中，我们用 TeX 的数学公式记法。如你所见，你可以使用通常的 TeX 命令，不过要注意把 & 和大于小于符号分别写成 & < 和 >。

第一步是告诉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 文件在 calibre 中转成 EPUB，这样就能得到一本可以轻而易举与他人分享的电子书。

<!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>In-line 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>

更多信息

由于优秀的电子书阅读器使用MathJax书库来呈现数学，因此是在电子书中了解更多数学知识并获得帮助的最佳场所是 MathJax网站 [https://www.mathjax.org].

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 创建AZW3 、 EPUB 、 MOBI目录

创建AZW3 、 EPUB 、 MOBI目录

创建目录功能让你可以用多种格式去构建你的书库。该帮助文件描述生成AZW3, EPUB和MOBI格式目录时的选项。

- 添加图书到目录

- 包含章节

- 前缀

- 排除书籍

- 排除类型

- 其他选项

- 自定义目录封面

- 其他帮助资源

添加图书到目录

如果你需要*所有*书库目录，删除主窗口中的任何搜索或过滤条件。选择一本书后，您书库中的所有图书都将成为包含在生成的目录中的候选图书。个别书籍可能会因各种条件而被排除在外；有关详细信息，请参阅下面的 排除类型 部分。

如果只需要*一些*你的书库目录，你有两个选择：

- 选择多本需要创建目录的书籍。如果在 calibre 的主窗口中选择了不止一本书籍，则只会对所选书籍进行创建。

- 使用搜索字段或者标签浏览器来过滤展示的书籍。只使用展示的书籍创建目录。

要开始生成目录，选择菜单项：guilabel：转换书籍 > 在您的 calibre 书库中创建书籍目录。 您还可以将 创建目录 按钮添加到 首选项 > 界面 > 工具栏与菜单 以便更轻松地访问 创建目录 对话框。

在操作界面“目录选项”中，选择**AZW3、EPUB或MOBI**作为目录格式。在“目录标题”字段中，提供将用于生成目录的名称。如果已经存在具有相同名称和格式的目录，则会将其替换为新生成的目录。

启用选项`自动将目录发送到设备`将在完成后将生成的目录下载到已连接的设备。

包含章节

复选标记启用的部分将包括在生成的目录中：

- “作者” -按作者排序的所有书籍，以列表格式呈现。非系列书籍列在系列书籍之前。

- “名称” - 所有图书按书名分类，以列表形式呈现。

- “系列” - 属于系列的所有图书，按系列排序，以列表形式呈现。

- “类型” - 各个图书的流派类型以列表的形式呈现，按作者和系列排序。

- “最近添加” - 所有书籍，按时间倒序排列。列表包括最近30天内添加的图书，然后按月列出添加的图书。

- “概述” - 每本书的详细描述页面，包括封面缩略图和评论。按作者排序，非系列书籍列在系列书籍之前。

前缀

前缀规则允许您在满足特定条件时向预订列表添加前缀。例如，你可能想要用复选标记标记你读过的书，或者用X标记你愿望清单上的书。

第一列中的复选框将启用该规则。`名称`是您提供的规则名称。“字段”是“标记”或书库中的自定义列。“值”是要匹配的“字段”的内容。当满足前缀规则时，书籍将被标记为选定的“前缀”。

上面的示例中指定了三个前缀规则：

1. “阅读图书”指定在名称为“上次阅读”的自定义列中包含任何日期的书籍将带有复选标记符号作为前缀。

2. “Wishlist”项指定任何带有“Wishllist”标记的书籍都将以X符号为前缀。

3. “书库” 图书指定自定义列“书库中的可用图书”中值为True(或是)的任何图书都将带有双箭头符号前缀。

第一个匹配规则提供前缀。已禁用或不完整的规则将被忽略。

排除书籍

排除规则允许您指定不会编目的图书。

第一列中的复选框可启用该规则。“Name”是您提供的规则名称。“Field”可以是“Tags”，也可以是库中的自定义列。“Value”是要匹配的“Field”的内容。当满足排除规则时，图书将从生成的目录中排除。

上面的示例中指定了两个排除规则：

1. “Catalogs”规则指定任何带有“Catalogs”标签的图书都将从生成的目录中排除。

2. “已存档”图书规则指定自定义列“状态”中值为“已存档”的任何图书都将从生成的目录中排除。

所有规则都会针对每本书进行评估。禁用或不完整的规则将被忽略。

排除类型

生成目录时，数据库中的标记将用作流派。例如，您可以使用标签“小说”和“非小说”。这些标签成为生成目录中的流派，书籍根据其指定的标签列在各自的流派列表下。一本书将被列在每一个有相应标签的流派部分。

您可能会出于其他目的使用某些标签，也许用 + 来表示已读的书，或者用方括号括起来的标签（例如“[Amazon Freebie]”）来表示书籍的来源。 “排除的流派”正则表达式允许您指定不希望在生成的目录中用作流派的标签。 默认排除正则表达式模式``[.+]+``排除任何形式为``[tag]``的标签，并排除``+``（阅读书籍的默认标签） 在生成的目录中用作流派。

您也可以在正则表达式中使用精确的标记名。例如，“[亚马逊免费赠品]”或“[古腾堡计划]”。如果要列出多个精确的标签以进行排除，请在它们之间放一个竖线（竖线）字符：“[亚马逊免费赠品]|[古腾堡项目]”。

“正则表达式结果”根据数据库中的标签和您输入的正则表达式模式，显示在构建目录时哪些标签将被排除。当您修改正则表达式模式时，结果会更新。

其他选项

“目录封面”指定是生成新封面还是使用现有封面。可以为您的目录创建自定义封面-有关详细信息，请参阅“custom_catalog_covers”。如果您已经创建了想要重复使用的自定义封面，请选择`使用现有封面`。否则，选择`生成新封面`。

“Extra Description Note”指定要插入到封面缩略图旁边的描述页中的自定义列的内容。例如，您可能想要使用“Last Read”自定义列显示上次阅读一本书的日期。有关描述笔记功能的高级用法，请参阅Calibre论坛中的“此帖子<https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395>”_。

“缩略图宽度”指定描述页面中包含的封面缩略图的宽度首选项。缩略图被缓存以提高性能。要尝试不同的宽度，请尝试生成一个只有几本书的目录，直到您确定了首选宽度，然后生成完整的目录。第一次生成具有新缩略图宽度的目录时，性能会较慢，但后续构建的目录将利用缩略图缓存。

`与评论合并`指定自定义列，其内容将在目录生成过程中与评论元数据进行非破坏性合并。例如，您可能有一个自定义列“Author bio”，您希望将其附加到注释元数据中。您可以选择在备注部分*之前或之后*插入自定义列内容，并可以选择使用水平标尺分隔符分隔追加的内容。符合条件的自定义列类型包括“文本、注释和复合”。

自定义目录封面

安装“生成封面插件<https://www.mobileread.com/forums/showthread.php?t=124219>“后，您可以为您的目录创建自定义封面。要安装插件，请进入`首选项>高级>插件>获取新插件`。

其他帮助资源

有关Calibre的Catalog功能的更多信息，请参见MobileRead论坛粘滞的`创建目录-从这里开始<https://www.mobileread.com/forums/showthread.php?t=118556>`_，您可以在这里找到如何定制目录模板以及如何提交错误报告的信息。

如需提问或与其他用户讨论Calibre的目录功能，请访问MobileRead论坛`书库管理<https://www.mobileread.com/forums/forumdisplay.php?f=236>`_。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 教程 »
- 虚拟书库

虚拟书库

在 calibre 中，虚拟书库是一种告诉 calibre 仅打开普通书库的子集的方法。 例如，您可能只想使用特定作者的书籍或仅具有特定标签的书籍。 使用虚拟书库是将大型图书收藏划分为较小的子收藏的首选方法。 它优于将您的图书馆分成多个较小的书库，因为当您想要搜索整个馆藏时，您可以简单地返回到整个书库。 无法在 calibre 中同时搜索多个单独的书库。

虚拟书库不同于简单的搜索。 搜索只会限制图书列表中显示的图书列表。 虚拟书库可以做到这一点，此外它还限制左侧“标签浏览器”中显示的条目。 标签浏览器将仅显示来自虚拟书库中图书的标签、作者、系列、出版商等。 因此，虚拟书库的行为就像实际图书馆仅包含有限的书籍一样。

创建虚拟书库

要使用虚拟书库，请单击搜索栏左侧的“虚拟书库”按钮，然后选择“创建虚拟书库”选项。 作为第一个示例，让我们创建一个虚拟书库，该书库仅向我们显示特定作者的书籍。 单击下图所示的“作者”链接，选择您要使用的作者，然后单击“确定”。

“创建虚拟书库”对话框已为您填写。 单击“确定”，您将看到一个新的虚拟书库已创建，并自动切换到仅显示所选作者的书籍。 就calibre而言，就好像您的书库仅包含所选作者的书籍一样。

您可以随时切换回完整的书库，方法是再次点击“虚拟书库”并选择名为“<None>”的条目。

虚拟书库基于*搜索*。 您可以使用任何搜索作为虚拟书库的基础。 虚拟书库将仅包含与该搜索匹配的书籍。 首先，在搜索栏中输入您要使用的搜索或使用“标签浏览器”构建搜索。 当您对返回的结果感到满意时，单击“虚拟书库”按钮，选择“创建书库”并输入新虚拟书库的名称。 然后，将根据您刚刚输入的搜索创建虚拟书库。搜索非常强大，有关您可以使用它们执行的操作的示例，请参阅“search_interface”。

有用的虚拟书库示例

- 在最后一天添加到calibre的图书：

日期：>1天前

- 上个月添加到 calibre 的书籍：

日期：>30天前

- 评分为 5 星的书籍：

评分：5

- 评分至少为 4 星的书籍：

评分：>=4

- 没有评级的书籍：

评级：无

- calibre 中的“获取新闻”功能下载的期刊：

标签：=新闻 和 作者：=calibre

- 没有标签的书籍：

标签：无

- 没有封面的书籍：

封面：无

使用虚拟书库

您可以编辑或删除以前创建的虚拟书库，方法是单击`虚拟书库`并选择适当的操作。

您可以通过“首选项->界面->行为”告诉 calibre，在打开当前书库时您始终希望应用特定的虚拟书库。

通过单击“虚拟书库”按钮并选择“*当前搜索”条目，您可以快速将当前搜索用作临时虚拟书库。

您可以将所有可用的虚拟书库显示为图书列表上方的选项卡。 如果您喜欢经常在虚拟书库之间切换，这尤其方便。 单击“虚拟书库”按钮并选择“将虚拟书库显示为选项卡”。 您可以通过拖放来重新排列选项卡并关闭您不想看到的选项卡。 右键单击选项卡栏可以恢复关闭的选项卡。

在搜索中使用虚拟书库

您可以使用“vl:”前缀搜索虚拟书库中的书籍。 例如，vl:Read 将查找 Read 虚拟书库中的所有书籍。 搜索``vl:Read 和 vl:”Science Fiction”`` 将找到 Read 和 Science Fiction 虚拟书库中的所有书籍。

vl: 后面的值必须是虚拟书库的名称。 如果虚拟书库名称包含空格，则用引号将其引起来。

虚拟书库搜索的一种用途是在内容服务器中。 在“首选项 -> 通过网络共享 -> 需要用户名和密码”中，您可以限制用户可见的 calibre 书库。 对于每个可见的书库，您可以指定一个搜索表达式以进一步限制可以查看的书籍。 使用 vl:"虚拟书库名称" 将书籍限制为虚拟书库中的书籍。

使用附加限制

您可以使用“其他限制”进一步限制虚拟书库中显示的书籍。 附加限制是您之前创建的已保存搜索，可以应用于当前虚拟书库以进一步限制虚拟书库中显示的图书。 例如，假设您有一个虚拟书库，其中包含标记为“历史小说”的书籍，以及一个显示未读书籍的已保存搜索，您可以单击“虚拟书库”按钮，然后选择“其他限制”选项以仅显示 未读过的历史小说书籍。 要了解已保存的搜索，请参阅“saved_searches”。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- Calibre:// URL方案

Calibre:// URL方案

Calibre将自身注册为Calibre：//URL的处理程序。因此，您可以使用它们来执行从其他程序/文档或通过命令行打开书籍、搜索书籍等操作。例如，在命令行中运行以下命令：

calibre calibre://switch-library/Some_Library

将使用名为``某些书库``的书库打开Calibre。书库名称是书库文件夹的文件夹名称，用下划线替换空格。特定值``_``表示当前书库。下面记录了各种类型的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

这将在calibre中显示带有“book_id”（数字）的书籍。 通过将鼠标悬停在“图书详细信息”面板中的“单击打开”链接上，可以在 calibre 界面中看到图书的 ID，它是图书文件夹路径末尾括号中的数字。

您可以通过右键单击“图书详细信息”面板并选择“复制图书链接”来复制 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``是图书内部的可选位置。获取这些链接的最简单方法是在阅读器中打开一本书，然后在阅读器控件中选择`转到->位置`，此时会给出这样一个链接，您可以将其复制/粘贴到其他地方。

搜索书籍

URL语法为：

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

这里的查询是任何有效的”搜索表达式<search_interface>”。如果搜索表达式比较复杂，可以”将其编码为十六进制字符串<hex_encoding>”，并使用``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 中显示您想要的注释，然后单击“复制 URL”按钮将 URL 复制到剪贴板并将其粘贴到您需要的任何位置。

这里的“Field_Name”是列的名称，例如“authors”或“tags”。 对于用户创建的列，将字段名称中的前导“#”替换为下划线，因此“#mytags”变为“_mytags”。

除了使用“Item_Id”通过 id 指定项目之外，您还可以使用“val_Item_Name”或“hex_Hex_Encoded_Item_Name”通过名称指定它们。 例如：

calibre://book-details/Library_Name/authors/val_John%20Doe

URL参数的十六进制编码

URL 参数的十六进制编码是通过首先将参数编码为 UTF-8 字节，然后将每个字节替换为表示该字节的两个十六进制字符来完成的。 例如，字符串“abc”是UTF-8中的字节“0x61 0x62和0x63”，因此编码版本是字符串：“616263”。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 自定义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_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处理的文件类型。

- 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 in Tag browser # Set which author field to display in the Tag browser (the list of authors, # series, publishers etc on the left hand side). The choices are author and # author_sort. This tweak affects only what is displayed under the authors # category in the Tag browser and Content server. Please note that if you set this # to author_sort, it is very possible to see duplicate names in the list because # although it is guaranteed that author names are unique, there is no such # guarantee for author_sort values. Showing duplicates won't break anything, but # it could lead to some confusion. When using 'author_sort', the tooltip will # show the author's name. # Examples: # categories_use_field_for_author_name = 'author' # categories_use_field_for_author_name = 'author_sort' categories_use_field_for_author_name = 'author'

#: 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 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'", u'L´', u'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': ('Lo\\s+', 'Il\\s+', "L'", 'L´', 'La\\s+', 'Gli\\s+', 'I\\s+', 'Le\\s+', 'Uno\\s+', 'Un\\s+', 'Una\\s+', "Un'", 'Un´', 'Dei\\s+', 'Degli\\s+', 'Delle\\s+', 'Del\\s+', 'Della\\s+', 'Dello\\s+', "Dell'", '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'

取代图标，模板，等等

备注
calibre直接支持图标主题，有几个图标主题可供calibre使用，您可以通过图形界面：`首选项->界面->外观->更改图标主题`来使用。最好使用图标主题，而不是覆盖单个图标。

calibre允许您使用您喜欢的自定义版本覆盖静态资源，如图标、JavaScript和元数据封套模板、目录等。所有静态资源都存储在calibre安装位置的resources子文件夹中。在Windows上，这通常是：C:\Program Files\Calibre2\app\resources。在macOS上，通常是：/Applications/calibre.app/Contents/Resources/Resources/。在Linux上，如果您使用calibre网站上的二进制安装程序，它将是：/opt/calibre/resources。这些路径可以根据您选择安装calibre的位置而更改。

您不应更改此资源文件夹中的文件，因为您的更改将在下次更新calibre时被覆盖。而是转到图形界面“首选项->高级->杂项”，然后单击“打开calibre配置文件夹”。在此配置文件夹中，创建一个名为resources的子文件夹，并将要覆盖的文件放在其中。将文件放在适当的子文件夹中，例如将图像放在“resources/images”等中。下次启动时，calibre将自动使用自定义文件，而不是内置文件。

例如，如果您想更改“删除书籍”操作的图标，您首先要查看内置资源文件夹，并看到相关文件是“resources/images/remove_books.png”。 假设您有一个名为“my_remove_books.png ”的 PNG 格式备用图标，您可以将其保存在配置文件夹中，名称为 “resources/images/remove_books.png”。 calibre 用户界面使用的所有图标都位于 “resources/images”及其子文件夹中。 在此处放置覆盖文件将比自定义图标主题具有更高的优先级。

制作您自己的Calibre图标和主题

如果你已经创建了一组漂亮的图标，并希望通过Calibre内置的图标主题支持与其他Calibre用户分享它们，你可以很容易地将你的图标打包成一个主题。 为此，请转到“首选项->杂项->创建图标主题”，选择放置图标的文件夹。然后填写主题元数据并单击“确定”。这将生成一个包含主题图标的ZIP文件。你可以把它上传到calibre论坛`Mobilead<https://www.mobileread.com/forums/forumdisplay.php?f=166>`__，然后我会通过calibre内置的图标主题系统提供你的主题。默认情况下，您刚刚创建的图标主题也将安装为calibre中的当前主题。如果您正在测试主题，请记住从“resources/images”文件夹中删除图像，以便使用主题中的图标。

从Calibre 6开始，你可以定制白天模式和夜间模式的图标。只需创建两个版本的图标，并使用后缀表示夜间模式的``-for-dark-theme``和表示白天模式的``-for-light-theme``来命名文件。例如，Modify-for-Dark-Theme.png``和``Modify-For-light-Theme.png。然后，Calibre将根据当前主题自动使用适当的图标。

用插件自定义Calibre

Calibre是以模块化的形式设计的，其所有功能几乎都以插件的形式实现。这些插件用作于文本转换、新闻（或称之为诀窍）下载，和用户界面的各种组件，来连接到不同的设备，或在将文件添加到Calibre时处理他们等等。您可以在“选项→高级→插件”中完整地得到在Cailbre里已经内建的插件清单。

您可以编写自己的插件来定制和扩展calibre的行为。calibre中的插件架构非常简单，请参阅教程“pluginstutorial”。

一旦你写好了插件，你就可以把它上传到Calibre插件论坛`Mobileread<https://www.mobileread.com/forums/forumdisplay.php?f=237>`__，它将通过Calibre的内置插件更新器提供。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 自定义Calibre »
- 插件API文档

插件API文档

定义了各种抽象的基类，它们的子类可以创建功能强大的插件。这些有用的类包括：

- 插件

- 文件类型插件

- 元数据插件

- 书目插件

- 元数据下载插件

- 转换插件

- 设备驱动程序

- 用户界面操作

- 首选项插件

插件
class calibre.customize.Plugin(plugin_path)[源代码]

一个 calibre 插件。重要的成员包括：

- self.installation_type：存储插件的安装方式。

- self.plugin_path：存储包含以下内容的 ZIP 文件的路径

如果它是内置插件，则此插件或无

- self.site_customization: 存储一个用户输入的自定义字符串

由用户。

在子类中应当重写的方法：

- 初始化

- 定制_帮助

有用的方法：

- 临时文件

- __输入__

- 加载资源

supported_platforms = []

该插件适用的平台列表。 例如：['windows', 'osx', 'linux']

name = 'Trivial Plugin'

该插件的名称。 您必须将其设置为 Trivial Plugin 以外的其他内容才能正常工作。

version = (1, 0, 0)

此插件的版本为三-元组（主要、次要、修订版）

description = '不做修改'

描述此插件功能的短字符串

author = '未知'

这个插件的作者

priority = 1

当一种文件类型存在多个插件时，插件将按优先级降序运行。 优先级较高的插件将首先运行。 可能的最高优先级是“sys.maxsize”。 默认优先级为 1。

minimum_calibre_version = (0, 4, 118)

该插件需要的最早版本的 calibre

installation_type = None

这个插件的安装方式

can_be_disabled = True

如果为 False，用户将无法禁用此插件。 小心使用。

type = '基本'

该插件的类型。 用于在 GUI 中对插件进行分类

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，否则返回 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“插件名称”。 传递的任何参数都存在于 args 变量中。

文件类型插件
class calibre.customize.FileTypePlugin(plugin_path)[源代码]

基类：Plugin

与一组特定文件类型关联的插件。

file_types = {}

应运行此插件的文件类型集。 对所有文件类型使用“*”。 例如：{'lit', 'mobi', 'prc'}

on_import = False

如果为 True，则在将书籍添加到数据库时运行此插件

on_postimport = False

如果为 True，则在将书籍添加到数据库后运行此插件。 在这种情况下，将调用插件的 post import 和 post add 方法。

on_postconvert = False

如果为 True，则该插件将在书籍转换后运行。 在这种情况下，调用插件的 post convert 方法。

on_postdelete = False

如果为 True，则在从数据库中删除书籍文件后运行此插件。 在这种情况下，将调用插件的 post delete 方法。

on_preprocess = False

如果为 True，则该插件将在转换之前运行

on_postprocess = False

如果为 True，则在转换输出插件生成的最终文件后运行此插件。

type = '文件格式'

该插件的类型。 用于在 GUI 中对插件进行分类

run(path_to_ebook)[源代码]

运行插件。 必须在子类中实现。 它应该对电子书执行所需的任何修改，并返回修改后的电子书的绝对路径。 如果不需要修改，它应该返回原始电子书的路径。 如果遇到错误，它应该引发异常。 默认实现只是返回原始电子书的路径。 请注意，原始文件的路径（在运行任何文件类型插件之前，可用作 self.original_path_to_file）。

修改后的电子书文件应使用“temporary_file”方法创建。

参数:

path_to_ebook – 电子书的绝对路径。

返回:

修改后的电子书的绝对路径。

postimport(book_id, book_format, db)[源代码]

称为导入后，即在将书籍文件添加到数据库之后。 请注意，这与第一次创建图书记录时调用的“post add”不同。 每当新文件添加到图书记录时都会调用此方法。 它对于根据新添加的文件的内容修改图书记录非常有用。

参数:

- book_id – 添加的图书的数据库id。

- book_format – 添加的书籍的文件类型。

- db – 图书馆数据库。

postconvert(book_id, book_format, db)[源代码]

称为转换后，即转换后输出的书籍文件已添加到数据库中。 请注意，它仅在转换后运行，而不是在添加书籍后运行。 它对于根据新添加的文件的内容修改图书记录非常有用。

参数:

- book_id – 添加的图书的数据库id。

- book_format – 添加的书籍的文件类型。

- db – 图书馆数据库。

postdelete(book_id, book_format, db)[源代码]

称为删除后，即书籍文件从数据库中删除后。 请注意，删除图书记录时不会运行它，仅当删除图书中的一种或多种格式时才会运行。 它对于根据已删除文件的格式修改图书记录非常有用。

参数:

- book_id – 添加的图书的数据库id。

- book_format – 添加的书籍的文件类型。

- db – 图书馆数据库。

postadd(book_id, fmt_map, db)[源代码]

称为后添加，即在将一本书添加到数据库之后。 请注意，这与 post import 不同，post import 在将单个书籍文件添加到书籍后调用。 仅当第一次创建可能包含多个图书文件的整个图书记录时，才会调用 postadd()。 如果您希望在书籍首次添加到 calibre 时修改数据库中的书籍记录，这非常有用。

参数:

- book_id – 添加的图书的数据库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 = (7, 10, 0)

此插件的版本为三-元组（主要、次要、修订版）

author = 'Kovid Goyal'

这个插件的作者

type = '读取元数据'

该插件的类型。 用于在 GUI 中对插件进行分类

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 = (7, 10, 0)

此插件的版本为三-元组（主要、次要、修订版）

author = 'Kovid Goyal'

这个插件的作者

type = '编辑元数据'

该插件的类型。 用于在 GUI 中对插件进行分类

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 = '生成书目'

该插件的类型。 用于在 GUI 中对插件进行分类

cli_options = []

特定于此插件的 CLI 解析器选项，声明为 namedtuple Option：

从集合导入namedtuple Option=namedtupel（’Option’，’Option，default，dest，help’）cli_options=[选项（’-目录标题’，default=’我的目录’，dest=’catalog_title’，help=（_（’生成的目录的标题。n默认值：’）+“’”+’%ddefault’+”’”))]在calible.db.cli.cmd_catalog:Option_parser（）中解析的cli_options

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 = '元数据来源'

该插件的类型。 用于在 GUI 中对插件进行分类

author = 'Kovid Goyal'

这个插件的作者

supported_platforms = ['windows', 'osx', 'linux']

该插件适用的平台列表。 例如：['windows', 'osx', 'linux']

capabilities = frozenset({})

该插件支持的功能集。 有用的功能有：“识别”、“覆盖”

touched_fields = frozenset({})

该插件在识别阶段可能下载的元数据字段列表

has_html_comments = False

如果您的插件返回 HTML 格式的注释，请将此设置为 True

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)[源代码]

将作业列表尽可能均匀地拆分为最多 num 个组

test_fields(mi)[源代码]

返回 self.touched_fields 中 mi 对象上为 null 的第一个字段

clean_downloaded_metadata(mi)[源代码]

在将元数据对象放入 result_queue 之前，在插件的识别方法中调用此方法来规范元数据。 当然，您可以使用适合您的元数据源的自定义算法。

get_book_url(identifiers)[源代码]

返回 3 元组或 None。 3 元组的形式为：（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 元组的列表。 默认情况下，此方法仅调用 get_book_url。

get_cached_cover_url(identifiers)[源代码]

返回由标识符字典标识的书籍的缓存封面 URL，如果不存在此类 URL，则返回 None。

请注意，此方法必须仅返回经过验证的 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/等识别一本书。

如果指定了标识符(s)但未找到匹配项，并且此元数据源未存储所有相关标识符（例如，一本书的所有 ISBN），则此方法应仅使用标题和作者（假设已指定）重试。

如果此元数据源还提供封面，则应缓存封面的 URL，以便后续使用相同 ISBN/特殊标识符调用获取封面 API 时无需再次获取封面 URL。 为此，请使用缓存 API。

通过此方法放入 result_queue 的每个元数据对象都必须具有“source_relevance”属性，该属性是一个整数，指示元数据源为此查询返回结果的顺序。 该整数将由“compare_identify_results”使用。 如果顺序不重要，请将每个结果的顺序设置为零。

确保在将元数据对象放入 result_queue 之前缓存所有封面/ISBN 映射信息。

参数:

- log – 一个日志对象，用它来输出调试信息/错误

- result_queue – 一个结果队列，结果应该放入其中。 每个结果都是一个元数据对象

- abort – 如果 abort.is_set() 返回 True，则中止进一步处理并尽快返回

- title – 书名，可以为 None

- authors – 本书的作者列表，可以是 None

- identifiers – 其他标识符的字典，最常见的是 {‘isbn’:’1234…’}

- timeout – 超时，以秒为单位，网络请求挂起时间不应超过超时。

返回:

如果没有发生错误，则为 None，否则为适合向用户显示的错误的 unicode 表示形式

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
engineInputFormatPlugins 负责将文档转换为 HTML+OPF+CSS+等。 转换结果*必须*采用 UTF-8 编码。 主要操作发生在“convert”中。

type = '输入转换'

该插件的类型。 用于在 GUI 中对插件进行分类

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 文件一个。 如果输入文件可以是图像集合和非图像集合，则可以在转换方法中动态设置。 如果将此设置为 True，则必须实现返回图像列表的 get_images() 方法。

core_usage = 1

该插件使用的 CPU 核心数。 值为 -1 表示它使用所有可用核心

for_viewer = False

如果设置为 True，输入插件将执行特殊处理，使其输出适合查看

output_encoding = 'utf-8'

此输入插件创建文件所采用的编码。 None 值表示编码未定义，该值必须单独检测

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

所有输入格式插件共享的选项。 不要在子类中重写。 请改用“选项”。 每个选项都必须是“OptionRecommendation”的实例。

options = {}

自定义此插件行为的选项。 每个选项都必须是 :OptionRecommendation 的实例。

recommendations = {}

一组形式为（选项名称、推荐值、推荐级别）的三元组

get_images()[源代码]

如果此输入插件代表图像集合，则返回图像的绝对路径列表。 图像列表的顺序与书脊和目录的顺序相同。

convert(stream, options, file_ext, log, accelerators)[源代码]

该方法必须在子类中实现。 它必须返回创建的 OPF 文件或“OEBBook”实例的路径。 所有输出应包含在当前文件夹中。 如果此插件在当前文件夹之外创建文件，则必须在此方法返回之前将其删除/标记为删除。

参数:

- stream – 包含输入文件的类似文件的对象。

- options – 自定义转换过程的选项。 保证具有与该插件声明的所有选项相对应的属性。 此外，它还有一个详细属性，该属性采用从零开始的整数值。 数字越大意味着越详细。 另一个有用的属性是“input_profile”，它是“calibre.customize.profiles.InputProfile”的实例。

- file_ext – 输入文件的扩展名（不带 .）。 它保证是该插件支持的“file_types”之一。

- log – 一个 calibre.utils.logging.Log 对象。 所有输出都应使用该对象。

- accelarators – 输入插件可以轻松获取的各种信息的字典，这将加快转换的后续阶段。

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 GUI 中配置此插件的小部件。 该小部件必须是 PluginWidget 类的实例。 有关示例，请参阅内置输入插件。

class calibre.customize.conversion.OutputFormatPlugin(*args)[源代码]

基类：Plugin

OutputFormatPlugins 负责将 OEB 文档（OPF+HTML）转换为输出电子书。

可以假定 OEB 文档采用 UTF-8 编码。 主要操作发生在“convert”中。

type = '输出转换'

该插件的类型。 用于在 GUI 中对插件进行分类

can_be_disabled = False

如果为 False，用户将无法禁用此插件。 小心使用。

supported_platforms = ['windows', 'osx', 'linux']

该插件适用的平台列表。 例如：['windows', 'osx', 'linux']

file_type = None

该插件输出的文件类型（扩展名不带前导句点）

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

所有输入格式插件共享的选项。 不要在子类中重写。 请改用“选项”。 每个选项都必须是“OptionRecommendation”的实例。

options = {}

自定义此插件行为的选项。 每个选项都必须是 :OptionRecommendation 的实例。

recommendations = {}

一组形式为（选项名称、推荐值、推荐级别）的三元组

property description

str(对象=’’) -> str str(bytes_or_buffer[, 编码[, 错误]]) -> str

从给定对象创建一个新的字符串对象。如果指定了编码或错误，则对象必须公开将使用给定编码和错误处理程序解码的数据缓冲区。否则，返回返回object.__str__()(如果已定义)或repr(对象)的结果。编码默认为sys.getdefaultencoding()。错误默认为“严格”。

convert(oeb_book, output, input_plugin, opts, log)[源代码]

将“oeb_book”（它是“calibre.ebooks.oeb.OEBBook”的实例）的内容渲染到输出指定的文件中。

参数:

- 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 GUI 中配置此插件的小部件。 该小部件必须是 PluginWidget 类的实例。 有关示例，请参阅内置输出插件。

设备驱动程序

所有设备驱动程序的基类是“DevicePlugin”。 但是，如果您的设备将自身作为 USBMS 驱动器公开给操作系统，则您应该使用 USBMS 类，因为它实现了支持此类设备所需的所有逻辑。

class calibre.devices.interface.DevicePlugin(plugin_path)[源代码]

基类：Plugin

定义与电子书阅读器通信的后端应实现的接口。

type = '设备接口'

该插件的类型。 用于在 GUI 中对插件进行分类

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

如果设备支持在sync_booklists期间更新封面缩略图，则将此设置为True。 将其设置为 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

如果状态栏中没有“无”，GUI 会将其显示为消息。 如果打开需要很长时间，则很有用

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

一组扩展是设备上的“虚拟书籍”，因此无法查看/保存/添加到书库。 例如：frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

向用户显示虚拟图书扩展的消息。

NUKE_COMMENTS = None

是否删除发送到设备的书籍副本中的评论。 如果不是“无”，这应该是注释将被替换的短字符串。

MANAGES_DEVICE_PRESENCE = False

如果为True，则表示该驱动程序完全管理设备检测、弹出等。 如果将此设置为 True，则*必须*实现 detector_managed_devices 和 debug_management_device_detection 方法。 将此设置为 true 的驱动程序负责检测设备、管理设备黑名单、弹出设备列表等。 calibre 将定期调用 detector_management_devices() 方法，如果返回检测到的设备，calibre 将调用 open()。 每次返回设备时都会调用 open()，即使之前对 open() 的调用失败，因此驱动程序必须维护自己的失败设备黑名单。 类似地，当弹出时，calibre将调用eject()，然后假设下一次调用Detect_management_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’:detailed_msg} 形式的字典，以便 calibre 在运行一些回调后向用户弹出一条消息（当前仅 upload_books）。 请注意不要向用户发送过多的垃圾邮件。 在*每个*回调后都会检查此变量，因此仅在真正需要时才设置它。

classmethod get_open_popup_message()[源代码]

GUI 将其显示为非模式弹出窗口。 应该是 OpenPopupMessage 的实例

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 时调用。

扫描该驱动程序可以处理的设备。 如果找到设备，则应返回设备对象。 该对象将作为connected_device 传递给open() 方法。 如果没有找到设备，则返回 None。 返回的对象可以是任何东西，calibre 不使用它，它只传递给 open()。

此方法由 GUI 定期调用，因此请确保它不会占用太多资源。 使用缓存以避免重复扫描系统。

参数:

- 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 – 来自设备扫描仪的设备信息

can_handle_windows(usbdevice, debug=False)[源代码]

对设备执行进一步检查以查看该驱动程序是否能够处理它的可选方法。 如果不是，则应返回 False。 该方法仅在供应商、产品 ID 和 bcd 匹配后调用，因此它可以执行一些相对耗时的检查。 默认实现返回 True。 该方法仅在 Windows 上调用。 另请参见“can_handle”。

请注意，对于基于 USBMS 的设备，此方法默认委托给“can_handle”。 因此，您只需要在 USBMS 的子类中重写“can_handle”即可。

参数:

usbdevice – 由`calibre.devices.winusb.scan_usb_devices`返回的usbdevice

can_handle(device_info, debug=False)[源代码]

can_handle_windows 的 Unix 版本。

参数:

device_info – 是一个元组（vid、pid、bcd、制造商、产品、序列号）

open(connected_device, library_uuid)[源代码]

执行任何设备特定的初始化。 在检测到设备之后但在与设备通信的任何其他函数之前调用。 例如：对于将自身呈现为 USB 大容量存储设备的设备，此方法将负责安装该设备，或者如果该设备已自动安装，则负责查找其已安装的位置。 方法 calibre.devices.usbms.device.Device.open 具有此函数的实现，应该作为 USB 大容量存储设备的一个很好的示例。

此方法可以引发 OpenFeedback 异常以向用户显示消息。

参数:

- connected_device – 我们正在尝试打开的设备。 它是（供应商 id、产品 id、bcd、制造商名称、产品名称、设备序列号）的元组。 但是，某些设备没有序列号，并且在 Windows 上仅存在前三个字段，其余字段均为“无”。

- library_uuid – 当前 calibre 库的 UUID。 如果没有库（例如从命令行使用时），则可以为 None。

eject()[源代码]

从操作系统中卸载/弹出设备。 这不会检查是否有需要与设备通信的待处理 GUI 作业。

注意：此方法可能无法与其余设备方法在同一线程上调用。

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 元素列表。 如果不存在任何内存卡，则不会为内存卡的前缀设置“无”。 例如。 (‘/place’, ‘/place2’) (无, ‘place2’) (‘place’, 无) (无, 无)

total_space(end_session=True)[源代码]

获取安装设备上的可用总空间：

1. 主内存

2. 存储卡A

3. 存储卡B

返回:

一个 3 元素列表，总空间（以字节为单位）为 (1, 2, 3)。 如果特定设备没有任何这些位置，则应返回 0。

free_space(end_session=True)[源代码]

获取挂载点上的可用空间：

1. 主内存

2. 存储卡 A

3. 存储卡 B

返回:

一个 3 元素列表，其可用空间以字节为单位 (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”。 如果 on_card 不是 None，则 FreeSpaceError 的文本必须包含单词“card”，否则它必须包含单词“memory”。

参数:

- files – 路径列表

- names – 书籍应上传到设备的文件名列表。 len(名称) == len(文件)

- metadata – 如果不是 None，则它是“Metadata”对象的列表。 这个想法是使用元数据来确定将书籍放置在设备上的位置。 len(元数据) == len(文件). 除了常规封面（封面路径）之外，还可能存在缩略图属性，应优先使用该属性。 缩略图属性的形式为（宽度、高度、cover_data as 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)、books(oncard=’carda’)、books(oncard=’cardb’))。

delete_books(paths, end_session=True)[源代码]

删除设备上路径中的书籍。

classmethod remove_books_from_metadata(paths, booklists)[源代码]

从元数据列表中删除书籍。 此功能不得与设备通信。

参数:

- paths – 设备上书籍的路径。

- booklists – 包含调用结果的元组 (books(oncard=None)、books(oncard=’carda’)、books(oncard=’cardb’))。

sync_booklists(booklists, end_session=True)[源代码]

更新设备上的元数据。

参数:

booklists – 包含调用结果的元组 (books(oncard=None)、books(oncard=’carda’)、books(oncard=’cardb’))。

get_file(path, outfile, end_session=True)[源代码]

读取设备上“path”处的文件并将其写入输出文件。

参数:

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_PLUGBOARD_NAME）、您感兴趣的格式（“真实”格式或“device_db”）以及插件板（这些都是由set_plugboard提供的，与您获得此方法的位置相同）。

返回:

无或单个插件板实例。

set_driveinfo_name(location_code, name)[源代码]

将driveinfo 文件中的设备名称设置为“name”。 此设置将持续存在，直到重新创建文件或再次更改名称。

非磁盘设备应根据 get_device_information() 方法返回的位置代码来实现此方法。

prepare_addable_books(paths)[源代码]

给定一个路径列表，返回另一个路径列表。 这些路径指向书籍的可添加版本。

如果准备一本书时出现错误，则该书返回的列表中的位置应为三元组，而不是路径：（original_path、异常实例、traceback）

startup()[源代码]

当 calibre 启动设备时调用。 执行任何所需的初始化。 请注意，可以实例化该类的多个实例，因此可以多次调用 __init__ ，但只有一个实例会调用此方法。 该方法在设备线程上调用，而不是在 GUI 线程上调用。

shutdown()[源代码]

当 calibre 关闭时调用，无论是永久关闭还是准备重新启动。 进行任何所需的清理。 该方法在设备线程上调用，而不是在 GUI 线程上调用。

get_device_uid()[源代码]

必须返回当前连接设备的唯一 ID（这在成功调用 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)[源代码]

设置该驱动程序应忽略的设备 uid 列表。

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()[源代码]

启动插件时由设备管理器调用。 如果此方法返回一个字符串，则 a) 它支持设备管理器的动态控制接口，b) 与插件对话时将使用该名称。

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

start_plugin()[源代码]

调用此方法来启动插件。 该插件应该开始接受设备连接，但它确实这样做了。 如果插件已经接受连接，则不执行任何操作。

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

stop_plugin()[源代码]

调用此方法来停止插件。 该插件不应再接受连接，并且应在其自身后面进行清理。 这个方法很可能应该调用 shutdown。 如果插件已经不接受连接，则不执行任何操作。

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

get_option(opt_string, default=None)[源代码]

返回 opt_string 指示的选项的值。 该方法可以在插件未启动时调用。 如果选项不存在，则返回 None。

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

set_option(opt_string, opt_value)[源代码]

设置 opt_string 指示的选项的值。 该方法可以在插件未启动时调用。

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

is_running()[源代码]

如果插件启动则返回 True，否则返回 false

该方法可以在GUI线程上调用。 实现此方法的驱动程序必须是线程安全的。

synchronize_with_db(db, book_id, book_metadata, first_call)[源代码]

当设备上的书籍与 calibre 数据库中的书籍匹配时在书籍匹配期间调用。 该方法负责将数据从设备同步到 calibre 的数据库（如果需要）。

该方法必须返回一个二值元组。 第一个值是一组 calibre 书籍 ID，如果 calibre 的数据库已更改，如果数据库未更改，则为“无”。 如果第一个值是空集，则设备上书籍的元数据将使用 calibre 的元数据进行更新并返回给设备，但不会对该书籍进行 GUI 刷新。 当口径数据正确但必须发送到设备时，这非常有用。

第二个值本身就是一个 2 值元组。 元组中的第一个值指定是否应将书籍格式发送到设备。 目的是允许验证设备上的书与书的口径相同。 如果不发送书籍，则该值必须为 None，否则返回设备上的基本文件名（类似 foobar.epub 的字符串）。 请务必在名称中包含扩展名。 设备子系统将为所有返回值非 None 的书籍构造一个 send_books 作业。 注意：除了稍后检索扩展名之外，在设备使用模板生成文件名的情况下（大多数设备都会这样做），该名称将被忽略。 返回元组中的第二个值指示格式是否是未来的。 如果是则返回 True，否则返回 False。 calibre 将向用户显示一个对话框，列出所有未来日期的书籍。

极其重要：该方法在 GUI 线程上调用。 对于设备管理器的线程来说，它必须是线程安全的。

book_id：数据库中书籍的 calibre id。 book_metadata：来自设备的书籍的元数据对象。 first_call：如果这是同步期间的第一次调用，则为 True，否则为 False

class calibre.devices.interface.BookList(oncard, prefix, settings)[源代码]

基类：list

书籍清单。 每个 Book 对象必须具有字段

1. 书名

2. 作者

3. 大小（书籍文件大小）

4. 日期时间（UTC 时间元组）

5. 路径（设备上书籍的路径）

6. 缩略图（可以为 None）缩略图是带有图像数据的 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大容量存储的设备

此类设备的基类是“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 id 字符串中标识设备主内存的字符串 这可以是 None、字符串、字符串列表或已编译的正则表达式

WINDOWS_CARD_A_MEM = None

标识 Windows PnP id 字符串中设备的第一张卡的字符串 这可以是 None、字符串、字符串列表或已编译的正则表达式

WINDOWS_CARD_B_MEM = None

标识 Windows PnP id 字符串中设备的第二张卡的字符串 这可以是 None、字符串、字符串列表或已编译的正则表达式

OSX_MAIN_MEM_VOL_PAT = None

新驱动程序检测使用它来区分主内存和存储卡。 应该是与 macOS 分配的主内存挂载点匹配的正则表达式

BACKLOADING_ERROR_MESSAGE = None

MAX_PATH_LEN = 250

设备上创建的路径的最大长度

NEWS_IN_FOLDER = True

将新闻放在自己的文件夹中

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[源代码]

参数:

- key – 解锁设备的密钥

- log_packets – 如果为 true，则记录传入/传出设备的数据包流

- report_progress – 针对各种任务以百分比进度（0 到 100 之间的数字）调用的函数。 如果以-1调用则表示该任务没有任何进度信息

- detected_device – 来自设备扫描仪的设备信息

set_progress_reporter(report_progress)[源代码]

设置报告进度信息的功能。

参数:

report_progress – 针对各种任务以百分比进度（0 到 100 之间的数字）调用的函数。 如果以-1调用则表示该任务没有任何进度信息

card_prefix(end_session=True)[源代码]

返回卡上路径前缀的 2 元素列表。 如果不存在任何内存卡，则不会为内存卡的前缀设置“无”。 例如。 (‘/place’, ‘/place2’) (无, ‘place2’) (‘place’, 无) (无, 无)

total_space(end_session=True)[源代码]

获取安装设备上的可用总空间：

1. 主内存

2. 存储卡A

3. 存储卡B

返回:

一个 3 元素列表，总空间（以字节为单位）为 (1, 2, 3)。 如果特定设备没有任何这些位置，则应返回 0。

free_space(end_session=True)[源代码]

获取挂载点上的可用空间：

1. 主内存

2. 存储卡 A

3. 存储卡 B

返回:

一个 3 元素列表，其可用空间以字节为单位 (1, 2, 3)。 如果特定设备没有任何这些位置，则应返回 -1。

windows_sort_drives(drives)[源代码]

调用以消除不根据“WINDOWS_CARD_NAME”区分主内存和存储卡的设备的歧义。 例如：EB600

can_handle_windows(usbdevice, debug=False)[源代码]

对设备执行进一步检查以查看该驱动程序是否能够处理它的可选方法。 如果不是，则应返回 False。 该方法仅在供应商、产品 ID 和 bcd 匹配后调用，因此它可以执行一些相对耗时的检查。 默认实现返回 True。 该方法仅在 Windows 上调用。 另请参见“can_handle”。

请注意，对于基于 USBMS 的设备，此方法默认委托给“can_handle”。 因此，您只需要在 USBMS 的子类中重写“can_handle”即可。

参数:

usbdevice – 由`calibre.devices.winusb.scan_usb_devices`返回的usbdevice

open(connected_device, library_uuid)[源代码]

执行任何设备特定的初始化。 在检测到设备之后但在与设备通信的任何其他函数之前调用。 例如：对于将自身呈现为 USB 大容量存储设备的设备，此方法将负责安装该设备，或者如果该设备已自动安装，则负责查找其已安装的位置。 方法 calibre.devices.usbms.device.Device.open 具有此函数的实现，应该作为 USB 大容量存储设备的一个很好的示例。

此方法可以引发 OpenFeedback 异常以向用户显示消息。

参数:

- connected_device – 我们正在尝试打开的设备。 它是（供应商 id、产品 id、bcd、制造商名称、产品名称、设备序列号）的元组。 但是，某些设备没有序列号，并且在 Windows 上仅存在前三个字段，其余字段均为“无”。

- library_uuid – 当前 calibre 库的 UUID。 如果没有库（例如从命令行使用时），则可以为 None。

eject()[源代码]

从操作系统中卸载/弹出设备。 这不会检查是否有需要与设备通信的待处理 GUI 作业。

注意：此方法可能无法与其余设备方法在同一线程上调用。

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)[源代码]

将driveinfo 文件中的设备名称设置为“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”。 如果 on_card 不是 None，则 FreeSpaceError 的文本必须包含单词“card”，否则它必须包含单词“memory”。

参数:

- files – 路径列表

- names – 书籍应上传到设备的文件名列表。 len(名称) == len(文件)

- metadata – 如果不是 None，则它是“Metadata”对象的列表。 这个想法是使用元数据来确定将书籍放置在设备上的位置。 len(元数据) == len(文件). 除了常规封面（封面路径）之外，还可能存在缩略图属性，应优先使用该属性。 缩略图属性的形式为（宽度、高度、cover_data as 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)、books(oncard=’carda’)、books(oncard=’cardb’))。

delete_books(paths, end_session=True)[源代码]

删除设备上路径中的书籍。

remove_books_from_metadata(paths, booklists)[源代码]

从元数据列表中删除书籍。 此功能不得与设备通信。

参数:

- paths – 设备上书籍的路径。

- booklists – 包含调用结果的元组 (books(oncard=None)、books(oncard=’carda’)、books(oncard=’cardb’))。

sync_booklists(booklists, end_session=True)[源代码]

更新设备上的元数据。

参数:

booklists – 包含调用结果的元组 (books(oncard=None)、books(oncard=’carda’)、books(oncard=’cardb’))。

classmethod normalize_path(path)[源代码]

带有平台本机路径分隔符的返回路径

用户界面操作

如果您要在 ZIP 文件中添加自己的插件，则应该对 InterfaceActionBase 和 InterfaceAction 进行子类化。 InterfaceActionBase 子类的 load_actual_plugin 方法必须返回 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 方法。

初始化后，该插件可以通过“gui”成员访问主 calibre GUI。 您可以通过名称访问其他插件，例如：

self.gui.iactions['Save To Disk']

要访问实际的插件，请使用“interface_action_base_plugin”属性，该属性仅在插件初始化后才可用。 如果您想使用插件类中的方法，例如 do_user_config()，则很有用。

由“action_spec”指定的 QAction 会自动创建并以“self.qaction”形式提供。

name = 'Implement me'

插件名称。 如果存在两个同名插件，则优先级较高的插件优先。

priority = 1

插件优先级。 如果存在两个同名插件，则优先级较高的插件优先。

popup_type = 1

将此插件添加到工具栏时的菜单弹出类型

auto_repeat = False

按住快捷键时是否自动重复此操作。

action_spec = ('text', 'icon', 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'

操作类型 “当前”表示作用于当前视图 “全局”表示操作不作用于当前视图，而是作用于整个 calibre

accepts_drops = False

如果为 True，则此 InterfaceAction 将有机会与拖放事件进行交互。 有关详细信息，请参阅方法“accept_enter_event”、“accept_drag_move_event”、“drop_event”。

accept_enter_event(event, mime_data)[源代码]

当且仅当此界面操作能够处理拖动事件时，此方法应返回 True。 不要对事件调用接受/忽略，这将由 calibre UI 处理。

accept_drag_move_event(event, mime_data)[源代码]

当且仅当此界面操作能够处理拖动事件时，此方法应返回 True。 不要对事件调用接受/忽略，这将由 calibre UI 处理。

drop_event(event, mime_data)[源代码]

当且仅当该接口操作能够处理放置事件时，此方法应该执行一些有用的操作并返回 True。 不要对事件调用接受/忽略，这将由 calibre UI 处理。 您不应在此函数中执行阻塞/长操作。 相反，发出信号或使用 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，则不会为此操作注册键盘快捷键。 如果为“无”，则注册没有默认键绑定的键盘快捷键。 字符串和字符串列表使用指定的默认键绑定注册快捷方式。

- 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()[源代码]

当主 GUI 布局完成时，每个操作调用一次。 如果您的操作需要对布局进行更改，则应该在此处完成，而不是在“initialization_complete”中完成。

initialization_complete()[源代码]

当主 GUI 初始化完成时，每个操作调用一次。

tag_browser_context_action(index)[源代码]

在标签浏览器中显示上下文菜单时调用。 index 是指向右键单击的标签浏览器项的 QModelIndex。 使用index.valid()测试其有效性，并使用index.data(Qt.ItemDataRole.UserRole)获取底层TagTreeItem对象。 此方法生成的任何操作对象都将添加到上下文菜单中。

shutting_down()[源代码]

当主 GUI 正在关闭时，每个插件调用一次。 释放所有使用的资源，但尽量不要长时间阻止关闭。

class calibre.customize.InterfaceActionBase(*args, **kwargs)[源代码]

基类：Plugin

supported_platforms = ['windows', 'osx', 'linux']

该插件适用的平台列表。 例如：['windows', 'osx', 'linux']

author = 'Kovid Goyal'

这个插件的作者

type = '用户界面操作'

该插件的类型。 用于在 GUI 中对插件进行分类

can_be_disabled = False

如果为 False，用户将无法禁用此插件。 小心使用。

load_actual_plugin(gui)[源代码]

该方法必须返回实际的界面操作插件对象。

首选项插件
class calibre.customize.PreferencesPlugin(plugin_path)[源代码]

基类：Plugin

代表“首选项”对话框中显示的小部件的插件。

这个插件只有一个重要的方法“create_widget”。 插件的各个字段控制它在 UI 中的分类方式。

supported_platforms = ['windows', 'osx', 'linux']

该插件适用的平台列表。 例如：['windows', 'osx', 'linux']

author = 'Kovid Goyal'

这个插件的作者

type = '首选项'

该插件的类型。 用于在 GUI 中对插件进行分类

can_be_disabled = False

如果为 False，用户将无法禁用此插件。 小心使用。

config_widget = None

包含名为 ConfigWidget 的类（实现 ConfigWidgetInterface）的模块的导入路径。 由“create_widget”使用。

category_order = 100

该插件的“类别”应位于类别列表中的位置。

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

当且仅当执行了 restore_to_defaults 方法时设置为 True。

restore_defaults_desc = '将设置还原为默认值。您必须点击“应用”以实际保存默认设置。'

“恢复默认值”按钮的工具提示

restart_critical = False

如果为 True，首选项对话框将不允许用户设置更多首选项。 仅当“commit”返回 True 时才有效。

genesis(gui)[源代码]

在显示小部件之前调用一次，应执行任何必要的设置。

参数:

gui – calibre主要图形用户界面

initialize()[源代码]

应该将所有配置值设置为其初始值（存储在配置文件中的值）。 “返回”语句是可选的。 如果不显示对话框，则返回 False。

restore_defaults()[源代码]

应该将所有配置值设置为默认值。

commit()[源代码]

保存所有更改的设置。 如果更改需要重新启动，则返回 True，否则返回 False。 引发“AbortCommit”异常以指示发生错误。 您有责任向用户提供有关错误是什么以及如何纠正错误的反馈。

refresh_gui(gui)[源代码]

提交此小部件后调用一次。 负责使 GUI 重新读取任何更改的设置。 请注意，默认情况下 GUI 无论如何都会重新初始化各种元素，因此大多数小部件不需要使用此方法。

initial_tab_changed()[源代码]

如果在小部件显示之前但初始化之后更改了最初显示的选项卡，则调用。

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[源代码]

包含用于轻松添加标准配置小部件的代码的基类，如复选框、组合框、文本字段等。 请参阅“注册”方法。

此类自动处理注册设置的更改通知、重置为默认值、gui 对象和配置对象之间的转换等。

如果您的配置小部件继承自此类，但包含未注册的设置，则应覆盖“ConfigWidgetInterface”方法并调用覆盖内的基类方法。

changed_signal

每当用户更改此小部件中的值时，都必须发出此信号

supports_restoring_to_defaults = True

当且仅当执行了 restore_to_defaults 方法时设置为 True。

restart_critical = False

如果为 True，首选项对话框将不允许用户设置更多首选项。 仅当“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 – 如果此设置是基于多项选择（组合框）的设置，则为选项列表。 该列表是两个元素元组的列表，其形式为：[(gui name, value), ...]

- setting – 负责管理此设置的类。 默认类处理几乎所有情况，因此很少使用此参数。

initialize()[源代码]

应该将所有配置值设置为其初始值（存储在配置文件中的值）。 “返回”语句是可选的。 如果不显示对话框，则返回 False。

commit(*args)[源代码]

保存所有更改的设置。 如果更改需要重新启动，则返回 True，否则返回 False。 引发“AbortCommit”异常以指示发生错误。 您有责任向用户提供有关错误是什么以及如何纠正错误的反馈。

restore_defaults(*args)[源代码]

应该将所有配置值设置为默认值。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面

命令行界面

备注

在macOS系统中，命令行工具位于Calibre软件包中，例如，如果您安装的Calibre位于文件夹:/Applications 中，, 命令行工具位于文件夹:file:/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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibre

calibre

calibre [options] [path_to_ebook or calibre url ...]

启动主图形用户界面，并可选地将位于path_to_book的电子书添加到数据库中。您还可以指定:command:calibre URL来执行各种不同的操作，而不仅仅是添加书籍。例如：
calibre://view-book/test_library/1842/epub

将通过:command:`calibre`电子书阅读器以EPUB格式打开书库“test_library”中的id为1842的书籍。书库名称是书库的文件夹名称，空格用下划线替换。用户手册中对各种基于URL的操作进行了详细描述。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibre-customize

calibre-customize

calibre-customize options

通过加载外部插件自定义 calibre。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--add-plugin, -a

通过指定包含插件的 ZIP 文件的路径来添加插件。

--build-plugin, -b

致插件开发者：指向你所开发插件文件夹的路径。此命令会自动压缩插件并在 calibre 中更新它。

--customize-plugin

自定义插件。 设置插件名称和以逗号分隔的自定义字符串。 自定义字符串与您在主calibre界面中自定义插件时输入的字符串相同。

--disable-plugin

禁用该名称的插件

--enable-plugin

启用该名称的插件

--help, -h

显示此帮助信息并退出

--list-plugins, -l

列出全部已安装的插件

--remove-plugin, -r

按名称删除自定义插件(对内置插件无效)。

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibre-debug

calibre-debug

calibre-debug [options]

各种有助于调试calibre的命令行接口。如果没有选项，此命令将启动内置的Python解释器。你也可以在 debug 模式下运行主要的 cailbre GUI，calibre 电子书阅读器和 calibre 编辑器。

它还包含了各种没有专用命令行工具的calibre的接口，比如字体子集，电子书对比工具等等。

你也能用 calibre-debug 来运行独立的脚本。像这样来操作：

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

在 -- 后的的所有东西都被传输到了脚本里。您也可以在脚本中使用calibre debug作为shebang。类似这样：

#!/usr/bin/env -S calibre-debug -e -- –

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--add-simple-plugin

通过指定包含插件代码的py文件的路径，添加一个简单的插件（例如仅由.py文件组成的插件）

--command, -c

运行 Python 代码。

--debug-device-driver, -d

调试'设备检测'

--default-programs

从 Windows 默认程序注册 / 取消注册 calibre。 --default-programs = (register|unregister)

--diff

启动 calibre diff工具。例如： calibre-debug --diff file1 file2

--edit-book

用调试模式启动 calibre "编辑书籍"工具。

--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-calibre-data /path/to/empty/export/folder /path/to/library/folder1 /path/to/library2 calibre-debug --export-all-calibre-data /export/folder all # 导出所有已知书库

--fix-multiprocessing

供内部使用

--gui, -g

在调试模式下运行用户界面。调试输出信息打印到 stdout 与 stderr 。

--gui-debug

使用调试控制台运行用户界面，日志输出到指定路径。仅适用于内部使用，使用-g选项在调试模式下运行用户界面。

--help, -h

显示此帮助信息并退出

--implode-book, -i

聚合以前拆解过的书籍。 用法：-i output_dir file.epub 从output_dir的文件中导入书籍，这些文件必须是由之前调用:option:`–explode-book`创建的。一定要指定与拆解时使用的文件格式相同的文件格式。

--import-calibre-data

导入先前导出的calibre数据

--inspect-mobi, -m

查看指定路径的 MOBI 文件

--paths

设置 calibre 环境时输出路径是必需的

--run-plugin, -r

运行一个提供命令行接口的插件。例如： calibre-debug -r“插件名称”– file1 --option1 在–后方的内容将被当作参数传递给插件。

--run-test, -t

运行命名的测试。使用特殊值“all”运行所有测试。如果测试名称以句点开头，则假定为模块名称。如果测试名称以@开头，则假定为类别名称。

--run-without-debug

不要在设置了DEBUG标志的情况下运行

--shutdown-running-calibre, -s

本操作会导致正在运行的 calibre 关闭。请注意：如果正在执行某些任务，那么这些任务会被强行中断。

--subset-font, -f

将指定的字体子集化。在此选项后使用 -- 来传递选项到字体子集化程序。

--test-build

在构建中测试二进制模组

--version

显示程序版本号并退出

--viewer, -w

用调试模式打开电子书阅读器

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibre-server

calibre-server

calibre-server [选项] [书库文件夹的路径...]

启动caliber内容服务器。calibre 内容服务器通过互联网公开你的 calibre 书库。你可以将书库文件夹的路径指定为:command:calibre-server 参数。如果未指定任何路径，将使用calibre程序所知道的所有书库。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--access-log

访问日志文件的路径。此日志包含有关连接到服务器并发出请求的客户端的信息。默认情况下，不记录访问日志。

--ajax-timeout

在查询时等待服务器响应的时间(秒).

--auth-mode

选择认证方式. 设置服务器使用的HTTP身份验证模式。如果将服务器放在SSL代理后面，则设置为"basic"，否则将其设为 "auto"。如果配置了SSL，将使用"basic"，否则使用"digest"。

--auto-reload

当源代码更改时自动重新加载服务器。有利于开发。应该为关闭超时指定一个较小的值。

--ban-after

触发封禁的登录失败次数. 达到该登录失败次数后IP地址将被封禁

--ban-for

封禁反复登录失败的IP地址. 临时禁止在指定的分钟数内反复登录失败的IP地址的访问。有助于防止猜测密码。如果设置为零则不禁止。

--book-list-mode

选择默认的书籍列表显示模式. 设置用于新用户的默认书籍列表模式. 每名用户都可以单独重新设置此选项.默认使用封面网格。

--compress-min-size

响应使用数据压缩的最小大小(字节).

--custom-list-template

包含自定义书籍列表模式的模板的JSON文件的路径。创建这种模板文件的最简单方法是在caliber中前往'首选项->通过网络共享->书籍列表模板'，创建模板并导出它。

--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对你的 calibre 书库进行更改，那么这选项是非常有用。请注意，打开这个选项后意味着在服务器所在的计算机上运行的任何程序都可以对你的 calibre 书库进行更改。 默认情况下, 此选项是禁用的。

--enable-log-not-found, --disable-log-not-found

记录HTTP 404(未找到)的请求. 通常，服务器会记录所有未找到资源的HTTP请求。如果你的服务器被恶意程序攻击，则会产生大量的垃圾日志。使用此选项可将其关闭。 默认情况下, 此选项是启用的。

--enable-use-bonjour, --disable-use-bonjour

通过BonJour广播OPDS源. 通过BonJour广播OPDS源，这样基于OPDS的阅读应用程序就可以自动检测和连接到服务器。 默认情况下, 此选项是启用的。

--enable-use-sendfile, --disable-use-sendfile

传输时不复制文件以提高性能. 当通过网络发送文件时，将在内核传输中使用零复制，从而提高性能。 但是，它可能会导致在某些损坏的文件系统上的文件传输异常。如果遇到文件传输异常，请将其关闭。 默认情况下, 此选项是启用的。

--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 消息头的最大大小（KB）.

--max-job-time

工作进程的最大时间. 允许工作进程运行的最大时间量(分钟)。设为零则无限制。

--max-jobs

最大工作进程数. 工作进程按需启动，用于大型任务，如准备书籍以供阅读、添加书籍、转换等。通常，此类进程的最大数量取决于CPU内核的数量。您可以通过此设置控制它。

--max-log-size

日志文件最大大小（MB）. 由服务器生成的日志文件的最大容量。当日志变得大于这个容量时，旧日志将被自动覆盖。设置为零时禁用日志覆盖。

--max-opds-items

OPDS源的最大书籍数. 服务器将在单个OPDS请求中返回的最大书籍数量。

--max-opds-ungrouped-items

OPDS源中未分组项目的最大数量. 当有超过这个设定数量的结果时将以如作者/标签的首字母对返回的结果进行分组。设置为零以禁用分组。

--max-request-body-size

服务器允许上传的最大文件大小（MB）.

--num-per-page

在一页中显示的书籍数量. 浏览器中单个页面显示的书籍数量。

--pidfile

写入进程 PID 到指定的文件

--port

侦听连接的端口.

--search-the-net-urls

包含“在网上搜索”指定的URL的JSON文件的路径。创建这样的文件最简单的方法是进入'首选项->通过网络共享->在网上搜索'，创建URL并将其导出。

--shutdown-timeout

等待完全关闭的总时间(秒).

--ssl-certfile

SSL证书文件的路径.

--ssl-keyfile

SSL私钥文件的路径.

--timeout

空闲连接关闭的等待时间(秒).

--trusted-ips

允许来自特定IP的未经身份验证的连接更改书库. 通常，如果您不打开身份验证，服务器将以只读模式运行，从而不允许匿名用户更改您的Calibre书库。此选项允许从指定IP连接的任何人修改书库。必须是逗号分隔的网络地址或网络规范的列表。 如果您希望在不进行身份验证的情况下运行服务器，但仍使用Calibredb对您的Calibre书库进行更改，则此功能非常有用。请注意，启用此选项意味着从指定IP连接的任何人都可以更改您的Calibre库。

--url-prefix

所有URL的预置前缀. 如果希望在反向代理后运行此服务器，则非常有用。例如，使用 /calibre 作为URL前缀。

--userdb

用于身份验证的用户数据库路径。数据库是SQLite文件。使用:option:`–manage-users`创建它。您可以阅读以下关于管理用户的信息：https://manual.calibre-ebook.com/server.html#managing-user-accounts-from-the-command-line-only

--version

显示程序版本号并退出

--worker-count

用于处理请求的任务线程数.

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibre-smtp

calibre-smtp

calibre-smtp [选项] [from to和文本]

使用SMTP协议发送邮件。calibre-smtp 有两种操作模型。在“撰写”模式中，您可以指定“from to ”和“正文”，它们用于撰写和发送电子邮件。在筛选模式下，calibre-smtp 从STDIN读取完整的电子邮件正文在内的信息并发送。 ‘文本’是电子邮件的正文。如果未指定文本，则会从STDIN读取完整的含正文的电子邮件。“from”是发件人的电子邮件地址，“to ”是收件人的电子邮件地址。 当从STDIN读取完整的电子邮件内容时，仅在SMTP协商中使用“from”和“to”电子邮件，不修改邮件头。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--fork, -f

在后台发送邮件。如果使用此选项, 还应使用 --outbox 处理发送失败的邮件。

--help, -h

显示此帮助信息并退出

--localhost, -l

本机(localhost)的主机名。连接到SMTP服务器时使用。

--outbox, -o

邮件文件夹中存储发送失败的电子邮件的路径。

--timeout, -t

连接超时

--verbose, -v

更详细

--version

显示程序版本号并退出

COMPOSE MAIL

撰写电子邮件的选项。如果正文未设置，将被忽略。

--attachment, -a

邮件附件

--subject, -s

邮件标题

SMTP RELAY

使用SMTP中继服务器发送邮件的选项。calibre 将尝试直接发送电子邮件，除非指定了 –relay。

--cafile

一个PEM格式的CA证书文件(用于在使用TLS时验证服务器证书)的路径。默认情况下，使用系统CA证书。

--dont-verify-server-certificate

在使用TLS连接时不验证服务器证书。这是caliber 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- calibredb

calibredb

calibredb command [options] [arguments]

calibredb 是calibre数据库的命令行接口。它有几个子命令，记录如下。

calibredb 可以用来操作一个 calibre 数据库由运行在上的 calibre 或 calibre“内容服务器”指定本地机器或通过互联网。你可以运行一个 calibre “内容服务器”使用：calibre-server`程序或在主 calibre 程序中单击 “连接/共享 -> 启动内容服务器”。因为`calibredb 可以更改 calibre 书库，必须首先在服务器上设置身份验证。这里有两种方法：

- 如果您计划只连接到运行在同一台计算机上的服务器，则只需使用内容服务器的 --enable-local-write 选项，允许本地计算机上运行的任何程序，包括 calibredb，对 calibre 数据进行更改。从主 calibre 程序运行服务器时，此选项位于“首选项->通过网络共享->高级”。

- 如果要启用通过互联网的访问，则应在服务器上设置用户帐户，并使用：选项：–username 和：选项：–password，命令：calibredb 选项授予访问权限。您可以为命令：calibre-server 设置用户身份验证，方法是使用 --enable-auth 选项并使用

--manage-users 创建用户帐户。如果从主 calibre 程序运行服务器，请使用：

“首选项->通过网络共享->需要用户名/密码”。

要连接到正在运行的内容服务器，请将服务器的URL传递给： –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是要在内容服务器上连接的书库的书库id。您可以使用特殊的library_id值 - 来获得服务器上可用书库id的列表。有关如何通过内容服务器设置访问的详情，请参阅https://manual.calibre-ebook.com/generated/en/calibredb.html。

--password

连接到 calibre 内容服务器的密码。若要从标准输入读取密码，请使用特殊值：<stdin>。若要从文件读取密码，请使用<f:/path/to/file>（即<f:后跟文件的完整路径和尾随 >）。上面的角括号是必需的，记住要转义它们，或者使用引号。

--timeout

通过网络连接到calibre书库时的超时时间，以秒为单位。默认值为两分钟。

--username

用于连接到内容服务器的用户名

--version

显示程序版本号并退出

list

calibredb list [options]

列出 calibre 数据库中可用书籍。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/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"在字段列表中，则要运行的模板。默认值：无

--template_file, -t

如果"template" 在字段列表中，则包含要运行的模板的文件的路径。默认值：无

--template_heading

模板栏的标题。默认值 template. 如果设置了选项:option:–for-machine ，则忽略此选项

add

calibredb add [选项] file1 file2 file3 ...

将指定文件当做书籍添加到数据库。你可以指定目录，请查看下面目录相关选项。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--authors, -a

设置已添加书籍的作者

--automerge, -m

如果找到具有类似书名和作者的书籍，自动将输入格式(文件)合并到现有书籍记录中。值“Ignore”表示丢弃重复的格式。值“overwrite”表示书库中的重复格式将被新添加的文件覆盖。值“new_record”表示将重复的格式放入新的书籍记录中。

--cover, -c

设置已添加书籍的封面路径

--duplicates, -d

即使已经存在，也添加书籍到数据库中。 根据书名和作者进行比较。 请注意，:option:`–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的书籍。id为以逗号分隔的 id 号列表（你可以使用搜索命令来获得 id 号）。例如，23,34,57-85（如果指定一个区间，该区间的最后一个数字不会被包括在内）。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--permanent

不要使用回收站

add_format

calibredb add_format [options] id ebook_file

将 ebook_file中的电子书添加到由id标识的书籍的可用格式中。您可以使用搜索命令获取id。如果格式已经存在，则替换它，除非指定了不替换选项。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--as-extra-data-file

将文件作为额外的数据文件而不是电子书格式添加到书籍中

--dont-replace

如果格式已经存在不要替换它

remove_format

calibredb remove_format [options] id fmt

从ID标识的逻辑书籍中删除特定格式- fmt。你可以通过使用搜索命令获取id。fmt 应该是形如LRF或TXT或EPUB的一个文件扩展名。如果逻辑书籍没有 fmt 可用则什么也不做。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

show_metadata

calibredb show_metadata [options] id

显示在 calibre 数据库中存储的指定id的书籍的元数据。 id是来自搜索命令的id号。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--as-opf

以OPF格式(XML)打印元数据

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

根据 OPF 文件 –metadata.opf 设置存储在 calibre 数据库中的以 book_id 标识的书籍的元数据。 book_id 是来自搜索命令的 id 号。您可以通过使用 –as-opf 切换到 show_metadata 命令来快速了解 OPF 格式。还可以用 –field 选项设置各个字段的元数据。如果使用 –field 选项则不需要指定 OPF 文件。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--field, -f

设置字段。格式是field_name:value，例如：--field tags:tag1,tag2。使用:option:–list-fields`来获取所有字段名称的列表。你可以多次指定此选项来设置多个字段。注意：对于语言，你必须使用ISO639语言代码（例如en代表英文，fr代表法语等）。对于书籍标识符，语法是:option:–field` identifiers:isbn:XXXX,doi:YYYYY。对于布尔(是/否)字段使用true 和 false 或者 yes 和 no。

--list-fields, -l

列出可用于 --field 选项的元数据字段名称

export

calibredb export [options] ids

导出指定id(以逗号分隔的列表)的书籍至文件系统。导出操作将保存书籍的所有格式，以及其封面和元数据(保存至一个opf文件)。也会保存与书籍相关联的任何额外的数据文件。你可以使用搜索命令来获得id。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--all

导出数据库中所有的书籍，忽略列表中的id。

--dont-asciiize

让calibre将文件名中的所有非英文字符转换成英文对应字符。如果保存到不完全支持Unicode文件名的旧文件系统中，这将非常有用。 设定此项将关闭此操作。

--dont-save-cover

通常，calibre将把封面保存为单独文件，和电子书文件一起生成。 设定此项将关闭此操作。

--dont-save-extra-files

保存书籍时保存与书籍关联的所有数据文件 设定此项将关闭此操作。

--dont-update-metadata

通常，calibre将根据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

calibredb catalog /path/to/destination.(csv|epub|mobi|xml...) [options]

Export a catalog in format specified by path/to/destination extension. Options control how entries are displayed in the generated catalog output. Note that different catalog formats support different sets of options. To see the different options, specify the name of the output file and then the –help option.

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--ids, -i

要编目的逗号分割的数据库 ID 列表。 一旦声明，则忽略 --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

用正则表达式描述要排除的视为类型的标签。 默认值： '[.+]|^+$' 不包括括号内的标签，例如 '[Project Gutenberg]', 和 '+', 默认的用于阅读书籍的标签。 适用于： AZW3, EPUB, MOBI 输出格式

--exclusion-rules

指定用于从生成的书目中排除书籍的规则。 排除规则的模型是('<rule name>','标签','<comma-separated list of tags>') 或('<rule name>','<custom column>','<pattern>')。 例如： (('存档的书籍','#status'','已存档'),) 将在自定义栏目“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' 或 '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

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

add_custom_column

calibredb add_custom_column [options] label name datatype

创建一个自定义栏目，栏目名为你自定义的名称，不能包含空格或冒号。数据类型可为：bool, comments, composite, datetime, enumeration, float, int, rating, series, text

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--display

用于自定义如何解释此栏目中的数据的选项的字典。这是一个 JSON 字符串。对于枚举栏目，使用 --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" 有许多选项可以进入显示变量，按栏目类型的选项是： composite: composite_template, composite_sort, make_category,contains_html, use_decorations datetime: date_format enumeration: enum_values, enum_colors, use_decorations int, float: number_format text: is_names, use_decorations 找到适当组合的最好方法是在图形界面中创建适当类型的自定义栏目，然后查看书籍的备份 OPF（确保自从添加该栏目以后已经创建了新的 OPF）。在 OPF 中的新栏目中，你将看到 JSON 的“显示”。

--is-multiple

此栏目存储类似标签的数据(例如，多个逗号分隔的值)。仅当数据类型为文本时才适用。

custom_columns

calibredb custom_columns [options]

列出可用的自定义栏目。显示栏目标签和id。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--details, -d

显示每个栏目的详情。

remove_custom_column

calibredb remove_custom_column [options] label

删除由标签标识的自定义栏目。可以使用custom_columns command命令查看可用栏目。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--force, -f

无需确认

set_custom

calibredb set_custom [options] column id value

为 id 标识的书籍设置自定义栏目的值。您可以使用搜索命令获取id列表。您可以使用 custom_columns 命令获取自定义栏目名的列表。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--append, -a

如果栏目可以有多个值，将指定值添加到已有值之后，而非覆盖已有值。

restore_database

calibredb restore_database [options]

从calibre书库每个文件夹中的OPF文件含有的元数据恢复此数据库，如果你的metadata.db文件已损坏，此方法很有用。

警告：此命令会完全重新生成数据库。你会丢失所有搜索记录，自定义分类，元数据处理规则，已保存的每本书的转换设置和自定义的新闻获取规则。恢复的元数据和OPF文件中的一样准确。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--really-do-it, -r

确认恢复。除非指定了此选项，否则命令不会运行。

check_library

calibredb check_library [options]

对书库的文件系统执行一些检查。报告是invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--csv, -c

以 CSV 输出

--ignore_extensions, -e

逗号分隔的将被忽略的扩展名列表。 默认值：all

--ignore_names, -n

逗号分隔的将被忽略的名称列表。 默认值：all

--report, -r

逗号分隔的报告列表。 默认值: all

--vacuum-fts-db

清空全文搜索数据库。根据数据库的大小，这可能会非常慢并且占用大量内存。

list_categories

calibredb list_categories [options]

在数据库中生成一个类别信息的报告。该信息与分类浏览器中显示的信息相同。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--categories, -r

逗号分隔类别列表检索名。默认值：全部

--csv, -c

以 CSV 输出

--dialect

产生CSV文件的类型。选择：excel, excel-tab, unix

--item_count, -i

只输出每个类别中的项(标签、作者等)的数目，而不是类别内的每项包含的书籍数

--width, -w

输出中的单行宽度最大值。默认为检测到的屏幕大小。

backup_metadata

calibredb backup_metadata [options]

将存储在数据库中的元数据备份到每个书籍文件夹中的单个 OPF 文件中。这通常是自动的，但是您可以运行此命令来强制重新生成 OPF 文件，并使用 –all 选项。

请注意，通常不需要这样做，因为每次元数据更改时， OPF 文件都会自动备份。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--all

通常，此命令只对OPF文件过期的书籍有效。这个选项使它在所有的书籍上运行。

clone

calibredb clone path/to/new/library

创建当前书库的副本。这将创建一个新的、空的书库，它所有的自定义栏目、虚拟书库和其他设置与当前书库相同。

副本书库将没有任何书籍。如果您想要创建一个完整的副本书库，包括所有的书籍，那么只需使用您的文件系统工具来复制书库文件夹。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

embed_metadata

calibredb embed_metadata [options] book_id

通过calibre 数据库中的元数据更新存储在 calibre 书库中的实际书籍文件中的元数据。通常，只有从 calibre 导出文件时才更新元数据，如果希望文件就地更新，则此命令很有用。请注意，不同的文件格式支持不同数量的元数据。你可以使用book_id 的特殊值“all”来更新所有书籍中的元数据。还可以指定许多由空格分隔的书籍id 和由连字符分隔的ID区间。例如：calibredb embed_metadata 1 2 10-15 23

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--only-formats, -f

只更新指定格式的文件中的元数据。可为多种格式指定多次。默认情况下，所有格式都会更新。

search

calibredb search [options] search expression

搜索书库中指定的搜索项，返回与搜索表达式匹配的以逗号分隔的书籍id列表。输出格式对于输入接受id列表的其他命令非常有用。搜索表达式可以是来自calibre强大的搜索查询语言中的任何内容，例如：calibredb searchauthor:asimov ‘title:”i robot”’

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--limit, -l

返回的最大结果数。默认是所有结果。

fts_index

calibredb fts_index [options] enable/disable/status/reindex

控制全文搜索索引进程。

enable

启用此书库的全文索引

disable

关闭此书库的全文索引

status

显示当前索引的状态

reindex

可用于重新索引特定书籍或整个书库。要重新索引特定书籍请在reindex命令后将书籍id指定为

附加参数。如果未指定书籍id，则会重新

索引整个书库。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--indexing-speed

索引的速度。使用fast可使用所有计算机资源进行快速索引，使用slow可使用较少的资源进行索引。请注意，每次调用索引后，索引速度都会重置为slow。

--wait-for-completion

等待所有书籍都编入索引，定期显示索引进度

fts_search

calibredb fts_search [options] search expression

对整个书库或其子集进行全文搜索。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

--do-not-match-on-related-words

只匹配精确的字词而不是相关的字词。因此，correction将与orrecting不匹配。

--include-snippets

包括每个匹配项相关的文本片段。请注意，这会使搜索速度慢得多。

--indexing-threshold

允许搜索前必须索引多少书库，以百分比表示。默认为90

--match-end-marker

用于标示文本段内匹配单词的结尾的标记

--match-start-marker

用于标示文本段内匹配单词的开头的标记

--output-format

输出搜索结果的格式。纯文本为“text”，JSON输出为“json”。

--restrict-to

使用搜索表达式或id限制搜索的书籍。例如：ids:1,2,3表示按id限制，或search:tag:foo表示限制具有标签foo的书籍。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [options]

转换不同格式的电子书。

input_file 表示输入文件，output_file 表示输出文件。这两者作为命令行参数必须指定到最前面。

输出电子书格式由output_file的文件扩展名推测。output_file也可以是特殊格式.EXT，其中EXT是输出文件扩展名。在这种情况下，输出文件的名称源自输入文件的名称。请注意，文件名不能以连字符开头。如果output_file不含扩展名，那么它将被视为一个文件夹，由HTML文件组成的“开放电子书”（OEB）将被写入该文件夹。这些文件会被视为正常文件而被输出插件所识别。

在指定输入和输出文件后，你可以自定义特定的转换选项。根据输入和输出文件格式不同可用的转换选项也不同。如需获取针对输入和输出文件的帮助，请在命令行中输入 -h。

对于转换系统的完整文档请查阅电子书转换

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

选项以及选项的默认值都取决于输入和输出格式，所以你应该经常检查:

ebook-convert myfile.input_format myfile.output_format -h

以下是一些常见转化的选项，接着是特定输入输出的选项

- 输入格式选项

- 输出格式选项

- 界面外观

- 智能处理

- 查找与替换

- 结构检测

- 目录

- 元数据

- 调试

--help, -h

显示此帮助信息并退出

--input-profile

设置输入配置。输入配置让程序了解如何解析输入文档并进行转换。例如页面分辨率(i.e. 以像素为单位)。可选项有： 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_large, generic_eink, generic_eink_hd

--version

显示程序版本号并退出

输入格式选项

处理输入 mobi 文件的选项设置

--input-encoding

指定输入文件的字符编码。该设置将覆盖文档声明的字符编码。该设置对于处理未声明字符编码和字符编码声明错误的输入文件十分有用。

输出格式选项

处理输出 epub 的选项设置

--dont-split-on-page-breaks

关闭在分页符处将文件切分为多个的功能. 通常, 会在分页符处将输入文件做切分生成多个文件. 由此可以输出使用资源少、解析快的电子书. 当然切分操作会增加处理时间, 如果输入文件含有较多的分页符, 请关闭此功能.

--epub-flatten

仅当需要使用FBReaderJ阅读EPUB文件时使用本选项。启用本选项会消除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

将生成的EPUB文件的内容解压到指定文件夹。请注意，该文件夹的内容会被清空。

--flow-size

将大于该大小 (以KB为单位) 的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表示形式。请小心使用，因为这将用ASCII替换Unicode字符。例如，它将用“Pele”替换“Pelé”。此外，请注意，如果一个字符有多个表示形式（例如，中文和日语共享的字符），则将基于Calibre界面语言的来确定所用的形式。

--base-font-size

基准字体大小（以PT为单位）。在生成的书籍中所有的字体尺寸将基于这个大小重新缩放。通过选择更大的尺寸，可以使输出中的字体更大，反之亦然。默认情况下，当值为0时，根据你选择的'输出配置'确定基准字体大小。

--change-justification

更改文本对齐方式。 使用值"左对齐" 将转换资源中所有的文本为左对齐。 (如：未对齐) 使用值"justify" 将转换资源中所有的未对齐的文本为居中对齐。使用值"original"(默认)则不更改源文件的对齐方式。请注意，仅部分输出格式支持对齐方式。

--disable-font-rescaling

禁用文字大小缩放功能。

--embed-all-fonts

嵌入输入文档中引用到但未嵌入的字体。软件会在你的系统中查找这些字体，找到了就嵌入。字体嵌入只有当你的目标格式支持时才有用，例如 EPUB、AZW3、DOCX 和 PDF。请保证你拥有在文档中嵌入字体的合适授权。

--embed-font-family

在书籍中嵌入指定的字体。这里指定了书籍中使用的“基础”字体。如果输入文件指定了字体，就有可能覆盖此处的基础字体。你可以使用“过滤样式信息”的选项来删除输入文档中指定的字体。注意，字体内嵌只在一些输出格式中有用，主要是 EPUB、AZW3 和 DOCX。

--expand-css

默认情况下，calibre 将使用各种CSS属性的简写形式，如边距 margin、填充padding、边框border等。此选项将使用完整书写方式。注意，在生成EPUB文件时，由于Nook不能处理简写的CSS，所以将'输出配置'设置为Nook配置中的一个时，CSS总是使用完整书写。

--extra-css

CSS 样式表或原始 CSS 文件路径。该 CSS 样式表将会被添加到源文件的样式规则中，它可以用于覆盖那些样式规则。

--filter-css

将从所有CSS样式规则中删除的以逗号分隔的 CSS 属性列表。如果某些样式信息阻碍了阅读器设备中的样式设置，则此选项非常有用。例如：字体、颜色、左边距、右边距

--font-size-mapping

将CSS字体名称映射为以PT为单位的字体大小. 例如您可以将这里的值设为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 会把“连字”转换成对应的字母组合。本选项用于选择保留“连字”。

--line-height

行高（以PT为单位）。控制连续文本行之间的行高。仅适用于未定义自身行高的内容元素。大多数情况下，最小行高值更加有用。默认不做行高调整。

--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)。如果你将此值设置为负数，则使用输入文档中自身定义的缩进，换句话说就是 calibre 不更改缩进处理。

--smarten-punctuation

将普通引号、破折号和省略号转换为与它们对应的正确印刷体。有关详情, 请参阅 https://daringfireball.net/projects/smartypants。

--subset-embedded-fonts

只嵌入字体中用到的字。每种嵌入字体缩减到只包含文档中用到的字型。这能减小字体文件的体积。在嵌入特别大的，包含大量未使用字的字体时有用(如中文)。

--transform-css-rules

包含转换本书中CSS样式的规则的文件路径。创建这样一个文件的最简单方法是在 calibre GUI中使用向导创建规则。在“界面外观->转换样式”的转换对话框部分访问它。 一旦创建规则，就可以使用“导出”按钮将它们保存到文件中。

--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

包含查找替换正则表达式的文件路径。该文件必须包含正则表达式交替行后面跟着替换规则（它可以是一个空行）。正则表达式必须是Python正则表达式的语法并且文件必须是UTF-8编码。

--sr1-replace

sr1-search搜索到的内容的替换文本。

--sr1-search

要被sr1-replace替换的搜索规则（正则表达式）。

--sr2-replace

sr2-search搜索到的内容的替换文本

--sr2-search

要被sr2-replace替换的搜索规则（正则表达式）。

--sr3-replace

sr3-search搜索到的内容的替换文本

--sr3-search

要被sr3-replace替换的搜索规则（正则表达式）。

结构检测

设置自动检测文档结构。

--chapter

用于检测章节标题的XPath表达式。默认情况下，将包含单词“chapter”、“book”、“section”、“prolog”、“epilogue”或“part”的<h1>或<h2>标签以及任何具有class＝“chapter”的标签被视为章节标题。所用表达式的必须使用上述列出的元素。要禁用章节检测，请使用表达式“/”。有关使用此功能的更多帮助，请参阅calibre用户手册中的XPath教程。

--chapter-mark

指定如何标记检测到的章节。值“pagebreak”将在章节之前插入分页符。值“rule”将在章节前插入一个空行。值“none”将禁用章节标记，值“both”将同时插入分页符和一个空行来标记章节。

--disable-remove-fake-margins

有些文档通过在每个段落上指定左右边距来指定页边距。 calibre 会尝试检测并删除这些页边距。有时候 calibre 会错误的删除不应该删除的页边距，在这种情况下，你可以禁用这项功能。

--insert-metadata

将书籍元数据添加到书籍的开头。如果你的电子书阅读设备不支持直接显示和搜索元数据，该功能可能有所帮助。

--page-breaks-before

XPath表达式。分页符插入在指定元素之前。要禁用，请使用表达式：/

--prefer-metadata-cover

使用从源文件中检测到的封面文件.

--remove-first-image

删除输入电子书的第一张图片。用于输入文档有一个封面图像但其没有被设为封面的情况。在这种情况下，如果你在 calibre 设置了封面，又没有选择该项，输出文件将最终有两个封面图片。

--start-reading-at

用于检测文档阅读时的起始页的位置的 XPath 表达式。某些电子书阅读器(以 Kindle 为代表)使用此位置来打开书籍。请参阅 calibre 用户手册中的 XPath 教程了解使用此功能的更多帮助。

目录

设置自动生成目录。默认情况下若源文件有目录，它将被优先使用，而不是自动创建。

--duplicate-links-in-toc

当从输入文档中的链接创建目录时，允许创建重复项。即允许目录中出现名称相同的项，前提是它们指向文档中不同的位置。

--level1-toc

XPath表达式，指定应添加到一级目录中的所有标签。如果设置了此项，则它优先于其他形式的自动检测。有关示例，请参阅calibre用户手册中的XPath教程。

--level2-toc

XPath表达式，指定应添加到二级目录中的所有标签。每项都添加到前一级目录项下。有关示例，请参阅calibre用户手册中的XPath教程。

--level3-toc

XPath表达式，指定应添加到三级目录中的所有标签。每项都将加入到二级目录项下。请参阅 calibre 用户手册中 XPath 教程的例子。

--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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- ebook-edit

ebook-edit

ebook-edit [opts] [path_to_ebook] [name_of_file_inside_book ...]

运行 calibre 书籍编辑工具。你还可以指定书中的文件名称，这些文件将自动打开以进行编辑。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--detach

断开任何存在的控制终端，如果有的话（仅Linux）

--help, -h

显示此帮助信息并退出

--select-text

打开书籍进行编辑时，要在书籍中选择的文本

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- ebook-meta

ebook-meta

ebook-meta ebook_file [options]

对电子书文件读/写元数据

支持读取元数据的格式：azw, azw1, azw3, azw4, cb7, cbc, cbr, cbz, chm, docx, epub, fb2, fbz, html, htmlz, imp, 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, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

不同的文件格式支持不同种类的元数据。如果你试图在某种格式上设置其不支持的元数据，这种元数据会被忽略。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--author-sort

用以进行作者排序的字符串. 如果该参数未指定, 同时作者字段为非空, 它将由作者字段自动生成.

--authors, -a

设置作者. 请用 & 字符隔开多个作者. 作者的名称按'名字 姓氏'的顺序.

--book-producer, -k

设置书籍制作人。

--category

设置书籍类别.

--comments, -c

设置电子书描述。

--cover

使用指定文件作为封面.

--date, -d

设置出版日期。

--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

设置 LRF 文件的 BookID

--publisher, -p

设置电子书出版商。

--rating, -r

设置评分。评荐值为 1 到 5 之间的数字。

--series, -s

设置书籍所属丛书。

--tags

设置书籍的标签。多个标签之间请用逗号隔开。

--title, -t

设置书名。

--title-sort

用以进行书名排序的版本值. 如果该参数未指定, 同时书名字段为非空, 它将由书名字段自动生成.

--to-opf

指定 OPF 文件名。元数据会被写入 OPF 文件。

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- ebook-polish

ebook-polish

ebook-polish [选项] 输入文件 [输出文件]

优化书籍就是在你精心制作的电子书的基础上精益求精。

优化将尝试尽量减少对你的电子书内部代码的调整。与转换不同，它不做展开CSS、重命名文件、更改字体大小、调整边距等操作。每个动作只做所需的最小的设置调整。

你应该在电子书创作过程中的最后使用这个工具。

注意，优化只对AZW3 或 EPUB格式的文件起作用。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--add-soft-hyphens, -H

给书中的所有字词添加软连字符。这样，在不支持连字的电子阅读器中，当文本对齐时，可以更好地渲染呈现书籍。

--compress-images, -i

无损压缩书中的图片，减少文件大小而不影响图片质量。

--cover, -c

图片封面路径。更改指定的电子书的封面。如果无封面，或者封面没有正确识别，插入一个新的封面。

--embed-fonts, -e

文档中尚未嵌入所有引用的字体。此操作将会扫描你电脑中的字体， 如果找到这些字体，将嵌入这些字体到文档中。 请确保你有足够的权限嵌入在此文档使用的字体。

--help, -h

显示此帮助信息并退出

--jacket, -j

在书籍的开头插入一个“书籍护封”页，它包含所有的书籍元数据，如 书名、标签、作者、丛书、简介等。任何以前的书籍护封都将被替换。

--opf, -o

OPF文件路径。从OPF文件更新书中的元数据。

--remove-jacket

删除先前插入的书籍护封页。

--remove-soft-hyphens

从本书所有文字中删除软连字符。

--remove-unused-css, -u

从样式表和 <style> 标签中删除所有未使用的 CSS 规则。 有些用出版模板制作的书籍会有大量不匹配任何内容的额外的CSS 规则。 这些额外的CSS 规则会减慢阅读器的速度，因为它们必须先全部解析。

--smarten-punctuation, -p

把纯文本中的破折号、省略号、引号、多个连字符转换为排版正确的等效形式。 注意，该算法可能有时会产生不正确的结果，特别是缩写开头有单引号时。

--subset-fonts, -f

嵌入字体子集意味着可以减少嵌入字体的体积大小，因为只 嵌入书籍中所调用的字符。这有助于大幅减少字体文件的大 小（通常能减少近一半的体积）。 举个例子，如果一本书为标题使用了特定的字体，字体子集 将只保留标题中使用的字符。或者，如果书中嵌入了字体的粗体和斜体，但粗体和斜体文本相对较少，或者完全没有，那么粗体和斜体字体可以减少到只有几个字符，或者完全删除。 子集字体唯一的缺点是，如果你想在以后为你的书籍添加更 多的内容，新的内容可能无法使用原有的字体子集。

--upgrade-book, -U

如果可能, 升级本书的内部结构。 比如, 升级 EPUB 2 书籍到 EPUB 3 书籍。

--verbose

产生更详细的输出，有利于调试。

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- ebook-viewer

ebook-viewer

ebook-viewer [options] file

阅读一本电子书。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--continue

继续读上次打开的书

--detach

断开任何存在的控制终端，如果有的话（仅Linux）

--force-reload

强制重新载入所有已打开书籍

--full-screen, --fullscreen, -f

如果指定了此选项，则“电子书阅读器”窗口将在启动时尝试全屏显示。

--help, -h

显示此帮助信息并退出

--new-instance

即使设置了仅使用单个查看器窗口的选项，也可以打开新的查看器窗口

--open-at

打开指定书籍的位置。该位置是一个定位或者一个可以通过使用阅读器中“转到->位置”操作获得的位置。或者，您可以使用格式toc:something，它将在包含字符串“something”的第一个目录项的位置处打开。格式toc-href:something将匹配目录节点的href（内部链接目标）。匹配是精确的，如果要匹配子字符串，请使用toc-ref-contains:something格式。格式ref:something将使用引用模式引用。格式search:something打开书籍后会搜索指定的内容。格式regex:something 打开书籍后用正则表达式搜索。

--raise-window

如果指定了此选项，则“电子书阅读器”窗口将在启动时尝试移到最前面。

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [options]

从在线资源中获得书籍元数据。你必须至少指定书名、作者、ISBN 其中之一。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- lrf2lrs

lrf2lrs

lrf2lrs book.lrf

转换 LRF 文件为 LRS (XML UTF-8 编码的)文件

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--dont-output-resources

不将嵌入的图像和字体文件保存到硬盘

--help, -h

显示此帮助信息并退出

--output, -o

输出 LRS 文件

--verbose

更详细

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- lrfviewer

lrfviewer

lrfviewer [options] book.lrf

阅读 LRF 格式的电子书 book.lrf

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--disable-hyphenation

关闭连字符。应该会显著提升渲染速度。

--help, -h

显示此帮助信息并退出

--profile

配置LRF渲染器

--verbose

显示渲染过程的更多信息

--version

显示程序版本号并退出

--visual-debug

打开视觉辅助工具以调试渲染引擎

--white-background

默认背景是米白色，因为看起来更舒服一点。使用该选项将背景设为纯白。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- lrs2lrf

lrs2lrf

lrs2lrf [选项] file.lrs

编译 LRS 文件为 LRF 文件。

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--help, -h

显示此帮助信息并退出

--lrs

转换 LRS 到 LRS，对调试有用。

--output, -o

输出文件路经

--verbose

耗时的处理

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 命令行界面 »
- web2disk

web2disk

web2disk URL

URL的例子：https://google.com

每当您将包含空格的参数传递给%p程序时，请将参数放在引号中。例如: “/some path/with spaces”

[选项]

--base-dir, -d

保存URL的基础文件夹。默认 .

--delay

连续获取的最小间隔秒数。默认为 0 s

--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 s

--verbose

显示详细输出信息。有利于调试

--version

显示程序版本号并退出

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 设置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

- Debug技巧

- 使用print语句

- 使用交互式Python解释器

- 使用Python调试器作为远程调试器

- 在自己的IDE环境下使用调试器

- 在calibre的Python环境中执行任意脚本

- 在你的项目中使用calibre

- 二进制安装calibre

- Linux源码安装

- calibre丰富的API文档

设计理念
calibre源自Unix，它的设计是高度模块化的。模块间相互作用通过定义良好的接口实现。这使在calibre中添加新功能或者修改bug十分方便，这种便利性使calibre的开发很迅速。正因这种模块化设计思想，calibre对所有功能，都有一个完整的命令行接口，可以参考 `generated/zh_CN/cli-index`进行了解。

模块化设计的思想通过“插件”体现出来。关于“插件”的编写，这里有一份教程 tutorial。举个例子，如果要给calibre添加一个新的设备支持，一般只需要利用设备驱动插件范式写不到100行代码即可。你能从获取`built-in drivers <https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices>`_。类似地，为新的转换格式添加支持，也只需要改写输入/输出格式插件。模块化设计的另一个例子是抓取新闻，recipe system <news>。对于更多可加入calibre的插件，可以参考 Index of plugins [https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767]。

代码布局

所有calibre的Python代码都在``calibre``包中。它包含了以下的主要子包。

- 设备 – 所有的设备驱动器。通过浏览部分内置驱动器来获取他们的运作方式。

- 更多细节可以参考，:devices.interface。这其中定义了被驱动器支持的接口,。另外``devices.usbms``也值得关注，它定义了一个连接了usbms设备的通用驱动器。 在calibre中，所有的基于usbms的驱动器都继承自它.。

- 电子书 - 所有电子书转换/元数据代码。 一个好的起点是“calibre.ebooks.conversion.cli”，它是为 ebook-convert 命令提供动力的模块。 转换过程是通过“conversion.plumber”控制的。 与格式无关的代码全部在“ebooks.oeb”中，与格式相关的代码在“ebooks.format_name”中。

- 元数据的读取、写入和下载都在``ebooks.metadata``中

- 转换发生在传输途径中，传输途径的结构请参见“转换简介”。 该传输途径由输入插件、各种转换和输出插件组成。 构建和驱动传输途径的代码位于“plumber.py”中。 该传输途径适用于电子书的表示，就像解压的 epub，具有清单、书脊、目录、指南、html 内容等。管理此表示的类是“ebooks.oeb.base”中的 OEBBook 。 在转换过程中应用于本书的各种转换位于“oeb/transforms/.py”中。 输入和输出插件位于“conversion/plugins/.py”中。

- 电子书编辑使用不同的容器对象。详见 polish_api。

- db – 数据库后端。参考`db_api`了解calibre的接口。

- 内容服务器: “srv”是calibre的内容服务器。

- gui2 – 图形用户接口。GUI初始化位于``gui2.main``与``gui2.ui``。电子书浏览器位于``gui2.viewer``。电子书编辑器位于``gui2.tweak_book``。

如果要定位所有各种Calibre可执行文件的入口点，请在“linux.py <https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py>”中查看``entry_points``结构_。

如果你不理解某些代码，你可以在`development forum <https://www.mobileread.com/forums/forumdisplay.php?f=240>`_中提出，你很可能会得到其他开发者的帮助。

得到代码

你可以通过两种方式获取calibre的源代码：使用版本控制系统或者直接下载:website_base:源码<dist/src>
calibre使用`Git <https://www.git-scm.com/>`_，一个分布式版本控制系统。Git在所有calibre支持的平台上均可用。安装Git后，你就可以通过下面命令获得源码:

git clone https://github.com/kovidgoyal/calibre.git

在 Windows 上，您将需要完整的路径名，类似于文件“C:\Program Files\Git\git.exe”。

calibre是一个拥有很长时间版本管理历史的大项目，所以下载源码可能需要一点时间（10分钟到1小时，取决于你的网速）。

如果你想更快地获取源码，可以从:website_base:`归档 <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 `bug tracker上的票证上即可<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``时，小心不要包含合并。

如果您计划在 calibre 上进行大量开发，那么最好的方法是创建一个 GitHub [https://github.com] 帐户。 以下是设置您自己的 calibre 分支的基本指南，该指南允许您提交拉取请求以包含到主 calibre 存储库中：

- 按这篇文章在你的电脑上安装git:Setup Git [https://help.github.com/articles/set-up-git]

- 设置ssh密钥以进行GitHub身份验证，参考这里:Generating SSH keys [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

用你的GitHub用户名代替<username>。这将使本地校验你的分支。

- 无论何时你都能修改代码并提交。当你准备好待合并部分时，这样做:

git push

进入 https://github.com/<username>/calibre 点击 Pull Request 按钮，产生一个可被合并的pull请求。

- 你能从主仓库中更新你的本地代码副本，使用:

git pull upstream

您还应该关注 calibre 开发论坛 [https://www.mobileread.com/forums/forumdisplay.php?f=240]。 在进行重大更改之前，您应该在论坛中讨论它们或直接联系 Kovid（他的电子邮件地址遍布源代码）。

Windows开发环境

备注

你必须想上文所述的那样先得到calibre的源码。

正常安装Calibre，使用`Windows Installer<https://calibre-ebook.com/download_windows>`__。然后打开命令提示符并切换到先前检出的口径代码文件夹。例如：

cd C:\Users\kovid\work\calibre

Calibre是包含src和resource子文件夹的文件夹。

下一步是将环境变量``CALIBRE_DEVELOP_FROM``设置为src文件夹的绝对路径。因此，按照上面的示例，它应该是``C:Userskovidworkcalibresrc`。以下是<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!”。

你也可以通过Microsoft Visual Studio建立calibre开发环境。如果你希望这么做，可以参考这里的教程 here [https://www.mobileread.com/forums/showthread.php?t=251201] 。

macOS开发环境

备注

你必须想上文所述的那样先得到calibre的源码。

使用`提供的.dmg<https://calibre-ebook.com/download_osx>`__.正常安装Calibre。然后打开终端并切换到之前检出的口径代码文件夹，例如：

cd /Users/kovid/work/calibre

Calibre是包含src和resource子文件夹的文件夹。Calibre命令行工具位于Calibre应用程序包中，在：file:`/Applications/calibre.app/Contents/MacOS`中，如果您希望轻松运行命令行工具，则应将此文件夹添加到PATH环境变量中。

下一步是创建一个bash脚本，该脚本将在调试模式下运行Calibre时将环境变量``CALIBRE_DEVELOP_FROM``设置为src文件夹的绝对路径。

创建一个空文本文件:

#!/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启动时，你最好看终端上的诊断信息。你也应该注意到GUI界面的版本后有一个*号，这表示你从源码运行程序。

Linux开发环境

备注

你必须想上文所述的那样先得到calibre的源码。

calibre主要开发环境是Linux。你有两种方法配置开发环境。你可以正常安装calibre，在运行时环境下完成开发。这个方法与Windows与macOS开发类似。或者，从源码进行安装calibre。从源码配置开发环境的命令在源码树中的INSTALL文件。接下来我们将使用第一种方法，这也是推荐的方法。

使用`二进制安装程序<https://calibre-ebook.com/download_linux>`_.安装Calibre。然后打开终端，切换到之前检出的calibre代码文件夹，例如：

cd /home/kovid/work/calibre

Calibre是包含src和resource子文件夹的文件夹。

下一步是将环境变量``CALIBRE_DEPLE_FROM``设置为src文件夹的绝对路径。因此，按照上面的示例，它应该是``/home/kovid/work/calibre/src``。如何设置环境变量取决于您的Linux发行版以及您使用的是什么Shell。

备注

建议使用上游提供的二进制安装程序。如果您坚持使用发行版提供的包，请改用``CALIBRE_PYTHON_PATH``和``CALIBRE_RESOURCES_PATH``变量。

一旦你设置了环境变量，打开一个新终端，检查环境变量是否设置正确，使用命令:

echo $CALIBRE_DEVELOP_FROM

设置环境变量意味着calibre现在可以从指定位置载入它的所有Python代码。

就是这样！ 现在您已准备好开始破解 calibre 代码。 例如，在您喜欢的编辑器中打开文件 “src/calibre/__init__.py”并添加以下行：

print("Hello, world!")

靠近文件顶部。 现在运行命令“calibredb”。 输出的第一行应该是“Hello, world!”。

在一台电脑上同时安装正常模式与开发模式的calibre
calibre源码树非常稳定，很少崩溃，但如果你需要从源码运行一个测试书库并且你还要运行released版本的calibre作为日常使用。你能方便地通过批处理文件或者shell脚本启动calibre。下面的例子展示如何在Windows上通过批处理文件启动calibre(其他平台的命令也是一样的，只是用shell脚本代替批处理文件)。

用常用库启动发布版本的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"

Debug技巧

Python 是一门拥有动态类型的编程语言。它拥有着出色的内省功能。 Kovid 在没有使用调试器的情况下，完成了calibre的核心代码。这里给出一些debug的策略:

使用print语句

这是Kovid最喜欢的Debug方式。只需要在目标点插入print语句，然后在终端运行程序。举例，你能从终端打开GUI界面，像这样:

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-debug命令。一旦上述代码完成，calibre会停止，等待调试器的连接。

现在打开一个终端或者命令提示符，使用下列命令打开debug会话:

calibre-debug -c "from calibre.rpdb import cli; cli()"

您可以在`关于PDB模块的Python stdlib文档<https://docs.python.org/library/pdb.html#debugger-commands>中`了解如何使用Python调试器。

备注

默认情况下，远程调试器连接到端口4444。你也可以通过设置 set_trace() 与 cli() 函数更换端口，像这样: set_trace(port=1234) and cli(port=1234)。

备注

Python调试器不能控制多线程，所以你不得不在每个线程中使用 set_trace 函数一次，每次都得用一个不同的端口号。

在自己的IDE环境下使用调试器

如果内置调试器支持远程调试，则可以在您最喜欢的Python IDE中使用它。第一步是将Calibre src检出添加到IDE中的``PYTHONPATH``中。换句话说，您在上面设置为``CALIBRE_DEVELOP_FROM``的文件夹也必须位于您的IDE的``PYTHONPATH`中。

然后将IDE的远程调试器模块放入calibre源代码检测的“src”子文件夹中。 添加启动远程调试器所需的任何代码，以在感兴趣的位置进行校准，例如在主函数中。 然后正常运行 calibre。 您的 IDE 现在应该能够连接到在 calibre 内运行的远程调试器。

在calibre的Python环境中执行任意脚本
calibre-debug 提供了一些方便的开关来执行你自己的代码，并且可以访问calibre模块:

calibre-debug -c "some Python code"

非常适合在命令行上测试一小段代码。它的工作方式与-c切换至Python解释器的方式相同：

calibre-debug myscript.py

可以用来执行自己的Python脚本。 除了完全初始化calibre环境外，它的工作方式与将脚本传递给Python解释器的方式相同，因此您可以在脚本中使用所有calibre代码。 要在脚本中使用命令行参数，请使用以下格式：

calibre-debug myscript.py -- --option1 arg1

-- 会使后面的所有参数被传到你的脚本里。

在你的项目中使用calibre

在你的Python项目中可以直接使用calibre的功能与代码。有两种方法可以这么做:

二进制安装calibre

如果您有calibre的二进制安装，则可以使用calibre附带的Python解释器，如下所示：

calibre-debug /path/to/your/python/script.py -- arguments to your script

Linux源码安装

除了使用上面的技巧，如果你在Linux中进行源码安装，你可以直接import calibre，就像下面这样:

import init_calibre
import calibre

print(calibre.__version__)

在将init_calibre模块设置为运行calibre代码的解释器之前，必须先导入init_calibre模块，然后再输入其他calibre模块/软件包。

calibre丰富的API文档

- 清单的API文档
- 插件API文档
- API文档之数据库接口
- 电子书编辑工具的API文档

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 设置Calibre开发环境 »
- API文档之数据库接口

API文档之数据库接口

此API是线程安全的(它使用多个读取器、单个写入器锁定方案)。您可以通过如下方式访问该API接口：

from calibre.library import db
db = db('Path to calibre library folder').new_api

如果您使用的Calibre插件是主Calibre图形用户界面的一部分，则可以通过以下方式访问该插件：

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[源代码]

来自 calibre 库的metadata.db 文件的内存缓存。 该类还充当用于访问数据库的线程安全 API。 内存中的缓存以正常形式维护，以获得最佳性能。

SQLITE 只是用作从metadata.db 稳健地读取和写入的一种方式。 所有表读取/排序/搜索/缓存逻辑都被重新实现。 这是获得最大性能和灵活性所必需的。

class EventType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

book_created = 4

当数据库中创建新的图书记录时，图书 id 作为唯一参数

book_edited = 8

编辑书籍格式时, 使用参数: (book_id, fmt)

books_removed = 5

当书籍从数据库中删除时，书籍 ID 列表作为唯一参数

format_added = 2

当为书籍添加一种格式时, 使用参数: (book_id, fmt)

formats_removed = 3

当从书中删除格式时，带有参数：（将图书 ID 映射到从书中删除的格式集）

indexing_progress_changed = 9

当索引进度发生变化时

items_removed = 7

当标签或作者等项目从某些书籍中删除时。 参数：（字段名称、受影响的图书 ID、已删除项目的 ID）

items_renamed = 6

当某些或所有书籍中的标签或作者等项目被重命名时。 参数：（字段名称、受影响的图书 ID、旧项目 ID 到新项目 ID 的映射）

metadata_changed = 1

当某些书籍的某些元数据发生更改时，带有参数：（更改字段的名称，受影响的书籍 ID 集）

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[源代码]

将指定的书籍添加到图书馆。 Books 应该是一个可迭代的 2 元组，每个 2 元组的形式为“(mi, format_map)”，其中 mi 是元数据对象，format_map 是“{fmt: path_or_stream}”形式的字典，例如： {‘EPUB’: ‘/path/to/file.epub’}`。

返回一对列表：“ids，duplicates”。 ids 包含数据库中所有新创建的图书的图书 ID。 根据“has_book”使用的简单重复检测启发式，“duplicates”包含数据库中已存在的所有书籍的“(mi, format_map)”。

add_custom_book_data(name, val_map, delete_first=False)[源代码]

添加 name 数据，其中 val_map 是 book_ids 到值的映射。 如果delete_first为True，则所有先前存储的name数据将被删除。

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 – 如果为 True 则替换现有格式，否则如果该格式已存在，则返回 False。

- run_hooks – 如果为 True，则文件类型插件将在添加之前和之后的格式上运行。

- 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_book_ids(type=<class 'frozenset'>)[源代码]

所有已知图书 ID 的冻结集。

all_field_for(field, book_ids, default_value=None)[源代码]

与 field_for 相同，只不过它同时对多本书进行操作

all_field_ids(name)[源代码]

字段“name”中所有值的冻结 ID 集。

all_field_names(field)[源代码]

所有字段名称的冻结集（仅应用于多一和多对多字段）

author_data(author_ids=None)[源代码]

将作者数据作为带有键的字典返回：名称、排序、链接

如果没有找到具有指定 ID 的作者，则返回空字典。 如果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”。

返回值是一组图书 ID，如果该项目或字段不存在，则返回空集。

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”。 如果不存在封面或 dest 与当前封面是同一文件，则返回 False。 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)[源代码]

返回封面图像或无。 默认情况下，以字节字符串形式返回封面。

警告：使用 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_custom_book_data(name, book_ids=())[源代码]

删除名称数据。 默认情况下删除所有数据，如果只想删除某些图书 ID 的数据，请传入图书 ID 列表。

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)[源代码]

返回由“book_id”标识的书籍的字段“name”的值。 如果不存在这样的书，或者没有为字段“name”定义值，或者不存在这样的字段，则返回“default_value”。

“default_value” 不用于标题、title_sort、作者、author_sort 和 series_index。 这是因为这些在数据库中总是有值。 “default_value” 用于所有自定义列。

is_multiple 字段的返回值始终是元组，即使未找到任何值（换句话说，default_value 被忽略）。 例外情况是返回值始终是字典的标识符。 返回的元组始终按链接顺序排列，即它们的创建顺序。

field_ids_for(name, book_id)[源代码]

返回由“book_id”标识的书籍上“name”字段的值的 ids（作为元组）。 如果没有值，或者没有这样的书，或者没有这样的字段，则返回一个空元组。

field_supports_notes(field=None) → bool[源代码]

当且仅当指定字段支持注释时返回 True。 如果字段为“无”，则返回支持注释的所有字段的冻结集。

find_identical_books(mi, search_restriction='', book_ids=None)[源代码]

查找mi中具有作者超集且标题相同的图书(标题是模糊匹配的)。另请参阅`data_for_find_aligant_books`。

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[源代码]

以字节串形式返回电子书格式，如果格式不存在，或者我们没有写入电子书文件的权限，则返回“None”。

参数:

- as_file – 如果为 True，则电子书格式作为文件对象返回。 请注意，文件对象是 SpooledTemporaryFile，因此如果您想要将格式复制到另一个文件，请使用“copy_format_to”代替以提高性能。

- as_path – 将格式文件复制到临时文件并返回临时文件的路径

- preserve_filename – 如果为 True 并返回路径，则文件名与库中使用的文件名相同。 请注意，使用此方法意味着重复调用会产生相同的临时文件（每次都会重新创建）

format_abspath(book_id, fmt)[源代码]

返回格式为“format”的电子书文件的绝对路径。 您几乎不应该使用它，因为它破坏了此 API 的线程安全承诺。 而是使用“copy_format_to”。

目前仅在 calibredb 列表、查看器、编辑书籍、将格式与原始格式进行比较、打开方式、批量元数据编辑和目录（通过 get_data_as_dict()）中使用。

除了查看器、打开方式和编辑书籍之外，我不相信其他任何人会使用此调用的结果执行任何文件写入 I/O。

format_hash(book_id, fmt)[源代码]

返回指定书籍的指定格式的哈希值。 哈希类型取决于后端，但通常是 SHA-256。

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[源代码]

返回指定书籍的指定格式的路径、大小和运行时间。 除非绝对必要，否则不应使用路径，因为直接访问它会破坏此 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]][源代码]

如果 field_name 为 None，则返回在指定字段或所有字段中具有注释的项目的所有 item_id

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 – 这本书id有问题。

返回:

{字段： {field_value， link_value}， … 对于field_value具有该书的非空链接值的所有字段

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[源代码]

在内部使用以实现标签浏览器

get_custom_book_data(name, book_ids=(), default=None)[源代码]

获取名称数据。默认情况下，会返回所有book_ids的数据，如果您只需要一些数据，请传入书籍 ID 列表。返回book_id到值的映射。如果无法解码特定值，则对其使用 default。

get_id_map(field)[源代码]

返回id号到指定字段值的映射。字段必须是多-一或多-多字段，否则会引发ValueError。

get_ids_for_custom_book_data(name)[源代码]

返回名称包含数据的书籍 ID 集。

get_item_id(field, item_name)[源代码]

返回item_name的项目ID(不区分大小写)，如果未找到，则返回None

get_item_ids(field, item_names)[源代码]

返回item_name的项目 ID（不区分大小写）

get_item_name(field, item_id)[源代码]

返回指定字段中由 item_id 指定的项目的项目名称。 另请参阅“get_id_map”。

get_item_name_map(field, normalize_func=None)[源代码]

返回条目值到 id 的映射

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 – 如果为 True，则返回 book_id 到当前 series_index 值的映射。

get_notes_resource(resource_hash) → dict | None[源代码]

返回包含资源数据和名称的字典，如果未找到具有指定哈希的资源，则返回 None

get_proxy_metadata(book_id)[源代码]

与“get_metadata”类似，不同之处在于它返回一个仅按需从数据库读取值的 ProxyMetadata 对象。 当只需要从返回的元数据对象访问少量字段时，这比 get_metadata 快得多。

get_usage_count_by_id(field)[源代码]

返回指定字段的所有值的 id 到使用计数的映射，该字段必须是多一或多对字段。

has_book(mi)[源代码]

当且仅当数据库包含与传入的 Metadata 对象具有相同标题的条目时返回 True。 比较不区分大小写。 另请参阅“有书的数据”。

has_format(book_id, fmt)[源代码]

当且仅当磁盘上存在该格式时返回 True

has_id(book_id)[源代码]

如果指定的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)[源代码]

返回指定项目的链接，如果有；如果未找到链接，则返回“无”

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][源代码]

获取有关书籍目录中额外文件的信息。

参数:

- book_id – 书籍的数据库书籍 ID

- pattern – 要搜索的文件名模式。 空模式匹配所有额外文件。 模式必须使用 / 作为分隔符。 使用 DATA_FILE_PATTERN 常量来匹配数据目录内的文件。

返回:

与指定模式匹配的所有额外文件的元组。 元组的每个元素都是 ExtraFile(relpath, file_path, stat_result)。 其中 relpath 是文件到图书目录的相对路径，使用 / 作为分隔符。 stat_result 是对文件调用 os.stat() 的结果。

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)[源代码]

返回已排序图书 ID 的列表。 如果 ids_to_sort 为 None，则返回所有图书 ID。

fields 必须是形式为 (field_name, ascending=True 或 False) 的二元组列表。 最重要的字段是第一个 2 元组。

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)[源代码]

返回指定首选项的值，如果未设置首选项，则返回指定为“default”的值。

read_backup(book_id)[源代码]

以字节串形式返回书籍的 OPF 元数据备份，如果不存在此类备份，则返回 None。

remove_books(book_ids, permanent=False)[源代码]

从数据库中删除 book_ids 指定的书籍并删除其格式文件。 如果“permanent”为 False，则格式文件将放置在每个库的垃圾目录中。

remove_formats(formats_map, db_only=False)[源代码]

从指定书籍中删除指定格式。

参数:

- formats_map – book_id 到要从书中删除的格式列表的映射。

- db_only – 如果为 True，则仅从数据库中删除该格式的记录，而不从文件系统中删除实际的格式文件。

返回:

书籍 ID 到实际从该书的文件系统中删除的格式集的映射

remove_items(field, item_ids, restrict_to_book_ids=None)[源代码]

删除指定字段中具有指定 ID 的所有项目。 返回受影响的图书 ID 集。 restrict_to_book_ids 是一组可选的书籍 ID。 如果指定，这些项目只会从这些书中删除。

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 – 要执行重命名的一组可选书籍 ID，默认为所有书籍。

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 是必要的，在存在复合列的情况下更新搜索缓存时可能会发生 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)[源代码]

在数据库中搜索指定的查询，返回一组匹配的图书 ID。

参数:

- restriction – 与指定查询进行 AND 运算的限制。 请注意，限制已被缓存，因此搜索 a AND b 将比搜索带有限制 b 的 a 慢。

- virtual_fields – 内部使用（虚拟字段，例如用于搜索的 on_device）。

- book_ids – 如果不是“无”，则将搜索一组图书 ID，而不是搜索所有图书。

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_conversion_options(options, fmt='PIPE')[源代码]

options 必须是 {book_id:conversion_options} 形式的映射

set_cover(book_id_data_map)[源代码]

设置这本书的封面。 数据可以是 QImage、QPixmap、文件对象或字节串。 它也可以是“无”，在这种情况下，任何现有的覆盖物都会被删除。

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[源代码]

设置“name”指定的字段的值。 返回受更改影响的所有图书 ID 的集合。

参数:

- book_id_to_val_map – 将 book_ids 映射到应应用的值。

- allow_case_change – 如果为 True，多对一或多对多字段的大小写将会改变。 例如，如果一本书有标签“tag1”，而您将另一本书的标签设置为“Tag1”，那么如果allow_case_change为True，这两本书都将有标签“Tag1”，否则它们都会有标签“Tag1”。 有标签``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 – 字典（字段值：链接，…）。 请注意，这些是值，而不是字段 ID。

返回:

通过设置链接更改书籍

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[源代码]

从“Metadata”对象“mi”设置书籍“id”的元数据

设置force_changes=True将强制set_metadata更新字段，即使mi包含空值。 在这种情况下，“无”与“空”不同。 如果 mi.XXX 为 None，则不替换 XXX，否则替换。 标签、标识符和封面属性是特殊情况。 标签和标识符不能设置为 None，因此如果 force_changes 为 true，它们将始终被替换。 您必须确保 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)[源代码]

将指定首选项设置为指定值。 另请参阅“pref”。

split_if_is_multiple_composite(f, val)[源代码]

如果 f 是复合列查找键并且列是 is_multiple，则将 v 拆分为唯一的非空值。 比较区分大小写。 订单不保留。 返回一个 list() 以与代理元数据字段 getter 兼容，例如标签。

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[源代码]

返回所有具有早于指定时间的标签“tag”的书籍的 ID。 标签比较不区分大小写。

参数:

- delta – timedelta 对象或 None。 如果没有，则返回带有该标签的所有 id。

- must_have_tag – 如果不是“无”，则匹配列表将仅限于具有此标签的书籍

- must_have_authors – 作者名单。 如果不是“无”，则匹配列表将仅限于具有这些作者的书籍（不区分大小写）。

unretire_note_for(field, item_id) → int[源代码]

取消指定项目之前已停用的注释。 当从数据库中删除项目时，注释将被停用

user_categories_for_books(book_ids, proxy_metadata_map=None)[源代码]

返回指定书籍的用户类别。 proxy_metadata_map 是可选的，在书籍的 ProxyMetadata 对象已存在的情况下，对于提高性能很有用。 它应该是 book_ids 到其对应的 ProxyMetadata 对象的映射。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 设置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)

如果您正在为电子书编辑器编写插件，您将获得正在编辑的图书的当前容器，如下所示：

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, opfpath, log, clone_data=None)[源代码]

容器将一本打开的电子书表示为一个充满文件和 OPF 文件的文件夹。 有两个重要的概念：

- 根文件夹。 这是电子书的基础。 所有电子书文件都位于该文件夹或其子文件夹中。

- 名称：这些是书籍文件相对于根文件夹的路径。 它们始终包含 POSIX 分隔符并且不带引号。 它们可以被认为是书中文件的规范标识符。 容器对象上的大多数方法都使用名称。 名称始终采用 NFC Unicode 范式。

- 克隆：容器对象支持高效的磁盘克隆，用于在电子书编辑器中实现检查点。 为了实现此目的，您永远不应该直接访问文件系统上的文件。 相反，使用“raw_data”或“open”来读取/写入书中的组件文件。

当使用此类提供的方法在href和名称之间进行转换时，它们假定所有href都被引用。

abspath_to_name(fullpath, root=None)[源代码]

将绝对路径转换为相对于“root”的规范名称

参数:

root – 基础文件夹。 默认情况下使用此容器对象的根。

add_file(name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None)[源代码]

将文件添加到此容器。文件条目自动在OPF清单和书脊中创建（如果文件是文本文档）

add_name_to_manifest(name, process_manifest_item=None)[源代码]

向具有指定名称的文件的清单中添加条目。返回清单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 – 要将保存的电子书文件写入的路径。如果没有，则使用原始书籍文件的路径。

- keep_parsed – 如果为True，则已提交项的解析表示形式保留在缓存中。

commit_item(name, keep_parsed=False)[源代码]

将解析后的对象提交到磁盘（它被序列化并写入底层文件）。 如果“keep_parsed”为 True，解析后的表示将保留在缓存中。 另请参阅：“已解析”

dirty(name)[源代码]

将name对应的已解析对象标记为dirty。 另请参阅：“已解析”。

exists(name)[源代码]

如果存在与规范名称相对应的文件/文件夹，则为 True。 请注意，此函数受到底层操作系统文件系统的限制，特别是大小写（不）敏感性。 因此，在不区分大小写的文件系统上，即使名称的大小写与底层文件系统文件的大小写不同，也会返回 True。 另请参阅“has_name”

filesize(name)[源代码]

返回由指定规范名称表示的文件的大小，以字节为单位。 自动处理混乱的解析对象。 另请参阅：“已解析”

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

指南类型到规范名称的映射

has_name(name)[源代码]

当且仅当存在与指定名称具有相同规范名称的文件时返回 True。 与“exists”不同，此方法始终区分大小写。

href_to_name(href, base=None)[源代码]

将 href（相对于 base）转换为名称。 base 必须是名称或 None，在这种情况下使用 self.root。

insert_into_xml(parent, item, index=None)[源代码]

将项目插入父级(如果索引为无，则追加)，修复缩进。仅适用于自动关闭的项目。

is_dir = False

如果此容器表示解压缩的图书(目录)

iterlinks(name, get_line_numbers=True)[源代码]

遍历名称中的所有链接。 如果 get_line_numbers 为 True，则产生 (link, line_number, offset) 形式的结果。 其中 line_number 是发生链接的 line_number，offset 是距行开头的字符数。 请注意，如果偏移量不为零，则实际上可以包含多条线。

make_name_unique(name)[源代码]

确保本书中不存在“name”。 如果存在，则返回不存在的修改版本。

manifest_has_name(name)[源代码]

如果清单具有与Name对应的条目，则返回True

property manifest_id_map

清单ID到规范名称的映射

manifest_items_of_type(predicate)[源代码]

媒体类型与谓词匹配的所有清单项的名称。 predicate 可以是一个集合、一个列表、一个字符串或一个带有单个参数的函数，它将使用媒体类型进行调用。

manifest_items_with_property(property_name)[源代码]

具有指定属性的所有清单项

property manifest_type_map

将清单媒体类型映射到该媒体类型的规范名称列表

property mi

本书的元数据作为 Metadata 对象。 请注意，每次请求此属性时都会动态构造此对象，因此请谨慎使用它。

name_to_abspath(name)[源代码]

将规范名称转换为操作系统相关的绝对路径

name_to_href(name, base=None)[源代码]

将名称转换为相对于基准的 href，该基准必须是名称或 None，在这种情况下 self.root 用作基准

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

OPF <package> 元素上设置的版本

property opf_version_parsed

OPF 的 <package> 元素上设置的版本作为整数元组

opf_xpath(expr)[源代码]

计算 OPF 文件上的 XPath 表达式的便捷方法，具有预定义的 opf: 和 dc: 命名空间前缀。

parsed(name)[源代码]

返回按名称指定的文件的已解析表示形式。 对于 HTML 和 XML 文件，将返回 lxml 树。 对于 CSS 文件，会返回 css_parser 样式表。 请注意，已解析的对象会被缓存以提高性能。 如果对解析的对象进行任何更改，则必须调用“dirty”，以便容器知道更新缓存。 另请参阅“替换”。

raw_data(name, decode=True, normalize_to_nfc=True)[源代码]

返回name指定文件对应的原始数据

参数:

- decode – 如果为 True 并且文件具有基于文本的 MIME 类型，则对其进行解码并返回 unicode 对象而不是原始字节。

- normalize_to_nfc – 如果为 True，则返回的 unicode 对象将按照 EPUB 和 AZW3 文件格式的要求标准化为 NFC 范式。

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)[源代码]

将 name 对应的已解析对象替换为 obj，它必须是相似的对象，即 HTML/XML 的 lxml 树或 CSS 文件的 css_parser 样式表。

replace_links(name, replace_func)[源代码]

使用replace_func替换名称中的所有链接，它必须是接受URL并返回替换的URL的可调用函数。 如果完成任何实际替换，它还必须具有设置为 True 的“replaced”属性。 创建此类可调用对象的便捷方法是使用“LinkReplacer”和“LinkRebaser”类。

serialize_item(name)[源代码]

将已解析的对象（由规范名称标识）转换为字节串。 请参阅“已解析”。

set_spine(spine_items)[源代码]

将spine 设置为spine_items，其中spine_items 是形式为（名称、线性）的可迭代对象。 如果清单中不存在其中一个名称，则会引发错误。

property spine_items

一个迭代器，生成书脊中每个项目的路径。 另请参阅：“spine_iter”和“spine_items”。

property spine_iter

一个迭代器，为书脊中的每个项目生成项目，名称为 is_linear。 item 是 lxml 元素，name 是规范文件名，如果 item 是线性的，则 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 – 一个可调用函数，它接受两个参数``(name,anchor)``并返回一个新的锚点。 如果您需要更改 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 指定的位置分割 name 指定的文件。 拆分会自动迁移受影响文件的所有链接和引用。

参数:

- 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 表达式匹配的所有标记）。 另请参阅：“拆分”。 拆分会自动迁移受影响文件的所有链接和引用。

参数:

before – 如果为 True，则分割发生在所识别的元素之前，否则发生在其之后。

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[源代码]

将指定文件合并为单个文件，自动迁移受影响文件的所有链接和引用。 该文件必须全部是 HTML 或 CSS 文件。

参数:

- category – 对于 HTML 文件必须是 'text' 对于 CSS 文件必须是 'styles'

- names – 要合并的文件列表

- master – 合并后的文件中哪个是*主*文件，即合并后保留的文件。

管理封面

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[源代码]

将书籍的封面设置为 cover_path 指向的图像。

参数:

- cover_path – 图像文件的绝对路径或书中图像的规范名称。 使用书中的图像时，您还必须设置选项，请参见下文。

- report – 带有单个参数的可选可调用对象。 它将被调用并提供有关正在处理的任务的信息。

- options – 无或控制封面设置方式的字典。 字典可以包含条目： keep_aspect：True 或 False（保留 EPUB 中封面的宽高比） no_svg：True 或 False（在 EPUB 标题页中使用 SVG 封面包装） 现有： 对或错（“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 表达式列表生成目录。 列表中的每个表达式对应于生成目录的一个级别。 例如： [‘//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)[源代码]

从现有 NCX 目录创建内联 (HTML) 目录。

参数:

title – 此目录的标题。

书籍编辑工具
class calibre.gui2.tweak_book.plugin.Tool[源代码]

基类：object

编辑图书插件中各个工具的基类。 有用的成员包括：

- “self.plugin”：对该工具所属的“calibre.customize.Plugin”对象的引用。

- self. boss

- self. gui

必须在子类中重写的方法：

- 创建动作

- 注册快捷方式

name = None

将其设置为一个唯一的名称，它将用作密钥

allowed_in_toolbar = True

如果为 True，用户可以选择将此工具放置在插件工具栏中

allowed_in_menu = True

如果为 True，则用户可以选择将此工具放置在插件菜单中

toolbar_button_popup_mode = 'delayed'

工具栏按钮的菜单（如果有）的弹出模式。 可能的值为“延迟”、“即时”、“按钮”

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)[源代码]

打开“path”处的电子书进行编辑。 如果电子书的格式不受支持或当前书籍有未保存的更改，则会显示错误。

参数:

edit_file – 新打开的书籍中要开始编辑的文件的名称。 也可以是姓名列表。

rewind_savepoint()[源代码]

撤消之前创建的恢复检查点，如果您创建检查点，然后中止操作而不进行任何更改，则非常有用

save_book()[源代码]

保存这本书。 保存在后台执行

set_modified()[源代码]

将书标记为已修改

show_current_diff(allow_revert=True, to_container=None)[源代码]

显示书籍相对于上次检查点状态的更改

参数:

- allow_revert – 如果为 True，diff 对话框将有一个按钮允许用户恢复所有更改

- to_container – 与当前容器进行比较的容器对象。 如果没有，则使用先前设置检查点的容器

show_editor(name)[源代码]

显示正在编辑“name”指定文件的编辑器

sync_preview_to_editor()[源代码]

将预览面板的位置同步到当前编辑器中的当前光标位置

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 下一页 |
- 上一页 |
- 开始 »
- 数字版权管理(DRM)

数字版权管理(DRM)

数字版权管理 (DRM) 是访问控制技术的通用术语，硬件制造商、出版商、版权所有者和个人可以使用该技术来尝试对数字内容和设备的使用施加限制。 有时，它也被轻蔑地描述为数字限制管理。 该术语用于描述任何禁止使用内容提供商不希望或预见的数字内容（合法或其他方式）的技术。 该术语通常不指无需修改文件或设备即可规避的其他形式的复制保护，例如序列号或密钥文件。 它还可以指与数字作品或设备的特定实例相关的限制。 DRM 技术试图通过阻止最终用户访问、复制或转换为其他格式来控制数字媒体的使用。参见`维基百科 <https://en.wikipedia.org/wiki/Digital_rights_management>`_。

DRM对我个人意味着什么？

当您购买带有 DRM 的电子书时，您并不真正拥有它，而是购买了按照卖家指定的方式使用它的许可。 DRM 限制了您可以对“购买”的电子书进行的操作。 购买带有 DRM 的书籍的人通常不知道这些限制的程度。 这些限制阻止您根据自己的喜好重新格式化电子书，包括进行风格更改，例如调整字体大小，尽管有软件可以让您对非 DRM 图书执行此类操作。 人们常常感到惊讶的是，他们购买的特定格式的电子书如果具有 DRM，则无法转换为另一种格式。 因此，如果您拥有亚马逊 Kindle 并购买了 Barnes and Nobles 销售的书籍，您应该知道，如果该电子书具有 DRM，您将无法在 Kindle 上阅读。 请注意，我说的是您购买的一本书，不是偷窃或盗版，而是购买。

DRM为作者做了什么？

DRMed电子书的出版商认为，DRM完全是为了作者的利益，并保护其艺术完整性和防止盗版。 但是DRM不能防止盗版。 想要盗版内容或使用盗版内容的人仍然可以成功。 当今，电子书的三种主要DRM方案由Amazon，Adobe和Barnes and 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 电子书的独立作者的书籍，并且您负担得起，请向他们捐款。 这是一笔值得花的钱，因为他们的电子书往往比您从 DRMed 图书出版商那里购买的电子书更便宜（可能有例外），并且可能会在您将来拥有的所有设备上运行，从而节省您购买电子书的成本。 又是电子书。 不要通过盗版其内容来阻止无 DRM 电子书的出版商和作者。 内容提供商应该因其努力而得到补偿。 不要因为他们试图通过提供无 DRM 的电子书来改善您的阅读体验而惩罚他们。 从长远来看，这对你是不利的。 如果您从同时带有 DRM 和无 DRM 书籍的卖家处购买了书籍，并且不知道他们是否带有 DRM，请务必在网站上发表评论或评论，告知未来的买家其 DRM 状态。 许多卖家认为向买家明确表明电子书是否带有 DRM 并不重要。 `在这里<https://www.defectivebydesign.org/guide/ebooks>`_您将找到无 DRM 生活的指南。

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 上一页 |
- 开始 »
- 词汇表

词汇表

RSS订阅

**RSS*(真正简单的聚合)*是一种Web源格式，用于发布频繁更新的内容，如新闻文章、博客帖子等。它是一种特别适合计算机阅读的格式，因此是将内容从Web获取到电子书的首选方式。互联网上还有许多其他的提要格式在使用，Calibre可以理解其中的大多数格式。特别是，它对博客常用的*ATOM*格式有很好的支持。

订阅清单

新闻源是一套指导Calibre如何将在线新闻来源，如杂志或博客，转换为电子书的说明。订阅新闻本质上是`Python<https://www.python.org>`_代码。因此，它能够将任意复杂的新闻来源转换为电子书。在最简单的层面上，它只是一组变量，如URL，提供足够的信息到Internet上并下载新闻。

HTML

**HTML*(超文本标记语言)*是用于电子出版的标准通用标记语言(SGML)的子集，是用于万维网的特定标准。

CSS

**CSS*(层叠样式表)*是一种语言，用于描述：术语`HTML`文档应该如何被渲染（视觉样式)。

API

API (应用编程接口) 是程序库提供的源代码接口，以支持对计算机程序对其进行服务的请求。

LRF

LRF 索尼电子书阅读器阅读的电子书格式。

URL

URL (统一资源定位器) 例如: http://example.com

正则表达式

正则表达式提供了一种简洁而灵活的方式来标识感兴趣的文本字符串，例如特定字符，单词或字符模式. 参阅正则表达式`教程 <regexp>` .

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 概览：模块代码

代码可用的所有模块

- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 prefname = 'plugin config dialog:'+self.type + ':' + self.name config_dialog = QDialog(parent) 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) try: config_widget = self.config_widget() except NotImplementedError: config_widget = None 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('The %s plugin has no command line interface' %self.name)

# }}}

[文档] 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 None

# }}}

[文档] 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 '~%s' % 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(list(requested_fields - all_fields)) print("invalid --fields specified: %s" % ', '.join(invalid_fields)) print("available fields in '%s': %s" % (current_library_name(), ', '.join(sorted(list(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: 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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('OpRec: %s: Recommended value not in choices'% self.option.name) if not (isinstance(self.recommended_value, (numbers.Number, bytes, str)) or self.recommended_value is None): raise ValueError('OpRec: %s:'%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 accelarators: 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('InputFormatPlugin: %s running'%self.name) if hasattr(stream, 'name'): log('on', stream.name) except: # 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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, Optional, Tuple 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 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 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, 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: 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 self.library_database_instance = (None 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()

@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 dont 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) 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)

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_ok 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_ok(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. ''' return self.backend.set_notes_for(field, item_id, doc, searchable_text, resource_hashes, remove_unused_resources)

[文档] @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) -> Optional[dict]: ' 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 ' return self.backend.unretire_note_for(field, item_id)

[文档] @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)) return self.backend.import_note(field, item_id, html, basedir, ctime, mtime)

[文档] @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('%s is not a many-one or many-many field' % 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('%s is not a many-one or many-many field' % 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('%s is not a many-one or many-many field' % 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): ' Return the item id for item_name (case-insensitive) or None if not found ' q = icu_lower(item_name) try: for item_id, item_val in self.fields[field].table.id_map.items(): if icu_lower(item_val) == q: return item_id except KeyError: return None

[文档] @read_api def get_item_ids(self, field, item_names): ' Return the item id for item_name (case-insensitive) ' rmap = {icu_lower(v) if isinstance(v, str) else v:k for k, v in iteritems(self.fields[field].table.id_map)} return {name:rmap.get(icu_lower(name) if isinstance(name, str) else name, None) for name in 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: raise NoSuchFormat('Record %d has no fmt: %s'%(book_id, 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: 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('Record %d has no %s file'%(book_id, fmt))

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: 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: 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: 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: return ()

def verify(fmt): try: name = self.fields['formats'].format_fname(book_id, fmt) except: 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: return None fname += ext bd = base_dir() d = os.path.join(bd, 'format_abspath') try: os.makedirs(d) except: 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: 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): ' 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) 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: # 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: 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: 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: 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: 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: 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: # 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: continue for fmt in fmts: try: name = self.fields['formats'].format_fname(book_id, fmt) except: 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 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 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('Cannot rename items for one-one fields: %s' % field) moved_books = set() id_map = {} for item_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,) 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('add_custom_book_data: no such book_ids: %d'%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) 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 return {vm.get(fid):v for fid,v in lm.items() if v}

[文档] @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) 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:"=%s"'%(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: 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: 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: 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'): 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 tuple(self.backend.all_annotations_for_book(book_id))

@read_api def annotation_count_for_book(self, book_id): return self.backend.annotation_count_for_book(book_id)

@read_api def all_annotation_users(self): return tuple(self.backend.all_annotation_users())

@read_api def all_annotation_types(self): 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 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 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): self.backend.delete_annotations(annot_ids)

@write_api def update_annotations(self, annot_id_map): 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'): 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'): 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 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: 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

[文档] @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): 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) 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: dest_cover = src_cover replace_cover = False 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 = self.field_for('identifier', src_id) or {} 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})

for key in self.field_metadata: # loop thru 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})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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- calibre.devices.interface

calibre.devices.interface 源代码

__license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' import os from collections import namedtuple from calibre import prints from calibre.constants import iswindows from calibre.customize import Plugin 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 None

# 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 None

[文档] 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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 DevicePlugin 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

[文档] 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('Ignoring the drive %s as it is readonly' % dl) 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('Ignoring the drive %s because failed to get free space for it' % dl) 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] 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: 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, False) 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: 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: 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('Unable to mount card (Error code: %d)'%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: pass else: try: os.remove(path) except: 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 in /dev with matching s/n etc. # 2. get list of volumes associated with each # 3. attempt to mount each one using Hal # 4. when finished, we have a list of mount points and associated dbus nodes # def open_freebsd(self): # 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") from .hal import get_hal hal = get_hal() vols = hal.get_volumes(d) if verbose: print("FBSD: ", vols)

ok, mv = hal.mount_volumes(vols) if not ok: raise DeviceError(_('Unable to mount the device')) for k, v in mv.items(): setattr(self, k, v)

# # ------------------------------------------------------ # # this one is pretty simple: # just umount each of the previously # mounted filesystems, using the stored volume object # def eject_freebsd(self): from .hal import get_hal hal = get_hal() if self._main_prefix: hal.unmount(self._main_vol) if self._card_a_prefix: hal.unmount(self._card_a_vol) if self._card_b_prefix: hal.unmount(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 open(self, connected_device, library_uuid): time.sleep(5) self._main_prefix = self._card_a_prefix = self._card_b_prefix = None 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 try: self.open_freebsd() except DeviceError: time.sleep(2) 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 = None
def 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: 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: pass for d in drives: try: eject(d) except Exception as e: print('Udisks eject call for:', d, 'failed:') print('\t', e)

[文档] def eject(self): if islinux: try: self.eject_linux() except: pass if isfreebsd: try: self.eject_freebsd() except: pass if iswindows: try: self.eject_windows() except: pass if ismacos: try: self.eject_osx() except: pass self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
def linux_post_yank(self): self._linux_mount_map = {}

[文档] def post_yank_cleanup(self): if islinux: try: self.linux_post_yank() except: import traceback traceback.print_exc() self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 import time from itertools import cycle from calibre import fsync, isbytestring, prints from calibre.constants import filesystem_encoding, is_debugging, 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 polyglot.builtins import itervalues, string_or_bytes def debug_print(*args, **kw): base_time = getattr(debug_print, 'base_time', None) if base_time is None: debug_print.base_time = base_time = time.monotonic() if is_debugging(): prints('DEBUG: %6.1f'%(time.monotonic()-base_time), *args, **kw)

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('Skipping undecodeable file: %r' % name) 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: 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: # 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('USBMS: count found in cache: %d, count of files in metadata: %d, need_sync: %s' % (len(bl_cache), len(bl), 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('USBMS: uploading %d books'%(len(files)))

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: # 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('USBMS: finished uploading %d books'%(len(files))) 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('USBMS: adding metadata for %d books'%(len(metadata)))

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('/') or 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: pass

[文档] def delete_books(self, paths, end_session=True): debug_print('USBMS: deleting %d books'%(len(paths))) 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('USBMS: finished deleting %d books'%(len(paths)))

[文档] def remove_books_from_metadata(self, paths, booklists): debug_print('USBMS: removing metadata for %d books'%(len(paths)))

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('USBMS: finished removing metadata for %d books'%(len(paths)))

# 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: 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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: 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: 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: 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 object.__getattribute__(self, '_data')

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'))) 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: 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( 'Custom field name %s must begin with \'#\''%repr(field)) 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: if DEBUG: traceback.print_exc()

# Old Metadata API {{{ def print_all_attributes(self): for x in STANDARD_METADATA_FIELDS: prints('%s:'%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 not in (None, 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 + \ ' [%s]'%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 = '%.2g'%(res/2) elif datatype in ['int', 'float']: try: fmt = cmeta['display'].get('number_format', None) res = fmt.format(res) except: 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 + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': res = '%.2g'%(res/2) elif key == 'size': res = human_readable(res) return (name, str(res), orig_res, fmeta)

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('%-20s: %s'%(str(x), str(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 + ' #%s'%self.format_series_index()) if not self.is_null('languages'): fmt('Languages', ', '.join(self.languages)) if self.rating is not None: fmt('Rating', ('%.2g'%(float(self.rating)/2)) 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(['%s:%s'%(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) + ' #%s'%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>%s</b></td><td>%s</td></tr>'%x return '<table>%s</table>'%'\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('Unknown value for %s: %s'%(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

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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__ = ['type', 'default', 'label', 'desc', 'name', 'choices']

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 = dict([(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 None

[文档] 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 None

[文档] 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 None

[文档] 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 None

[文档] 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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 parse as parse_html_tweak from calibre.ebooks.oeb.polish.utils import OEB_FONTS, CommentFinder, PositionFinder, adjust_mime_for_epub, guess_type, parse_css from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile from calibre.utils.filenames import hardlink_file, 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: shutil.copy2(spath, dpath)

def clone_container(container, dest_dir): ' Efficiently clone a container using hard links ' dest_dir = os.path.abspath(os.path.realpath(dest_dir)) clone_data = container.clone_data(dest_dir) cls = type(container) if cls is Container: return cls(None, None, container.log, clone_data=clone_data) return cls(None, 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 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): self.log = 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. """ def fix_data(d): return d.replace('\r\n', '\n').replace('\r', '\n') if isinstance(data, str): return fix_data(data) bom_enc = None if data[:4] in {b'\0\0\xfe\xff', b'\xff\xfe\0\0'}: bom_enc = {b'\0\0\xfe\xff':'utf-32-be', b'\xff\xfe\0\0':'utf-32-le'}[data[:4]] data = data[4:] elif data[:2] in {b'\xff\xfe', b'\xfe\xff'}: bom_enc = {b'\xff\xfe':'utf-16-le', b'\xfe\xff':'utf-16-be'}[data[:2]] data = data[2:] elif data[:3] == b'\xef\xbb\xbf': bom_enc = 'utf-8' data = data[3:] if bom_enc is not None: try: self.used_encoding = bom_enc return fix_data(data.decode(bom_enc)) except UnicodeDecodeError: pass try: self.used_encoding = 'utf-8' return fix_data(data.decode('utf-8')) except UnicodeDecodeError: pass data, self.used_encoding = xml_to_unicode(data) if normalize_to_nfc: data = unicodedata.normalize('NFC', data) return fix_data(data)

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

@property def book_type_for_display(self): return self.book_type.upper()

def __init__(self, rootpath, opfpath, log, clone_data=None): ContainerBase.__init__(self, 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: self.cloned = True for x in ('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('Could not locate opf file: %r'%opfpath)

# 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, 'name_path_map': { name:os.path.join(dest_dir, os.path.relpath(path, self.root)) for name, path in iteritems(self.name_path_map)} }

def clone_data(self, dest_dir): Container.commit(self, keep_parsed=False) self.cloned = True clone_dir(self.root, dest_dir) return self.data_for_clone(dest_dir)

[文档] def add_name_to_manifest(self, name, process_manifest_item=None): ' 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 = 'id' while item_id in all_ids: c += 1 item_id = 'id' + '%d'%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 = '%s-%d.%s' % (base, c, ext) return name

[文档] def add_file(self, name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None): ''' 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(('A file with the name %s already exists' % name) if self.has_name_case_insensitive(name) else ('An item with the href %s already exists in the manifest' % href)) 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) 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('Renaming of %s is not allowed' % current_name) 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 dont 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): if not self.has_name(name): return False try: return os.path.getsize(self.name_path_map[name]) > 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_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.opf_xpath('//opf:manifest/opf:item[@href and @id]')}

@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('{%s}itemref' % 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)

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 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.''' if index is None: parent.append(item) else: parent.insert(index, item) idx = parent.index(item) if idx == 0: item.tail = parent.text # If this is the only child of this parent element, we need a # little extra work as we have gone from a self-closing <foo /> # element to <foo><item /></foo> if len(parent) == 1: sibling = parent.getprevious() if sibling is None: # Give up! return parent.text = sibling.text item.tail = sibling.tail else: item.tail = parent[idx-1].tail if idx == len(parent)-1: parent[idx-1].tail = parent.text

[文档] 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]')} c = 0 item_id = id_prefix while item_id in all_ids: c += 1 item_id = id_prefix + '%d'%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(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('The file %s is not the same'%name) 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'

@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, log, clone_data=None, tdir=None): if clone_data is not None: super().__init__(None, None, 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: 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="%s" and @full-path]'%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__(tdir, opf_path, 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 clone_data(self, dest_dir): ans = super().clone_data(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="%s" and @full-path]'%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('//*[@id=%s]'%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('[\u0020\u0009\u000d\u000a]', '', 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 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) for name, data in iteritems(restore_fonts): with self.open(name, 'wb') as f: f.write(data)

@property def path_to_ebook(self): return self.pathtoepub

@path_to_ebook.setter def path_to_ebook(self, val): self.pathtoepub = val

# }}}

# 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 dont 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, log, clone_data=None, tdir=None): if clone_data is not None: super().__init__(None, None, 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__(tdir, opf_path, log) self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}

def clone_data(self, dest_dir): ans = super().clone_data(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): if log is None: log = default_log try: isdir = os.path.isdir(path) except Exception: isdir = False own_tdir = not tdir ebook_cls = (AZW3Container if path.rpartition('.')[-1].lower() in {'azw3', 'mobi', 'original_azw3', 'original_mobi'} and not isdir else EpubContainer) if own_tdir: tdir = PersistentTemporaryDirectory(f'_{ebook_cls.book_type}_container') try: ebook = ebook_cls(path, 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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('Cannot mark %s as cover as it does not exist' % name) mt = container.mime_map[name] if not is_raster_image(mt): raise ValueError('Cannot mark %s as the cover image as it is not a raster image' % name) 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('Cannot mark %s as cover as it is not in manifest' % name) 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('//opf:manifest/*[@id="%s"]' % 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: 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__', '0 0 %d %d'%(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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 = {x for x in 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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('Embedding referenced image: %s into jacket' % path) 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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()="%s"]' % 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 dont 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 += '#%s'%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 %s are both rename targets and destinations' % ', '.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('Cannot rename {0} to {1} as {1} already exists'.format(name, dest)) 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 + ('_%d.%s' % (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('//opf:manifest/opf:item[@href and @id="%s"]' % 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, '%s_%d.%s' % (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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 = '%s_split%d.%s' % (base, 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, '//*[@calibre-split-point="%d"]' % 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 = '%s_%d' % (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('Cannot merge files of type: %s' % category) if len(names) < 2: raise AbortError('Must specify at least two files to be merged') if master not in names: raise AbortError('The master file (%s) must be one of the files being merged' % master)

if category == 'text': merge_html(container, names, master) elif category == 'styles': merge_css(container, names, master)

container.dirty(master)

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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) + 'TOC: %s --> %s%s'%(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('./*[calibre:lower-case(local-name()) = "%s"]'%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 et = '{%s}type' % EPUB_NS for nav in root.iterdescendants(XHTML('nav')): if nav.get(et) == 'toc': ol = first_child(nav, XHTML('ol')) if ol is not None: 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 break 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 = '{%s}type' % EPUB_NS 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: 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='num_%d' % 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('//*[@id="%s"]'%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 = '{%s}type' % EPUB_NS 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('{%s}type' % EPUB_NS, 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('{%s}lang' % XML_NS, 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('{%s}type' % EPUB_NS, 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', 'level%d' % (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 = 'toc%d.xhtml' % c 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 # 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 '%s : menu action : %s'%(plugin.unique_name, 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 'Interface Action: %s (%s)'%(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: 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 dont 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 dont 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 implemente methods to execute the default action by drop some books from the library.

Inside the do_drop() method, the ids of the droped 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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('Unknown data type %s' % 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 = {}

[文档] 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 get_plugin(category, name): for plugin in preferences_plugins(): if plugin.category == category and plugin.name == name: return plugin raise ValueError( 'No Preferences Plugin with category: %s and name: %s found' % (category, name))

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): ''' 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 = 'config_widget_dialog_geometry_%s_%s'%(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.initialize() d.restore_geometry(gprefs, conf_name) 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): show_config_widget(category, name, gui=gui, show_restart_msg=True)

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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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

[文档] 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='%s%05d-' % (prefix, self.container_count), 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 path = path[-1] 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)

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: 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 = '%s_%d.%s' % (name, i, ext) try: with open(path, 'rb') as f: c.add_file(name, f.read()) except: 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')) d = TOCEditor(title=self.current_metadata.title, parent=self.gui) if d.exec() != 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: 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: 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: 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( '<pre>%s</pre>'%newname, '<pre>%s</pre>' % urlnormalize(newname)), '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, 'action_global_%s' % 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 } md.setUrls(list(map(QUrl.fromLocalFile, list(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 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('//*[@data-lnum="%s"]' % 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 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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 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 lxml import html 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.db.notes.exim import expand_note_resources, parse_html from calibre.ebooks.metadata import title_sort 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 _, calibre_langcode_to_name, canonicalize_lang from calibre.utils.titlecase import titlecase from polyglot.builtins import iteritems, itervalues 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('Class %s is not an instance of FormatterFunction'%( func_class.__class__.__name__)) name = func_class.name if name in self._functions: raise ValueError('Name %s already used'%name) 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('Class %s is not an instance of FormatterFunction'%( func_class.__class__.__name__)) name = func_class.name if not replace and name in self._functions: raise ValueError('Name %s already used'%name) 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): proxy = mi.get('_proxy_metadata', 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, 'library_database_instance', None) if wr is None: if name is not None: only_in_gui_error() 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): return get_database(mi, self.name)

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: lines = [] self.program_text = ''.join(lines)

[文档] class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 category = 'Relational' __doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt. In many cases the lexical comparison operators ' '(>, <, == etc) can replace this function.')

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 = _('strcmpcase(x, y, lt, eq, gt) -- does a case-sensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt.\n' '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 = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to ' 'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt. ' 'In many cases the numeric comparison operators ' '(>#, <#, ==# etc) can replace this function.')

def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): x = float(x if x and x != 'None' else 0) y = float(y if y and y != 'None' else 0) if x < y: return lt if x == y: return eq return gt

[文档] class BuiltinFirstMatchingCmp(BuiltinFormatterFunction): name = 'first_matching_cmp' category = 'Relational' arg_count = -1 __doc__ = doc = _('first_matching_cmp(val, [cmp1, result1,]+, else_result) -- ' 'compares "val < cmpN" in sequence, returning resultN for ' 'the first comparison that succeeds. Returns else_result ' 'if no comparison succeeds. Example: ' 'first_matching_cmp(10,5,"small",10,"middle",15,"large","giant") ' '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 = _('strcat(a [, b]*) -- can take any number of arguments. Returns the ' 'string formed by concatenating all the arguments')

def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 res = '' for i in range(0, len(args)): res += args[i] return res

[文档] class BuiltinStrlen(BuiltinFormatterFunction): name = 'strlen' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('strlen(a) -- Returns the length of the string passed as ' 'the argument')

def evaluate(self, formatter, kwargs, mi, locals, a): try: return len(a) except: 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. ' 'This function can often be ' 'replaced with the + operator.')

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 = _('subtract(x, y) -- returns x - y. Throws an exception if ' 'either x or y are not numbers. This function can often be ' 'replaced with 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 = _('multiply(x [, y]*) -- returns the product of its arguments. ' 'Throws an exception if any argument is not a number. ' 'This function can often be replaced with 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 = _('divide(x, y) -- returns x / y. Throws an exception if ' 'either x or y are not numbers.' ' This function can often be replaced with 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 = _('ceiling(x) -- returns the smallest integer greater ' 'than or equal to x. Throws an exception if x is ' 'not a number.')

def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(ceil(x)))

[文档] class BuiltinFloor(BuiltinFormatterFunction): name = 'floor' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('floor(x) -- returns the largest integer less ' 'than or equal to x. Throws an exception if x is ' 'not a number.')

def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(floor(x)))

[文档] class BuiltinRound(BuiltinFormatterFunction): name = 'round' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('round(x) -- returns the nearest integer to x. ' 'Throws an exception if x is not a number.')

def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(round(x)))

class BuiltinMod(BuiltinFormatterFunction): name = 'mod' arg_count = 2 category = 'Arithmetic' __doc__ = doc = _('mod(x) -- returns floor(remainder of x / y). ' 'Throws an exception if either x or y is not a number.')

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(int(x % y))

[文档] class BuiltinFractionalPart(BuiltinFormatterFunction): name = 'fractional_part' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('fractional_part(x) -- returns the value after the decimal ' 'point. For example, fractional_part(3.14) returns 0.14. ' 'Throws an exception if x is not a number.')

def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(modf(x)[0])

[文档] class BuiltinTemplate(BuiltinFormatterFunction): name = 'template' arg_count = 1 category = 'Recursion'

__doc__ = doc = _('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. 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 = _('eval(template) -- evaluates the template, passing the local ' 'variables (those \'assign\'ed to) instead of the book metadata. ' ' This permits using the template processor to construct complex ' 'results from local variables. Because the { and } ' 'characters are special, 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 = _('assign(id, val) -- assigns val to id, then returns val. ' 'id must be an identifier, not an expression. ' 'This function can often be replaced with the = operator.')

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 = _('list_split(list_val, sep, id_prefix) -- splits the list_val ' "into separate values using 'sep', then assigns the values " "to 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: split('one:two:foo', ':', 'var') is equivalent " "to var_0 = 'one'; var_1 = 'two'; var_2 = 'foo'.")

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 = _('print(a[, b]*) -- prints the arguments to standard output. ' 'Unless you start calibre from the command line (calibre-debug -g), ' 'the output will go to a black hole.')

def evaluate(self, formatter, kwargs, mi, locals, *args): print(args) return ''

[文档] class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('field(lookup_name) -- returns the metadata field named by lookup_name')

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 values from metadata' __doc__ = doc = _('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 ' "'default' if the field is undefined ('None').")

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 values from metadata' __doc__ = doc = _('raw_list(lookup_name, separator) -- returns the metadata list ' 'named by lookup_name without applying any formatting or sorting and ' 'with items separated by separator.')

def evaluate(self, formatter, kwargs, mi, locals, name, separator): res = getattr(mi, name, None) if not isinstance(res, list): return "%s is not a list" % name return separator.join(res)

[文档] class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 category = 'String manipulation' __doc__ = doc = _('substr(str, start, end) -- returns the start\'th through the end\'th ' 'characters of str. The first character in str 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, str_, start_, end_): return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]

[文档] class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 category = 'Iterating over values' __doc__ = doc = _('lookup(val, [pattern, field,]+ else_field) -- ' 'like switch, except the arguments are field (metadata) names, not ' 'text. The value of the appropriate field will be fetched and used. ' 'Note that because composite columns are fields, you can use this ' 'function in one composite field to use the value of some other ' 'composite field. This is extremely useful when constructing ' 'variable save paths')

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 = 'If-then-else' __doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not ' 'empty` if val 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 = 'If-then-else' __doc__ = doc = _('contains(val, pattern, text if match, text if not match) -- checks ' 'if val contains matches for the regular expression `pattern`. ' 'Returns `text if match` if matches are found, otherwise it returns ' '`text if no 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 over values' __doc__ = doc = _('switch(val, [pattern, value,]+ else_value) -- ' 'for each `pattern, value` pair, checks if `val` matches ' 'the regular expression `pattern` and if so, returns that ' '`value`. If no pattern matches, then `else_value` is returned. ' 'You can have as many `pattern, value` pairs as you want')

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 over values' __doc__ = doc = _('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 = _('strcat_max(max, string1 [, prefix2, string2]*) -- ' 'Returns a string formed by concatenating the arguments. The ' 'returned value is initialized to string1. `Prefix, string` ' 'pairs are added to the end of the value as long as the ' 'resulting string length is less than `max`. String1 is returned ' '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: 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: pass return result.strip()

[文档] class BuiltinInList(BuiltinFormatterFunction): name = 'in_list' arg_count = -1 category = 'List lookup' __doc__ = doc = _('in_list(val, separator, [ pattern, found_val, ]+ not_found_val) -- ' 'treating val as a list of items separated by separator, ' 'if the pattern matches any of the list values then return found_val.' 'If the pattern matches no list value then return ' 'not_found_val. The pattern and found_value pairs can be repeated as ' 'many times as desired. The patterns are checked in order. The ' 'found_val for the first match is returned. ' 'Aliases: in_list(), list_contains()') aliases = ['list_contains']

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 = _('str_in_list(val, separator, [string, found_val, ]+ not_found_val) -- ' 'treating val as a list of items separated by separator, if the ' 'string matches any of the list values then return found_val.' 'If the string matches no list value then return ' 'not_found_val. The comparison is exact match (not contains) and is ' 'case insensitive. The string and found_value pairs can be repeated as ' 'many times as desired. The patterns are checked in order. The ' 'found_val for 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 = _('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 = _('re(val, pattern, replacement) -- return val after applying ' 'the regular expression. All instances of `pattern` are replaced ' 'with `replacement`. As in all of calibre, these are ' 'Python-compatible regular expressions')

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 = _('re_group(val, pattern [, template_for_group]*) -- ' 'return a string made by applying the regular expression pattern ' 'to the val and replacing each matched instance with the string ' 'computed by replacing each matched group by the value returned ' 'by the corresponding template. The original matched value for the ' 'group is available as $. In template program mode, like for ' 'the template and the eval functions, you use [[ for { and ]] for }.' ' The following example in template program mode looks for series ' 'with more than one word and uppercases the first word: ' "{series:'re_group($, \"(\\S* )(.*)\", \"[[$:uppercase()]]\", \"[[$]]\")'}")

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(0, 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 = _('swap_around_comma(val) -- 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, the function ' 'returns val 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 = 'If-then-else' __doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' '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 = _('shorten(val, left chars, middle text, right chars) -- Return a ' 'shortened version of val, consisting of `left chars` ' 'characters from the beginning of val, followed by ' '`middle text`, followed by `right chars` characters from ' 'the end of the string. `Left chars` and `right chars` must be ' 'integers. For example, assume the title of the book is ' '`Ancient English Laws in the Times of Ivanhoe`, and you want ' 'it to fit in a space of at most 15 characters. If you use ' '{title:shorten(9,-,5)}, the result will be `Ancient E-anhoe`. ' 'If the field\'s length is less than left chars + right chars + ' 'the length of `middle text`, then the field will be used ' 'intact. 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 = 'count' arg_count = 2 category = 'List manipulation' aliases = ['list_count']

__doc__ = doc = _('count(val, separator) -- interprets the value as a list of items ' 'separated by `separator`, returning the number of items in the ' 'list. Most lists use a comma as the separator, but authors ' 'uses an ampersand. Examples: {tags:count(,)}, {authors: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 = _('list_count_matching(list, pattern, separator) -- ' "interprets 'list' 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, list_, pattern, sep): res = 0 for v in [x.strip() for x in list_.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 = _('list_item(val, 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 can be returned ' 'using `list_item(-1,separator)`. If the item is not in the list, ' 'then the empty value is returned. The separator has the same ' 'meaning as in the count function.')

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: return ''

[文档] class BuiltinSelect(BuiltinFormatterFunction): name = 'select' arg_count = 2 category = 'List lookup' __doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list ' 'of items, with the items being "id:value". Find the pair with the ' 'id equal to key, and return the corresponding value. Returns the ' 'empty string if no match is found.' )

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 values from metadata' __doc__ = doc = _('approximate_formats() -- return a comma-separated ' 'list of formats that at one point were associated with the ' 'book. There is no guarantee that this list is correct, ' 'although it probably is. ' 'This function can be called in template program mode using ' 'the template "{:\'approximate_formats()\'}". ' 'Note that format names are always uppercase, as in EPUB. ' '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 values from metadata' __doc__ = doc = _('formats_modtimes(date_format) -- return a comma-separated ' 'list of colon-separated items representing modification times ' 'for the formats of a book. The date_format parameter ' 'specifies how the date is to be formatted. See the ' 'format_date function for details. You can use the select ' 'function to get the mod 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: return ''

[文档] class BuiltinFormatsSizes(BuiltinFormatterFunction): name = 'formats_sizes' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('formats_sizes() -- return a comma-separated list of ' 'colon-separated items representing sizes in bytes ' 'of the formats of a book. 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: return ''

[文档] class BuiltinFormatsPaths(BuiltinFormatterFunction): name = 'formats_paths' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('formats_paths() -- return a comma-separated list of ' 'colon-separated items representing full path to ' 'the formats of a book. 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): fmt_data = mi.get('format_metadata', {}) try: return ','.join(k.upper()+':'+str(v['path']) for k,v in iteritems(fmt_data)) except: return ''

[文档] class BuiltinHumanReadable(BuiltinFormatterFunction): name = 'human_readable' arg_count = 1 category = 'Formatting values' __doc__ = doc = _('human_readable(v) -- return a string ' 'representing the number v in KB, MB, GB, etc.' )

def evaluate(self, formatter, kwargs, mi, locals, val): try: return human_readable(round(float(val))) except: return ''

[文档] class BuiltinFormatNumber(BuiltinFormatterFunction): name = 'format_number' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('format_number(v, template) -- format the number v using ' 'a Python formatting template such as "{0:5.2f}" or ' '"{0:,d}" or "${0:5,.2f}". The field_name part of the ' 'template must be a 0 (zero) (the "{0:" in the above examples). ' 'See the template language and Python documentation for more ' 'examples. You can leave off the leading "{0:" and trailing ' '"}" if the template contains only a format. 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: return '' try: # Try formatting the value as a float return template.format(v1) except: pass try: # Try formatting the value as an int v2 = trunc(v1) if v2 == v1: return template.format(v2) except: pass return ''

[文档] class BuiltinSublist(BuiltinFormatterFunction): name = 'sublist' arg_count = 4 category = 'List manipulation' __doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the ' 'value as a list of items separated by `separator`, returning a ' 'new list made from the `start_index` to the `end_index` item. ' '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 using ' 'basic template mode and assuming that the tags column (which is ' 'comma-separated) contains "A, B, C": ' '{tags:sublist(0,1,\\\\,)} returns "A". ' '{tags:sublist(-1,0,\\\\,)} returns "C". ' '{tags:sublist(0,-1,\\\\,)} returns "A, B".' )

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: return ''

[文档] class BuiltinSubitems(BuiltinFormatterFunction): name = 'subitems' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to ' 'break apart lists of items such as genres. It interprets the value ' 'as a comma-separated list of items, where each item is a period-' 'separated list. Returns a new list made by first finding all the ' 'period-separated items, then for each such item extracting the ' '`start_index` to the `end_index` components, then combining ' 'the results back together. The first component 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. ' 'Example using basic template mode and assuming a #genre value of ' '"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 ' 'value of "A.B.C, D.E.F", {#genre:subitems(0,1)} returns "A, D". ' '{#genre:subitems(0,2)} returns "A.B, D.E"')

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: pass return ', '.join(sorted(rv, key=sort_key))

[文档] class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('format_date(val, format_string) -- format the value, ' 'which must be a date, using the format_string, returning a string. ' 'The formatting codes are: ' '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 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: the date as a floating point number ' 'from_number[:fmt]: format the timestamp using fmt if present otherwise iso')

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: s = 'BAD DATE' return s

[文档] class BuiltinFormatDateField(BuiltinFormatterFunction): name = 'format_date_field' arg_count = 2 category = 'Formatting values' __doc__ = doc = _("format_date_field(field_name, format_string) -- format " "the value in the field 'field_name', which must be the lookup name " "of date field, either standard or custom. See '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 can't be used for computed dates or dates in string " "variables. Example: format_date_field('pubdate', 'yyyy.MM.dd')")

def evaluate(self, formatter, kwargs, mi, locals, field, format_string): try: if field not in mi.all_field_keys(): return _('Unknown field %s passed to function %s')%(field, 'format_date_field') val = mi.get(field, None) 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: traceback.print_exc() s = 'BAD DATE' return s

[文档] class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('uppercase(val) -- return val in upper case')

def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper()

[文档] class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('lowercase(val) -- return val in lower case')

def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower()

[文档] class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('titlecase(val) -- return val in title case')

def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val)

[文档] class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 category = 'String case changes' __doc__ = doc = _('capitalize(val) -- return val capitalized')

def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val)

[文档] class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('booksize() -- return value of the size field. ' '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: pass return '' self.only_in_gui_error()

[文档] class BuiltinOndevice(BuiltinFormatterFunction): name = 'ondevice' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('ondevice() -- return 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 values from metadata' __doc__ = doc = _('annotation_count() -- return the total number of annotations ' 'of all types attached to the current book. ' 'This function works only in the GUI.')

def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).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 values from metadata' __doc__ = doc = _("is_marked() -- check whether the book is 'marked' in " "calibre. If it is then return the value of the mark, " "either 'true' or the comma-separated list of named " "marks. Returns '' if the book is not marked.")

def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).data.get_marked(mi.id) return c if c else ''

[文档] class BuiltinSeriesSort(BuiltinFormatterFunction): name = 'series_sort' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('series_sort() -- return 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 values from metadata' __doc__ = doc = _('has_cover() -- return Yes if the book has a cover, ' 'otherwise return 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 over values' __doc__ = doc = _('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 = _('and(value [, value]*) -- ' 'returns the string "1" if all values are not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want. In many ' 'cases the && operator can replace this function.')

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 = _('or(value [, value]*) -- ' 'returns the string "1" if any value is not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want. In many ' 'cases the || operator can replace this function.')

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 = _('not(value) -- ' 'returns the string "1" if the value is empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. In many cases the ! operator can replace this ' 'function.')

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 = _("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.\n" "Example:") + "\n\n" + ( " program:\n" " list_join('#@#', $authors, '&', $tags, ',')\n\n") + _( "You can use list_join on the results of previous " "calls to list_join as follows:") + "\n" + ( " program:\n\n" " a = list_join('#@#', $authors, '&', $tags, ',');\n" " b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&')\n\n") + _( "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:") + "\n" + ( " program:\n" " list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \\1'), ',')")

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 = dict() 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 = _('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: list_union(), merge_lists()') 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 = _("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 are " "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).")

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 = _('list_remove_duplicates(list, separator) -- ' 'return a list made by removing duplicate items in the source list. ' 'If items differ only in case, the last of them is returned. ' 'The items in source 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 = _('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 = _('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 = _('list_sort(list, direction, separator) -- ' 'return list sorted using a case-insensitive sort. If direction is ' 'zero, the list 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, list1, direction, separator): res = [l.strip() for l in list1.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 = _('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 = _('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 = _('list_re_group(src_list, separator, include_re, search_re [, group_template]+) -- ' 'Like list_re except replacements are not optional. It ' 'uses re_group(list_item, search_re, group_template, ...) when ' 'doing the replacements on the resulting list.')

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(0, 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 = _('today() -- ' 'return a date string for today. This value is designed for use in ' 'format_date or days_between, but can be manipulated like any ' 'other string. The date is in ISO 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 = _('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: return '' i = d1 - d2 return '%.1f'%(i.days + (i.seconds/(24.0*60.0*60.0)))

[文档] class BuiltinDateArithmetic(BuiltinFormatterFunction): name = 'date_arithmetic' arg_count = -1 category = 'Date functions' __doc__ = doc = _('date_arithmetic(date, calc_spec, fmt) -- ' "Calculate a new date from 'date' 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: " "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. " "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, date, calc_spec, fmt=None): try: d = parse_date(date) 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 values from metadata' __doc__ = doc = _('language_strings(lang_codes, localize) -- ' 'return the strings for the language codes passed in lang_codes. ' '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: pass return ', '.join(retval)

[文档] class BuiltinLanguageCodes(BuiltinFormatterFunction): name = 'language_codes' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('language_codes(lang_strings) -- ' 'return the language codes for the strings 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: pass return ', '.join(retval)

[文档] class BuiltinCurrentLibraryName(BuiltinFormatterFunction): name = 'current_library_name' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('current_library_name() -- ' 'return the last name on the path to the current calibre library. ' 'This function can be called in template program mode using the ' 'template "{:\'current_library_name()\'}".')

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 values from metadata' __doc__ = doc = _('current_library_path() -- ' 'return the path to the current calibre library. This function can ' 'be called in template program mode using the template ' '"{:\'current_library_path()\'}".')

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 = _('finish_formatting(val, fmt, prefix, suffix) -- apply the ' 'format, prefix, and suffix to a value in the same way as ' 'done in a template like `{series_index:05.2f| - |- }`. For ' 'example, the following program produces the same output ' 'as the above template: ' 'program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")')

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 values from metadata' __doc__ = doc = _('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) 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 values from metadata' __doc__ = doc = _('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()".')

def evaluate(self, formatter, kwargs, mi, locals): return self.get_database(mi).data.get_base_restriction_name()

[文档] class BuiltinUserCategories(BuiltinFormatterFunction): name = 'user_categories' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('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 = _('transliterate(a) -- Returns a string in a latin alphabet ' 'formed by approximating the sound of the words in the ' 'source string. For example, if the source is "{0}"' ' the 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 = 'Template database functions' __doc__ = doc = _("get_link(field_name, field_value) -- fetch the link for " "field 'field_name' with value 'field_value'. If there is " "no attached link, return ''. Example: " "get_link('tags', 'Fiction') returns the link attached to " "the tag 'Fiction'.")

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api try: link = None item_id = db.get_item_id(field_name, field_value) 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 values from metadata' __doc__ = doc = _('author_links(val_separator, pair_separator) -- returns ' 'a string containing a list of authors and that author\'s ' 'link values in the ' 'form author1 val_separator author1link pair_separator ' 'author2 val_separator author2link etc. An author is ' 'separated from its link value by the val_separator string ' 'with no added spaces. author:linkvalue pairs are separated ' 'by the pair_separator string argument with no added spaces. ' 'It is up to you to choose separator strings 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 values from metadata' __doc__ = doc = _('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 (different from the author_sort in books). The ' 'returned list has the form author sort 1 val_separator ' 'author sort 2 etc. 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 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 values from metadata' __doc__ = doc = _("connected_device_name(storage_location) -- if a device is " "connected then return the device name, otherwise return " "the empty string. Each storage location on a device can " "have a different name. The 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_name'] except Exception: traceback.print_exc() raise self.only_in_gui_error()

[文档] class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): name = 'connected_device_uuid' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _("connected_device_uuid(storage_location) -- 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 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 = 'If-then-else' __doc__ = doc = _('check_yes_no(field_name, is_undefined, is_false, is_true) ' '-- checks the value of the yes/no field named by the ' 'lookup key field_name for a value specified by the ' 'parameters, returning "yes" if a match is found, otherwise ' 'returning an 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 undefined (neither True ' 'nor False) or True. More than one of is_undefined, ' 'is_false, or is_true can be set to 1. This function ' 'is usually used by the test() or is_empty() functions.')

def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true): # 'field' is a lookup name, not a value if field not in self.get_database(mi).field_metadata: raise ValueError(_("The column {} doesn't exist").format(field)) res = getattr(mi, field, None) 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 = _('rating_to_stars(value, use_half_stars) ' '-- Returns the rating as string of star characters. ' 'The value is a number between 0 and 5. Set use_half_stars ' 'to 1 if you want half star characters for custom ratings ' 'columns that support non-integer ratings, for example 2.5.')

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: 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 = _('swap_around_articles(val, separator) ' '-- returns the val with articles moved to the end. ' 'The value can be a list, in which case each member ' 'of the list is processed. If the value is a list then ' 'you must provide the list value separator. If no ' 'separator is provided then the value is treated as ' 'being a single value, not a list.')

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: traceback.print_exc() return separator.join(sorted(result, key=sort_key))

[文档] class BuiltinArguments(BuiltinFormatterFunction): name = 'arguments' arg_count = -1 category = 'Other' __doc__ = doc = _('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, effectively parameters. The variables ' 'are positional; they get the value of the parameter given ' 'in the call in the same position. If the corresponding ' 'parameter 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 = _('globals(id[=expression] [, id[=expression]]*) ' '-- Retrieves "global variables" that can be passed into ' 'the formatter. It both declares and initializes local ' 'variables with the names of the global variables passed ' 'in. If the corresponding variable is not provided in ' 'the passed-in 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 = _('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 = 'If-then-else' __doc__ = doc = _('field_exists(field_name) -- checks if a field ' '(column) named field_name exists, returning ' "'1' if so and '' 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 = _('character(character_name) -- returns the ' 'character named by character_name. For example, ' r"character('newline') returns a newline character ('\n'). " "The supported character names are 'newline', 'return', " "'tab', and 'backslash'.")

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 = 'String manipulation' __doc__ = doc = _('to_hex(val) -- returns the string encoded in 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 = 'Formatting values' __doc__ = doc = _('urls_from_identifiers(identifiers, sort_results) -- given ' 'a comma-separated list of identifiers, where an identifier ' 'is a colon-separated pair of values (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 = 'Template database functions' __doc__ = doc = _('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 can be used only in the GUI.')

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) try: ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') return len(ids) except Exception: traceback.print_exc()

[文档] class BuiltinBookValues(BuiltinFormatterFunction): name = 'book_values' arg_count = 4 category = 'Template database functions' __doc__ = doc = _('book_values(column, query, sep, use_vl) -- returns a list ' 'of the values contained in the column "column", separated ' 'by "sep", in the books found by searching for "query". ' 'If use_vl is 0 (zero) then virtual libraries are ignored. ' 'This function can be used only in the GUI.')

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) if column not in db.field_metadata: raise ValueError(_("The column {} doesn't exist").format(column)) try: ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') 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 = 'Template database functions' __doc__ = doc = _("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. " 'This function can be used only in the GUI.')

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).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 = 'Template database functions' __doc__ = doc = _("extra_file_names(sep [, pattern]) -- returns a sep-separated " "list of extra files in the book's '{}/' 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. " 'This function can be used only in the GUI.').format(DATA_DIR_NAME)

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).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 = 'Template database functions' __doc__ = doc = _("extra_file_size(file_name) -- returns the size in bytes of " "the extra file 'file_name' in the book's '{}/' folder if " "it exists, otherwise -1." 'This function can be used only in the GUI.').format(DATA_DIR_NAME)

def evaluate(self, formatter, kwargs, mi, locals, file_name): db = self.get_database(mi).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 = 'Template database functions' __doc__ = doc = _("extra_file_modtime(file_name, format_string) -- returns the " "modification time of the extra file 'file_name' in the " "book's '{}/' folder if it exists, otherwise -1.0. The " "modtime is formatted according to 'format_string' " "(see format_date()). If 'format_string' is empty, returns " "the modtime as the floating point number of seconds since " "the epoch. The epoch is OS dependent. " "This function can be used only in the GUI.").format(DATA_DIR_NAME)

def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string): db = self.get_database(mi).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 = 'Template database functions' __doc__ = doc = _("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. If 'plain_text' " "is non-empty, return the note's plain text. If the note " "doesn't exist, return '' in both cases. Example: " "get_note('tags', 'Fiction', '') returns the HTML of the " "note attached to the tag 'Fiction'.")

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value, plain_text): db = self.get_database(mi).new_api try: note = '' item_id = db.get_item_id(field_name, field_value) if item_id is not None: note_data = db.notes_data_for(field_name, item_id) if note_data is not None: if plain_text == '1': return note['searchable_text'].partition('\n')[2] # 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_data['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 note except Exception as e: traceback.print_exc() raise ValueError(e)

[文档] class BuiltinHasNote(BuiltinFormatterFunction): name = 'has_note' arg_count = 2 category = 'Template database functions' __doc__ = doc = _("has_note(field_name, field_value) -- return '1' " "if the value 'field_value' in the field 'field_name' " "has an attached note, '' otherwise. Example: " "has_note('tags', 'Fiction') returns '1' if the tag " "'fiction' has an attached note, '' otherwise.")

def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api note = None try: item_id = db.get_item_id(field_name, field_value) if item_id is not None: note = db.notes_data_for(field_name, item_id) except Exception as e: traceback.print_exc() raise ValueError(e) return '1' if note is not None else ''

_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(), BuiltinEval(), BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(), BuiltinFormatDate(), BuiltinFormatDateField(), BuiltinFormatNumber(), BuiltinFormatsModtimes(), BuiltinFormatsPaths(), BuiltinFormatsSizes(), BuiltinFractionalPart(), BuiltinGetLink(), BuiltinGetNote(), BuiltinGlobals(), BuiltinHasCover(), BuiltinHasExtraFiles(), BuiltinHasNote(), BuiltinHumanReadable(), BuiltinIdentifierInList(), BuiltinIfempty(), BuiltinLanguageCodes(), BuiltinLanguageStrings(), BuiltinInList(), BuiltinIsMarked(), BuiltinListCountMatching(), BuiltinListDifference(), BuiltinListEquals(), BuiltinListIntersection(), BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(), BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(), BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(), BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), 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('**** Compilation errors in user template function "%s" ****' % func_name) traceback.print_exc(limit=10) prints('**** End compilation errors in %s "****"' % 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。最后一次更新于 5月 03, 2024。

导航

- 模块
- 开始 »
- 模块代码 »
- 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': 'advertisment'} #: } #: #: 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 # JPEG image sizes

#: 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, JPEG 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

#: Set to False if you do not want to use gzipped transfers. 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 precessed 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 mozilla firefox user agent.

If your recipe requires that you login first, override this method in your subclass. For example, the following code is used in the New York Times recipe to login for full access::

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

''' 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']) 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'): # 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://') or url.startswith('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 None

[文档] 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: self.log.exception('Auto cleanup of URL: %r failed'%url)

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('Failed to parse url: %r, ignoring' % url) 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('Could not fetch index from %s'%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: doc = document_fromstring(article_html) frag = doc.xpath('//body')[-1] if frag.tag == 'html': root = frag elif frag.tag == 'body': root = document_fromstring( '<html><head><title>%s</title></head></html>' % extracted_title) root.append(frag) else: root = document_fromstring( '<html><head><title>%s</title></head><body/></html>' % extracted_title) 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('Ignoring invalid TOC thumbnail image: %r'%src) 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 = []

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']): x.name = 'div'

if job_info: url, f, a, feed_len = job_info try: article = self.feed_objects[f].articles[a] except: self.log.exception('Failed to get article object for postprocessing') pass 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: 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) + '\u2026' 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, 'feed_image_%d%s'%(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: 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('Removing duplicate article: %s from section: %s'%( article.title, 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, 'feed_%d'%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, 'article_%d'%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: 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, 'feed_%d'%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: 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: self.log.exception("Failed to download supplied masthead_url")

def resolve_masthead(self): self.masthead_path = None try: murl = self.get_masthead_url() except: 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: 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: 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, 'feed_%d'%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 = 'feed_%d/article_%d/'%(num, 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('%sindex.html'%adir) po = self.play_order_map.get(entries[-1], None) if po is None: self.play_order_counter += 1 po = self.play_order_counter arelpath = '%sindex.html'%adir 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, ('%sindex.html'%adir).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('feed_%d/index.html'%i) 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('feed_%d/index.html'%i, None, f.title, play_order=po, description=desc, author=auth))

else: entries.append('feed_%d/index.html'%0) 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 = 'article_%d/index.html'%a 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('^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')+' %s...'%(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 = 'Failed feed: %s'%(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): if self.resolve_internal_links and self.article_url_map: seen = set() for item in oeb.spine: for a in item.data.xpath('//*[local-name()="a" and @href]'): if a.get('rel') == 'calibre-downloaded-from': continue url = a.get('href') 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 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( 'https://news.calibre-ebook.com/subscribed_files/%s/0/temp.downloaded_recipe' % self.calibre_periodicals_slug ).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: self.log.exception('Failed to compile downloaded recipe') return os.path.abspath('index.html')

© 版权 Kovid Goyal。最后一次更新于 5月 03, 2024。

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. 设计您的图书，使其与calibre阅读器配合使用

电子书转换

1. 介绍

2. 界面外观

1. 字体
2. 文本
3. 布局
4. 样式
5. 改变样式
6. Transform HTML

3. 页面设置
4. 智能处理
5. 搜索并替换

6. 结构检测

1. Chapters and page breaks

2. 杂项

7. 目录

8. Using images as chapter titles when converting HTML input documents

9. Using tag attributes to supply the text for entries in the Table of Contents

10. How options are set/saved for conversion

11. Format specific tips

1. 转换Microsoft Word文档

1. Older .doc files

2. 转换文本文档
3. 转换PDF文档
4. Comic book collections
5. EPUB advanced formatting demo
6. Convert ODT documents

7. 转换为PDF

1. 页头和页脚

2. 可打印目录

3. Custom page margins for individual HTML files

编辑书籍

1. 基本的工作流程

2. 文件浏览器

1. 重命名文件
2. 合并文件
3. 更改文本文件顺序
4. 标记封面
5. 删除文件
6. 正在导出文件
7. 增加新的图片/字体等或者创建新空文件
8. 替代文件
9. 有效地将样式表链接到HTML文件

3. 搜索并替换

1. 保存的查找
2. 功能模式
3. 搜索时忽略 HTML 标签

4. 自动化工具

1. 正在编辑目录
2. 正在检查书籍
3. 正在添加封面
4. 嵌入使用到的字体
5. 子集化嵌入字体
6. 规范标点
7. 转换 CSS 属性
8. 移除无用的 CSS 规则
9. 正在修正HTML
10. 美化文件
11. 插入内联目录
12. 正在设置语义
13. 过滤样式信息
14. 升级本书的内部结构

5. 还原点

6. 实时预览面板

1. 分割HTML文件

7. 实时 CSS 面板

8. 杂项工具

1. 目录视图

2. 检查图书中单词的拼写

1. 正在添加新的字典

3. 插入特殊字符
4. 代码检查器视图
5. 正在检查外部链接
6. 正在下载外部资源
7. 按类型将文件整理到文件夹中
8. 将其他电子书格式的文件导入为 EPUB
9. 报表工具

9. 代码编辑器中的特殊功能

1. 语法高亮
2. 上下文帮助
3. 自动完成
4. 片段

calibre 内容服务器

1. 从其他设备访问内容服务器

1. 从家庭网络上的设备访问服务器

1. 家庭网络连接故障排除

2. 从互联网上任何地方访问服务器

2. 服务器接口界面

1. 书籍列表

2. 电子书阅读人

3. 浏览器支持
4. 启用脱机支持
5. 仅从命令行管理用户帐户

6. 将calibre内容服务器集成到其他服务器中

1. 使用完整的虚拟主机

2. 使用URL前缀

7. 在现代Linux系统上为calibre服务器创建服务

对比电子书

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. 我用calibre 生成的EPUB是不是有效？

8. 如何使用转换工具的一些高级功能？

设备集成

1. Calibre 支持哪些设备？

2. 我如何才能帮助我的设备获得calibre支持？

3. 我的设备没有被calibre检测到吗？

4. 我的设备不是标准的或不寻常的。我可以做些什么来连接到它？

5. 如何在iPad/iPhone/iPod Touch中使用Calibre？

1. 使用内容服务器

6. 如何在我的安卓手机/平板电脑或Kindle Fire上使用Calibre？

1. 使用 USB 线

2. OTA升级

7. 我可以使用Kindle或其他阅读设备中的网络浏览器访问我的calibre图书吗？

8. 我不能使用Calibre发送电子邮件？

9. 我的设备在Linux中以只读方式安装，因此Calibre无法连接到它？

10. 为什么Calibre不支持Kindle上的 收藏或Nook上的货架？

11. 当我尝试在我的Kobo Touch/Glo/等中使用Calibre时出现错误？

12. 我发送到我的电子墨水Kindle上的书籍的封面会瞬间出现，然后被一个通用的封面取代？

13. 我的MOBI文件的封面已经不再出现在适用于PC的Kindle/适用于Android/iPad的Kindle等等。

14. 我用Calibre把一些书转移到我的Kindle上，它们没有出现吗？

书库管理

1. 图书文件存放在哪里？

2. Calibre如何管理作者姓名和排序？

3. 为什么 calibre 不允许我将书籍存储在我自己的文件夹结构中？

4. 为什么 calibre 没有 foo 列？

5. 我可以有一栏显示格式或 ISBN 吗？

6. 如何将我的 calibre 数据从一台计算机转移到另一台计算机？

7. calibre 的书籍列表是空白的！

8. 我的 calibre 库在网络驱动器/NAS 上出现错误？

杂项

1. 亚马逊将停止通过电子邮件发送 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. 如何使用购买的 calibre EPUB 图书（或者如何处理 .acsm 文件）？

15. 我收到“权限被拒绝”错误？

16. 我可以让评论元数据显示在我的阅读器上吗？

17. 我如何才能使用我的HTTP代理？

18. 我想要一些功能增加到calibre。我能做什么?

19. Calibre 为什么没有自动更新？

20. Calibre 授权协议是什么？

21. 怎样在 U 盘上运行 Calibre？

22. 如何在我自己的Linux服务器上运行新闻下载和内容服务器等功能？

教程
添加你喜欢的新闻站点

1. 完全自动获取

1. calibre博客

2. bbc.co.uk

2. 自定义获取过程

1. 使用 bbc.co.uk 的印刷版
2. 替换文章样式
3. 数据交叉分析
4. 现实生活中的例子

3. 开发新新闻源的技巧

4. 以后阅读

5. API文档

1. 清单的API文档

管理图书的子组，例如“体裁”

1. 配置
2. 搜索
3. 限制
4. 有用的模板函数

XPath教程

1. 按标记名选取
2. 按属性选择
3. 按标签内容选择
4. 电子书样本
5. XPath内置函数

calibre 模板语言

1. 基本模板
2. 高级格式
3. Using templates to define custom columns
4. Templates and plugboards

5. Using functions in templates - Single Function Mode

6. General Program Mode

7. More complex programs in template expressions - Template Program Mode

8. Python Template Mode

9. 保存的模板

10. Providing additional information to templates

11. Notes on the difference between modes
12. User-defined Python template functions
13. Special notes for save/send templates
14. Tips

15. Function reference

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. 使用附加限制

Calibre:// URL方案

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. 代码布局

2. 得到代码

1. 提交要包含的修改

3. Windows开发环境
4. macOS开发环境
5. Linux开发环境

6. 在一台电脑上同时安装正常模式与开发模式的calibre

7. Debug技巧

1. 使用print语句
2. 使用交互式Python解释器
3. 使用Python调试器作为远程调试器
4. 在自己的IDE环境下使用调试器
5. 在calibre的Python环境中执行任意脚本

8. 在你的项目中使用calibre

1. 二进制安装calibre

2. Linux源码安装

9. calibre丰富的API文档

1. 清单的API文档

1. BasicNewsRecipe

2. 插件API文档

1. 插件
2. 文件类型插件
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)

1. DRM对我个人意味着什么？

2. DRM为作者做了什么？

3. DRM与自由

4. 为什么Calibre不支持DRM？

5. Calibre对内容提供商的看法是什么？

6. 我该如何帮助对抗DRM？

词汇表