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

ナビゲーション

- モジュール
- 次へ |
- 開始 »
- calibre ユーザーマニュアル

calibre ユーザーマニュアル
calibre は電子書籍のライブラリ管理ソフトです。ほとんどの主要な電子書籍で閲覧、コンバート、分類することができます。また多くの電子書籍リーダー端末に対応しています。インターネットに接続して書籍の書籍情報を取得することができます。ネットニュースをダウンロードして、読みやすい電子書籍に変換することができます。calibre はクロスプラットフォームで Linux、Windows、macOS で動作します。

今から calibre を使い始めます。何から始めましょうか？電子書籍を扱う前に知っておくべきことがあります。ドラッグ＆ドロップか「本を追加」をクリックして電子書籍を参照してください。書籍を追加すると、このようなものがメインビューに表示されます :

追加した本がリストに表示されている様子を、心ゆくまで堪能したあとは、きっとその本を読みたくなるでしょう。そのためには、本のフォーマットを、リーダーが対応するフォーマットに変換する必要があります。最初に calibre を起動した際に、ウェルカムウィザードが開始し、そこでリーダーデバイスの設定が行われます。本の変換はとても簡単です。変換したい本を選択して、「本を変換」ボタンをクリックします。とりあえず今のところは、オプションをすべて無視して、「OK」をクリックしましょう。すると右下隅のアイコンがくるくる回転しだします。回転が止まったら、変換完了です。「表示」ボタンをクリックして本を読みましょう。

リーダーで本を読みたい場合は、リーダーをコンピュータに接続し、calibre が検出するまで待ってから(10〜20秒)、「デバイスに送信」ボタンをクリックしてください。再びアイコンの回転が止まるのを待ってからリーダーを切断して、お読みください。前に示した手順で本を変換しなかった場合には、calibre はリーダーデバイスが扱える形式に自動的に変換します。

もう少し進んだな使い方をするためには、グラフィカルユーザインタフェース のページからはじめるとよいでしょう。さらに高度で多様な機能は、コマンドライン・インタフェース で学べます。また、よくある質問 も役に立つでしょう。

さらに質問がある場合や、他のユーザと calibre について議論したり、助けを求めたいことがある場合には、 フォーラムなどを利用してください [https://calibre-ebook.com/ja/help]

セクション

- グラフィカルユーザインタフェース
- お気に入りのニュースサイトを追加
- 電子書籍ビューア
- 電子書籍変換
- 電子書籍の編集
- calibre コンテンツサーバー
- 電子書籍を比較
- 電子書籍の書誌の編集
- よくある質問
- チュートリアル
- The calibre:// URL スキーム
- calibreをカスタマイズ
- コマンドライン・インタフェース
- calibre 開発環境の構築

- デジタル著作権管理 (Digital Rights Management, DRM)

- 用語

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- グラフィカルユーザインタフェース

グラフィカルユーザインタフェース

グラフィカルユーザインタフェース (GUI) は、すべてのライブラリ管理および電子書籍形式の変換機能へのアクセスを提供します。calibre を使用するための基本的なワークフローは、最初にハードディスクからライブラリに本を追加することです。 calibre は自動的に本から書誌情報を読み取り、内部データベースに追加します。データベースに登録された後は、それに対してさまざまな 動作 を実行できるようになります。たとえばある形式から別の形式への変換、読み取りデバイスへの転送、コンピュータ上での表示、書誌情報の編集などです。書誌の編集には、表紙、説明、タグの変更が含まれます。 ただし calibre は、追加したファイルのコピーを作成します。元のファイルは変更されません。

インタフェースは、いくつかの章に分割されています:

- アクション

- 環境設定

- カタログ

- 検索と並べ替え

- 検索インタフェース

- 検索の保存

- Searching the full text of all books

- 仮想ライブラリ

- 一時的に本をマーク

- ファイル名から書誌を推測

- 本の詳細

- タグブラウザ

- 表紙グリッド

- 表紙ブラウザ

- Adding notes for authors, series, etc.

- クイックビュー

- ジョブ

- キーボード ショートカット

アクション

アクションツールバーは、よく使われているアクションへの便利なショートカットを提供します。ボタンを右クリックすると、デフォルトのアクションのバリエーションを実行できます。ただしコンピュータに電子書籍リーダーが接続されているかどうかによって、アクションツールバーの外観すこし異なります。

- 本を追加

- 書誌を編集

- 本を変換

- 表示

- デバイスに送信

- ニュースを取得

- ライブラリ

- デバイス

- ディスクに保存

- 接続/共有

- 本を削除

本を追加

本を追加 アクションには、右ボタンをクリックして実行できる 7 種類のものがあります。

1. 単一のフォルダから本を追加: ファイル選択ダイアログが開かれ、フォルダ内のどの本を追加するかを指定できます。このアクションは 状況依存 です。つまり選択した カタログ によって動作が変わります。たとえば ライブラリ を選択した場合、選択した本はそのライブラリに追加されます。電子書籍リーダデバイスを選択した場合は、本がデバイスにアップロードされる、といった具合です。

2. フォルダとサブフォルダから本を追加: フォルダを選択できます。選択したフォルダとそのすべてのサブフォルダが再帰的にスキャンされ、見つかった電子書籍がライブラリに追加されます。 calibreに、単一のフォルダに存在するすべてのファイルを単一の本レコードとして追加するか、複数の本レコードとして追加するかを選択できます。calibre は、各フォルダがそれぞれ 1 冊の本だと想定しています。フォルダ内のすべての電子書籍ファイルは、異なる形式の同じ本と見なされます。このアクションは ディスクに保存 アクションと逆の働きをします。つまり ディスクに保存 し、本を削除して、フォルダごとに 1 冊の本モードで再追加しても、日付以外の情報は何も失われません(ただしディスクに保存アクションの設定を何も変更していないことが前提です)。

3. アーカイブ (ZIP/RAR) から複数の本を追加: 選択した ZIP または RAR ファイル内に保存されている複数の電子書籍を追加できます。これは便利なショートカットで、上記 2 種類のオプションのいずれかを使用して本を追加する前にアーカイブを解凍する手間が省けます。

4. 空の本を追加 (形式のない本): 空白の本レコードを作成できます。これを使用すれば、まだ本がコレクションに含まれていなくても手動で情報を入力できます。

5. ISBN から追加: ISBNを入力して、1 冊以上の本を追加できます。

6. 選択した本にファイルを追加: ライブラリ内の既存の本に関連付けられているファイルを追加または更新できます。

7. Add data files to selected book records: Allows you to add any number of extra files that will be stored in a data sub-directory in the book directory. See Adding extra data files to a book for details.

8. 選択した本に空のファイルを追加: 指定した形式の空のファイルを選択した本レコードに追加できます。

本を追加 アクションでは、さまざまな電子書籍形式から書誌情報を読み取ることができます。さらに、ファイル名から書誌情報を推測しようとします。この機能の設定方法については、ファイル名から書誌を推測 の章を参照してください。

既存の本に形式を追加するには、次の 3 つの方法があります:

1. メインウィンドウの右側にある本の詳細パネルにファイルをドラッグ&ドロップ

2. 本を追加 ボタンを右クリックして 選択した本にファイルを追加 を選択

3. 書誌を編集 からアクセスできる 書誌を編集 ダイアログの右上にある 本を追加 ボタンをクリック

書誌を編集

書誌を編集 アクションには、右クリックして実行できる 4 種類のものがあります。

1. 書誌を個別に編集: 本の書誌を1つずつ編集できます。オプションで、インターネットから表紙を含む書誌を取得できます。また、特定の電子書籍形式を本に追加したり削除したりできます。

2. 書誌を一括編集: 多数の本に共通な書誌フィールドを同時に編集できます。ライブラリビュー で選択した本すべてに対して操作を実行します。

3. 書誌と表紙をダウンロード: 本のリストから選択した本の書誌と表紙 (あれば) をダウンロードします。

4. 本レコードをマージ: 複数の本レコードの書誌と形式をマージできます。後でクリックしたレコードを削除するか、保持するかを選択できます。

5. Manage data files: Manage the extra data files associated with the selected books.

詳しくは 電子書籍の書誌の編集 を参照してください。

本を変換

電子書籍は、さまざまな形式から電子書籍リーダに適した形式に変換できます。購入可能な電子書籍の多くは、 デジタル著作権管理 (DRM) 技術によって保護されています。 calibre がこのような電子書籍を変換することはありません。多くの形式からDRMを削除するのは簡単ですが、これは違法である可能性があります。本を解放できるツールを自分で見つけてから、calibreを使用して変換する必要があります。

ほとんどの人にとって変換は、ワンクリックするだけの簡単なお仕事です。変換処理について詳しくは 電子書籍変換 を参照してください。

本を変換 アクションには、右ボタンをクリックして実行できる 3 種類のものがあります。

1. 個別に変換: 変換オプションを指定して、選択した本の変換をそれぞれカスタマイズできます。

2. 一括変換: オプションを 1 回指定するだけで、多数の電子書籍を一括変換できます。

3. calibre ライブラリの本からカタログを作成: すべての書誌を含む、ライブラリ内の本の完全なリストを生成し、XML、CSV、BiBTeX、EPUB、MOBIなどのいくつかの形式で作成できます。このカタログに含まれるのは、作成時にライブラリビューに表示されているすべての本です。したがって検索機能を使用することにより、カタログ化する本を絞り込むことも可能です。さらにマウスで複数の本を選択した場合には、選択した本だけがカタログに追加されます。 EPUB、MOBI、AZW3などの電子書籍形式でカタログを生成すると、次に電子書籍リーダを接続したときに、カタログがデバイスに自動的に送信されます。カタログの動作について詳しくは、AZW3 • EPUB • MOBI のカタログを作成 を参照してください。

表示

表示 アクションは、本を電子書籍ビューアプログラム内に表示します。calibre には多くの電子書籍形式に対応したビルトインのビューアが備わっています。他の形式には、OS のデフォルトアプリケーションを使用します。どの形式に対して内部ビューアを使用するかは、環境設定 → インタフェース → 動作 で設定できます。書籍に複数の形式がある場合、特定の形式を表示するにはボタンを右クリックします。

デバイスに送信

デバイスに送信 アクションには、右ボタンをクリックして実行できる 8 種類のものがあります。

1. メインメモリに送信: 選択した本を電子書籍リーダのメインメモリに送信します。

2. カード (A) に送信: 選択した本を電子書籍リーダのストレージカード (A) に送信します。

3. カード (B) に送信: 選択した本を電子書籍リーダのストレージカード (B) に送信します。

4. 特定の形式を送信: 選択した本を指定した形式で、デバイス上の選択したストレージに送信します。

5. デバイスを取り出す: デバイスを calibre から取り外します。

6. デフォルトのデバイス送信を設定: メインボタンをクリックしたときにどのオプションをデフォルトとするかを指定できます。選択肢は上記の 1 から 5 までと下記の 7 です。

7. 送信してライブラリから削除: 選択した本をデバイスの選択したストレージに転送し、ライブラリから **削除 します。

8. 注釈を取得 (実験的): デバイス上で作成した注釈を、calibre ライブラリの本のコメント書誌として転送します。

デバイスに送信したファイルの名前とフォルダ構造を制御するには、環境設定 → インポート/エクスポート → デバイスへの送信 でテンプレートを設定します。

ニュースを取得

ニュースを取得 アクションは、さまざまな Web サイトからニュースをダウンロードし、電子書籍リーダで読めるよう電子書籍に変換します。通常、新しく作成した電子書籍は電子書籍ライブラリに追加されますが、ダウンロードの終了時に電子書籍リーダが接続されている場合は、自動的にニュースもリーダにアップロードされます。

ニュースを取得 アクションは、それぞれのニュースサイトに対して簡単なレシピ (コードにして 10 〜 15 行) を使用します。自分のニュースソース用のレシピの作り方については、お気に入りのニュースサイトを追加 を参照してください。

ニュースを取得 アクションには、右ボタンをクリックして実行できる 3 種類のものがあります。

1. ニュースの取得スケジュール: 数百もの利用可能なリストの中からニュースソースを選択し、ダウンロードを予約することが可能です。予約は選択したニュースソースごとに設定できます。予約は柔軟性が高く、指定した曜日にダウンロードしたり、何日おきにダウンロードするかを指定したりできます。

2. カスタムニュースソースを追加: アクセスしたいカスタムニュースサイトからニュースをダウンロードするために、簡単なレシピを作成できます。RSSニュースフィードの URL を指定するだけで簡単にレシピを作成することもできますし、Python ベースのコードを作成して細かく指定することもできます。詳しくは お気に入りのニュースサイトを追加 を参照してください。

3. スケジュールしたすべてのニュースソースをダウンロード: スケジュールを設定したすべてのニュースソースを calibre に即座にダウンロードさせます。

ライブラリ

ライブラリ アクションを使用すると、ライブラリを作成したり、切り替えたり、名前を変更したり、削除したりできます。 calibre ではいくつでも必要なだけライブラリを作成できます。たとえばフィクションライブラリ、ノンフィクションライブラリ、外国語ライブラリ、プロジェクトライブラリなど、必要性に応じて任意の構造を作成できます。ライブラリは calibre の中で最も系統立てて作られた構造物です。本、タグ、カテゴリ、およびストレージ場所はライブラリごとに別々です。

1. ライブラリを切替え/作成: 次のことが可能です。 a) 別の場所にある既存の calibre ライブラリに接続する、b) 新しい場所に空のライブラリを作成する、c) 現在のライブラリを新しく指定した場所に移動する。

2. クイック切替え: calibre 内で登録されたか作成されたライブラリの間を切り替えることができます。

3. ライブラリ名を変更: ライブラリの名前を変更できます。

4. ランダムに本を選択: ライブラリ内の本をランダムに選択できます。

5. ライブラリを削除: calibre からライブラリを登録解除できます。

6. すべての calibre データをエクスポート/インポート: 新しいコンピュータに移動するために calibre のデータをエクスポートしたり、以前エクスポートされたデータをインポートすることができます。

7. <ライブラリ名>: アクション 7、8 などは、作成または接続した複数のライブラリ間で、アクセスをすぐに切り替えます。このリストには、よく使われるライブラリが最大 5 個まで登録されます。すべてのリストを見るには、クイック切替えメニューを使用してください。

8. ライブラリを保守: 現在のライブラリにデータの整合性の問題がないか確認し、バックアップから現在のライブラリのデータベースを復元できます。

注釈

電子書籍の書誌、たとえばタイトルや、著者や、タグなどは、calibre ライブラリフォルダ内に置かれている metadata.db という名前のファイルに保存されています。このファイルが破損すると (滅多にありません) 書誌が失われる可能性があります。しかし幸いにして calibre は、本ごとに書誌を自動的にバックアップし、それぞれの本のフォルダ内に OPF ファイルとして保存します。上で説明したように、ライブラリの保守の下にあるデータベースの復元アクションを使用すれば、calibre は個々の OPF ファイルから metadata.db ファイルを再構築してくれます。

本を別のライブラリとの間で (2 個以上のライブラリを構築したら) コピーしたり移動することもできます。本の上で右クリックして ライブラリにコピー を選択してください。

デバイス

デバイス アクションを使用すると、デバイスのメインメモリやストレージカード内にある本を表示したり、デバイスを取り出したり (calibre から取り外したり) することができます。このアイコンは、サポートされているデバイスを接続すると、メインの calibre ツールバーに自動的に表示されます。アイコンをクリックすれば、デバイス上の本が表示されます。本をライブラリからアイコンにドラッグ&ドロップすると、デバイスに転送されます。逆に、本をデバイスからツールバーのライブラリアイコンにドラッグ&ドロップすれば、デバイスから calibre ライブラリに本を転送することができます。

ディスクに保存

Save to disk アクションには、右ボタンをクリックして実行できる 5 種類のものがあります。

1. ディスクに保存: 選択した本をフォルダに整理されたディスクに保存します。フォルダ構造は次のようになります:

Author_(sort)
Title
Book Files

ディスクに保存されるファイルのファイル名とフォルダ構造は 環境設定 → インポート/エクスポート → ディスクに保存 でテンプレートを設定することにより制御できます。calibre テンプレート言語 も参照してください。

2. ディスク上のフォルダに保存: 選択した本をディスク上の単一のフォルダに保存します。

For 1. and 2., all available formats, as well as metadata, are stored to disk for each selected book. Metadata is stored in an OPF file. Saved books can be re-imported to the library without any loss of information by using the Add books action.

3. ** <優先する> 形式のみディスクに保存**：選択した書籍を (1.) で示したフォルダ構造でディスクに保存します。ただし保存するのは優先する形式の本だけです。優先する形式は 環境設定 → インタフェース → 動作 → 優先する出力形式 で設定できます。

4. ** <優先する> 形式のみディスク上のフォルダに保存**：選択した書籍を (1.) で示したフォルダ構造でディスク上の単一フォルダに保存します。ただし保存するのは優先する形式の本だけです。優先する形式は 環境設定 → インタフェース → 動作 → 優先する出力形式 で設定できます。

5. 指定形式をディスクに保存…: 選択した書籍を (1.) で示したフォルダ構造でディスクに保存します。ただし保存するのはポップアップリストから選択した形式の本だけです。

接続/共有

接続/共有 アクションを使用すると、コンピュータ上のデバイスまたはフォルダに手動で接続できます。Web ブラウザや電子メール経由でアクセスできるよう、calibre ライブラリを設定することも可能です。

接続/共有 アクションには、右ボタンをクリックして実行できる 4 種類のものがあります。

1. フォルダに接続: コンピュータ上の任意のフォルダにデバイスであるかのように接続し、calibre がデバイスに使えるすべての機能をそのフォルダに対して使用できます。デバイスが calibre にサポートされていないが、USB ディスクとしてなら利用可能な場合に役に立ちます。

2. コンテンツサーバを開始: calibre のビルトイン Web サーバを開始します。開始すると、(選択すれば) インターネットからWebブラウザを介して calibre ライブラリにアクセスできるようになります。 Web サーバへのアクセス方法は 環境設定 → 共有 → ネット共有 で設定できます。

3. メールでの共有を設定: 電子メールを使った本とニュースフィードの共有ができます。このオプション用の電子メールアドレスを設定した後、calibre は入力された電子メールアドレス宛てにニュースの更新と本の更新を送信します。calibre がメール送信する方法は 環境設定 → 共有 → メール共有 で設定できます。 1 つ以上の電子メールアドレスを設定すると、このメニューは、設定されたメールアドレスに書籍を送信するためのメニューに置き換わります。

本を削除

本を削除 アクションは 本を完全に削除する ため、十分に注意して使用してください。このアクションは状況依存です。つまり、選択している カタログ によって動作が変わります。ライブラリ を選択している場合、本はライブラリから削除されます。電子書籍リーダデバイスを選択している場合、本はデバイスから削除されます。指定した書籍の特定の形式のみを削除するには、書誌を編集 アクションを使用します。本の削除には、ボタンを右クリックして

1. 選択した本を削除: 本のリスト内で選択した本を 完全に 削除できます。

2. 選択した本から指定した形式のファイルを削除: 本のリスト内で選択した本の指定した形式の電子書籍を 完全に 削除できます。

3. 選択した本のすべての形式を (一部除外して) 削除...: 本のリスト内で選択した本から、指定した形式を除いてすべての電子書籍ファイルを 完全に 削除できます。

4. 選択した本のすべての形式を削除: 本のリスト内で選択した本から、すべての電子書籍ファイルを 完全に 削除できます。

5. 選択した本から表紙を削除: 本リスト内で選択した本から表紙画像ファイルを 完全に 削除できます。

6. デバイスから一致する本を削除: 本のリスト内で選択した本と一致する電子書籍を、接続中のデバイスから削除できます。

7. Restore recently deleted: Allows you to undo the removal of books or formats.

注釈

Note that when you use Remove books to delete books from your calibre library, the book record is deleted, but the books are temporarily stored, for a few days, in a trash folder. You can undo the delete by right clicking the Remove books button and choosing to Restore recently deleted books.

環境設定

環境設定 アクションを使用すると、calibre のさまざまな側面での動作方法を変更可能です。このアクションには、右ボタンをクリックして実行できる 4 種類のものがあります。

1. 環境設定: calibre の動作をさまざまな側面から変更できます。ボタンをクリックしてもこのアクションを実行します。

2. ウェルカムウィザードを開始: calibre が初めて開始されたときに現れる ウェルカムウィザード を開始できます。

3. calibreを拡張するプラグイン取得: calibre のプラグインを表示する新しいウィンドウを開きます。プラグインは calibre の機能を拡張するためにサードパーティによって開発されています。

4. デバッグモードで再起動: デバッグモードを有効にして、プログラムで発生した問題を解決するにあたり calibre の開発者を支援できます。ほとんどのユーザーにとっては開発者から有効にするよう指示されない限り、無効しておくべきものです。

カタログ

カタログ とは本のコレクションのことです。calibre は 2 種類のカタログを扱うことができます:

1. ライブラリ: コンピュータ上の calibre ライブラリに格納されている、本のコレクション。

2. デバイス: 電子書籍リーダに格納されている本のコレクション。リーダをコンピュータに接続すると利用できるようになります。

操作の多くは、たとえば本の追加、削除、表示などは、状況依存です。したがって、たとえば、 デバイスの カタログを選択しているときに 表示 ボタンをクリックすると、calibre はそのデバイス上のファイルを開いて表示します。 ライブラリの カタログを選択している場合は、代わりに calibre ライブラリ内のファイルが開かれます。

検索と並べ替え

検索と並べ替えのセクションでは、本のコレクションに対していくつか強力なアクションを実行できます。

- 列のタイトルをクリックすると、タイトル、著者、日付、評価などで並べ替えることができます。サブソート、つまり複数の列を使って並べ替えることもできます。たとえば、タイトル列をクリックしてから著者列をクリックすると、本は著者名順に並べ替えられ、同じ著者のすべてのエントリがタイトル順に並べ替えられます。

- 検索バーを使用して、特定の本または本のセットを検索できます。詳細については後述します。

- 変更したいものをリスト上で選択して E キーを押すと、書誌をすばやく簡単に編集できます。

- 本のセットに対して:ref:`アクション`を実行できます。複数の本を選択するには、次のいずれかのようにします:

- kbd:Ctrl キーを押したまま選択した本をクリックします。

- Shift キーを押したまま、選択したい範囲の最初の本と最後の本をクリックします。

- 設定 ダイアログを使って表示したい列を設定できます。

- To perform complex multiple column based sub-sorting add the Sort by tool to a toolbar via Preferences → Toolbars & menus.

検索インタフェース

検索バーに検索語を入力すると、すべての書誌を検索できます。例えば:

Asimov Foundation format:lrf

これは書誌に``Asimov`` and Foundation があり、LRF形式で利用できるライブラリ内のすべての本とマッチします。その他の例:

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

検索の種類

検索には 4 種類があります: 部分一致、完全一致、正規表現 (正規表現 <https://en.wikipedia.org/wiki/Regular_expression>`_を参照)、および `異形文字 です。検索の種類はプレフィックス文字で選択します。

部分一致検索

Searches with no prefix character are contains and are by default case insensitive. An item matches if the search string appears anywhere in the indicated metadata. You can make contains searches case sensitive by checking the option Case sensitive searching in Preferences / Searching. If the search option Unaccented characters match accented characters and punctuation is ignored is checked then a character will match all its variants (e.g., e matches é, è, ê, and ë) and all punctuation and whitespace are ignored. For example, if the Unaccented characters match ... option is checked then given the two book titles:

1. Big, Bothéred, and Bad

2. Big Bummer

検索は次のようになります:

- title:"er" は両方にマッチします ('e' は 'é' と 'e' の両方にマッチ)。

- title:"g " は空白を無視するので両方にマッチします。

- title:"g," はカンマを無視するので両方にマッチします。

- title:"gb" は 1 番目の本で ', ' を無視し、2 番目の本で空白を無視するため両方にマッチします。

- title:"g b" はカンマと空白を無視するので両方にマッチします。

- title:"db" は 'and Bad' にある空白を無視するため 1 番目にマッチします。

- title:"," はカンマを無視するので両方にマッチ (実際にはすべての本にマッチ) します。

アクセントのない文字を... オプションにチェックされていない場合、異形文字、句読点、および空白はすべて比較対象となります。

検索オプションには 大文字と小文字を区別 と アクセントのない文字をアクセントつきの文字に一致するとみなし、句読点を無視 のどちらか一方しか指定できません。

完全一致検索

等式検索は、検索文字列の前に等号 (=) を付けることで示されます。たとえば、クエリ tag:"=science" は "science" とはマッチしますが、"science fiction" や "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:

- tags:"=.History" : Trueです。History はタグのプレフィックスです。

- tags:"=.History.Military" : True です。``History.Military``はタグのプレフィックスです。

- tags:"=.History.Military.WWII" : True です。History.Military.WWII は不適切ではありますが、タグのプレフィックスです。

- tags:"=.Military" : Falseです。Military はタグのプレフィックスではありません。

- tags:"=.WWII" : Falseです。WWII はタグのプレフィックスではありません。

- tags:"=..History" : True です。階層には History の値が含まれています。

- tags:"=..Military" : True です。階層には Military の値が含まれています。

- tags:"=..WWII" : Trueです。 階層には WWII の値が含まれています。

- tags:"=..Military.WWII" : False です。.. 検索は 1 つの値しか見ません。

正規表現検索

Regular expression searches are indicated by prefixing the search string with a tilde (~). Any Python-compatible regular expression [https://docs.python.org/library/re.html] can be used. Backslashes used to escape special characters in regular expressions must be doubled because single backslashes will be removed during query parsing. For example, to match a literal parenthesis you must enter \\( or alternatively use super-quotes (see below). Regular expression searches are 'contains' searches unless the expression is anchored. Character variants are significant: ~e doesn't match é.

異形文字検索

異形文字検索は、検索文字列の前にキャレット (^) を付けることで示されます。この検索は、次の点を除いて (上述した) 部分一致検索と似ています:

- 大文字/小文字は常に無視されます。

- 異形文字は互いにマッチします。

- punctuation and whitespace are always significant.

検索オプション アクセントのない文字をアクセントつきの文字に一致するとみなし、句読点を無視 および 大文字と小文字を区別 は無視されます。この検索の動作には影響しません。

この検索を、アクセントのない文字を... オプション (上記を参照) がチェックされていると仮定して部分一致検索と比較してみましょう。上と同じ次の 2 つの本が与えられたとします:

1. Big, Bothéred, and Bad

2. Big Bummer

異形文字検索は次のようになります:

- title:"^er" は両方にマッチします ('e' は 'é' および 'e' の両方にマッチ)

- title:"^g" は両方にマッチします

- title:"^g " は空白を識別するため 2 番目にマッチします。

- title:"^g," はカンマを識別するため 1 番目にマッチします

- title:"^gb" は空白とカンマを識別するためマッチするものがありません

- title:"^g b" はカンマを識別するため 2 番目にマッチします

- title:"^db" にマッチするものはありません

- title:"^," はカンマを識別するので 1 番目の本に (すべての本の代わりに) マッチします

Search Expression Syntax

A search expression is a sequence of search terms optionally separated by the operators and and or. If two search terms occur without a separating operator, and is assumed. The and operator has priority over the or operator; for example the expression a or b and c is the same as a or (b and c). You can use parenthesis to change the priority; for example (a or b) and c to make the or evaluate before the and. You can use the operator not to negate (invert) the result of evaluating a search expression. Examples:

- not tag:foo finds all books that don't contain the tag foo

- not (author:Asimov or author:Weber) finds all books not written by either Asimov or Weber.

The above examples show examples of search terms. A basic search term is a sequence of characters not including spaces, quotes ("), backslashes (\), or parentheses (( )). It can be optionally preceded by a column name specifier: the lookup name of a column followed by a colon (:), for example author:Asimov. If a search term must contain a space then the entire term must be enclosed in quotes, as in title:"The Ring". If the search term must contain quotes then they must be escaped with backslashes. For example, to search for a series named The "Ball" and The "Chain", use:

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

If you need an actual backslash, something that happens frequently in regular expression searches, use two of them (\\).

It is sometimes hard to get all the escapes right so the result is what you want, especially in regular expression and template searches. In these cases use the super-quote: """sequence of characters""". Super-quoted characters are used unchanged: no escape processing is done.

その他の情報

先頭に等号、チルダ、または挿入記号がついた文字列を検索したい場合には、前にバックスラッシュをつけてください。

文字列にカッコまたはスペースが含まれている場合は、検索文字列を引用符 (") で囲みます。たとえば Science Fiction というタグを検索したい場合には tag:"=science fiction" と検索する必要があります。tag:=science fiction を検索すると science というタグを持ち、書誌に fiction が含まれているすべての本が見つかります。

検索ボックスの左側にある 高度な検索ダイアログ ボタン を使用すると、高度な検索クエリを簡単に作成できます。

検索に使用できるフィールドは tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl およびカスタム列です。デバイスが接続されていると、calibre ライブラリビューの検索時に ondevice フィールドが利用可能になります。カスタム列の検索名 (正式には ルックアップ名 と呼ばれます) を見つけるには、ライブラリビューの中にある列の見出し部分の上にマウスを合わせます。

日付

日付を検索する構文は次のとおりです:

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のとき、2/1/2009 は2009 年 2 月 1 日と解釈されます。ロケールが dd/mm/yyyy ならば 2009 年 1 月 2 日と解釈されます。一部の特殊な日付文字列が使用可能です。文字列 today は、今日の日付に変換されます。文字列 yesterday および thismonth (または現在の言語で翻訳された同等の文字列) も機能します。さらに文字列 daysago を使用して、数日前の日付と比較することができます。例えば:

date:>10daysago
date:<=45daysago

非英語版の calibre を使用する場合に文字列が翻訳されてしまうという潜在的な問題を回避するために、文字列 _today, _yesterday, _thismonth, および _daysago は常に使用可能です。これが翻訳されることはありません。

日付と数値を比較演算子を用いて検索

日付と数値フィールドは、関係演算子 = (等号), > (大なり), >= (以上), < (小なり), <= (以下), および != (不等号) をサポートします。評価フィールドは数値と見なされます。たとえば rating:>=3 と検索すると、評価が 3 以上のすべての本が検索されます。

特定のサイズの形式の本を検索するには、次のようにします:

- size:>1.1M は 1.1 MB を超える形式の本を探します

- size:<=1K はサイズが 1KB 以下の形式の本を探します

タグのような複数値フィールドで項目数を検索できます。この検索は文字 # で始め、続いて数値フィールドと同じ構文を使用します。たとえばタグが 4 個を超える本を検索するには、tags:#>4 を使用します。正確に 10 個のタグが付いた本を検索するには、tags:#=10 を使用します。

シリーズインデックス

シリーズインデックスは検索可能です。標準的なシリーズの場合、検索名は series_index です。カスタムシリーズ列では、列検索名の後に _index をつけます。たとえば、#my_series という名前のカスタムシリーズ列のインデックスを検索するには、検索名 #my_series_index を使用します。シリーズ列インデックスは数値ですから、上述した関係演算子を使用できます。

保存済み検索

特殊なフィールド search は 保存済み検索 で使用されます。"私の配偶者の本" という名前で検索を保存した場合には、検索バーに search:"私の配偶者の本" と入力して、保存した検索を再利用できます。検索の保存について詳しくは、下に説明します。

仮想ライブラリ

The special field vl is used to search for books in a Virtual library. For example, vl:Read will find all the books in the Read Virtual library. The search vl:Read and vl:"Science Fiction" will find all the books that are in both the Read and Science Fiction virtual libraries. The value following vl: must be the name of a
Virtual library. If the Virtual library name contains spaces then surround it with quotes.

フィールドに値があるか

フィールドの値の有無は、特殊な値 "true" および "false" を使用して検索できます。例えば:

- cover:false は表紙のない本を探します

- series:true シリーズになっている本を探します

- series:false シリーズでない本を探します

- comments:false finds all books with an empty comment

- formats:false は本のファイルのない本を探します (空のレコード)

Yes/no カスタム列

Yes/no カスタム列で false, empty, または blank を検索すると、その列に未定義の値を持つすべての本が見つかります。 true を検索すると、列に未定義の値がないすべての本が見つかります。yes または checked を検索すると、列に Yes が含まれる本が検索されます。 no または unchecked を検索すると、列に No が含まれる本が検索されます。ただし yes, no, blank, empty, checked および unchecked という単語は翻訳されます。現在の言語の同等の単語、または英単語のいずれかを使用できます。単語 true と false、および特殊な値 _yes, _no, および _empty は翻訳されません。

ID

識別子 (例: ISBN, DOI, LCCN) は拡張構文を使用します。識別子は isbn:123456789 のように、type:value の形式であることに注意してください。拡張構文を使用すると、検索するタイプと値を個別に指定できます。クエリのタイプ部分と値部分の両方で、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 が含まれている本を探します。

Categories visible in the Tag browser

The search in_tag_browser:true finds all books that are in categories
(tags, authors, etc.) currently shown in the Tag browser. This is useful if you set the two preferences Preferences → Look & feel → Tag browser → Hide empty categories and Find shows all items that match. With those two preferences set, doing a Find in the Tag browser shows only categories containing items matched by the Find. Then, the search in_tag_browser:true additionally finds books with these categories / items. You can easily run this search by pressing the key Ctrl+Alt+Shift+F or clicking the configure button in the Tag browser and choosing the Show only books that have visible categories entry.

テンプレートを使った検索

書誌フィールドの代わりに、calibre テンプレート言語 のテンプレートを使って検索することができます。それには、テンプレート、検索タイプ、および検索する値を入力します。構文は次のようになります:

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

template は、calibre のテンプレート言語で作られた何らかの有効なテンプレートです。search type は t (テキスト検索)、d (日付検索)、n (数値検索)、または b (設定/未設定 (ブーリアン)) のいずれかを指定します。value には何か検索したいものを指定します。さまざまな検索タイプに対して、上述した 検索の種類 を使用できます。検索文字列内にスペースが含まれている場合は、検索文字列全体を引用符で囲む必要があります。

例:

- template:"program: connected_device_name('main')#@#:t:kindle" -- kindle デバイスが接続されていれば true になります。

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" -- 1MB を超える EPUB ファイルの本を探します。

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" -- 10 日前よりも新しい EPUB ファイルの本を探します。

- template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" -- finds all books containing the series name in the tags. This example uses super-quoting because the template uses both single quotes (') and double quotes (") when constructing the search expression.

テンプレート検索クエリは ボタンをクリックして 高度な検索ダイアログ を使って簡単に作成できます。calibre テンプレートテスター を使えば特定の本に対してテンプレートをテストできます。これは 環境設定 → ツールバーメニュー を使ってツールバーやメニューに簡単に追加可能です。環境設定 → ショートカット からショートカットに割り当てることも可能です。

高度な検索ダイアログを開く

高度な検索ダイアログ

検索の保存
caliber では、頻繁に使用する検索に特別な名前をつけて保存すると、その検索を 1 回クリックするだけで再利用できます。これを行うにはまず、検索バーに入力するか、タグブラウザを使用するかして検索を作成します。次に、検索バーの横にある保存済み検索ボックスに、その検索に付ける名前を入力します。そして保存済み検索ボックスの横にあるプラスアイコンをクリックし、検索を保存します。

これで、タグブラウザの 保存済み検索 から保存済み検索が利用できるようになります。クリック 1 回で、任意の複雑な検索を簡単に再利用できます。もう一度作る必要がなくなるのです。

Searching the full text of all books
calibre can optionally index the full text of books in the library to allow for instant searching of words inside any book. To use this functionality click the FT button at the left edge of the search bar.

Then, enable indexing for the current library. Once indexing is complete you can search all the text in the full library. When you add new books to the library, they will be automatically indexed in the background. This search allows you to quickly find all books that contain a word or combination of words. You can even search for words that occur near other words, as shown in the examples in the search popup window. Note that this search tool will find only one occurrence of the search query in a particular book, not list every occurrence, for that it is best to search inside the book using the calibre E-book viewer.

You can re-index an individual book by right clicking on the Book details panel in calibre and choosing Re-index this book for full text searching.

仮想ライブラリ

仮想ライブラリ は、calibre のライブラリがコレクション全体の代わりに、ほんの数冊しか本を持っていないふりをする方法です。これは、大量の本のコレクションを小さくて管理しやすいまとまりに分割するための優れた方法です。仮想ライブラリを作成して使用する方法については、チュートリアル 仮想ライブラリ を参照してください。

一時的に本をマーク

You can temporarily mark arbitrary sets of books. Marked books will have a pin on them and can be found with the search marked:true. To mark a book press Ctrl+M or go to Preferences → Toolbars & menus and add the Mark books button to the main toolbar.

本をマーク を右クリックして ラベル付きで本をマーク を選択して、数種類のマークを作成することもできます。入力したテキストラベルは marked:"=入力したテキストラベル" とすると後で検索できます。

ファイル名から書誌を推測

通常、calibre は本のファイル内から書誌を読み取ります。が、代わりにファイル名から書誌を読み取るようにも設定できます。これは 環境設定 → インポート/エクスポート → 本の束 → 書誌をファイル内容から読み込む で設定します。

ファイル名から書誌を読み取る方法を、正規表現 (calibre で正規表現を利用するにあたって重要なこと を参照) を使用して制御することもできます。環境設定ダイアログの 本の セクションで、ライブラリに追加する電子書籍ファイルの名前から書誌を推測するために calibre が使用する正規表現を指定できます。デフォルトの正規表現は次のとおりでです:

title - author

つまり、最初の - までのすべての文字が本のタイトルであり、後続の文字が本の著者であることを前提としています。たとえば、ファイル名が次のとき:

Foundation and Earth - Isaac Asimov.txt

上のファイル名は、タイトル: Foundation and Earth 著者: Isaac Asimov と解釈されます。

Tip

ファイル名にハイフンが含まれていないと、上の正規表現はうまくいきません。

本の詳細

本の詳細画面には、現在選択されている本の表紙とすべての書誌が表示されます。本の詳細は非表示にすることもできます。それにはメインの calibre ウィンドウの右下端にある レイアウト ボタンを使用します。 本の詳細パネルに表示される著者名はクリック可能です。デフォルトでは、著者のWikipediaページに移動します。これは、著者名を右クリックしてこの著者の管理を選択することでカスタマイズできます。

同様に本の書誌をダウンロードすると、本の詳細パネルに自動的にリンクが表示され、リンク先は Amazon、WorldCat などの本の書誌のダウンロード元となります。

本の詳細パネルで電子書籍の形式を右クリックすると、削除したり、元のバージョンと比較したり、ディスクに保存したり、外部プログラムで開いたりすることができます。

本の表紙は、本の詳細パネルに画像をドラッグ&ドロップするだけで変更できます。外部プログラムで表紙画像を編集する場合は、表紙画像を右クリックして アプリケーションを選択して開く を選択します。

ファイルをブックの詳細パネルにドラッグ＆ドロップして、現在の本に電子書籍ファイルを追加することもできます。

本の詳細パネルの上でダブルクリックすると、別のポップアップウィンドウで開きます。

最後に、本の詳細パネルに表示する情報は 環境設定 → インタフェース → 外観 → 本の詳細 でカスタマイズできます。

タグブラウザ

タグブラウザを使えば、著者/タグ/シリーズ別にコレクションを簡単に閲覧できます。タグブラウザで著者名、たとえば Isaac Asimov をクリックすると、右側の本のリストにはその著者の本だけが表示されます。カテゴリ名をクリックしても同様です。たとえばシリーズをクリックすれば、そのシリーズの本がすべて表示されます。

項目をクリックすると 1 回目のときには、本のリストにその項目を含むか一致するだけが表示されます。上に挙げた例にならえば Isaac Asimovをクリックすると、その著者の本が表示されます。項目をもう一度クリックしたときには、その項目に子があるか次第で表示される内容が変わります (後述のサブカテゴリと階層項目を参照)。 Isaac Asimov の例でいくと、 Isaac Asimov をもう一度クリックすると、本のリストに Isaac Asimov 以外のものだけが表示されます。3 回目のクリックで条件が解除され、再びすべての書籍が表示されます。 Ctrl または Shift キーを押しながら複数の項目をクリックすると、選択した項目に基づいて表示が制限されます。たとえば、Ctrl キーを押しながら「歴史」と「ヨーロッパ」のタグをクリックすると、ヨーロッパの歴史に関する本を探すことができます。タグブラウザは、自動的に検索バーに検索式を生成しながら機能します。タグブラウザがどのようなものを生成するのか調べてみると、基本的な検索式の作り方がよく学べるでしょう。

タグブラウザに表示される項目のアイコンは、部分的に色付けされています。色の量は、そのカテゴリの本の平均評価によって変わります。したがって、たとえば Isaac Asimov の本の評価が平均して星 4 の場合、タグブラウザで Isaac Asimov のアイコンは 4/5 の部分が色付けされます。平均評価の値は、アイコンの上にマウスを置くと表示されます。

タグブラウザ にある外側の階層の項目、たとえば著者やシリーズなどは、カテゴリと呼ばれます。自分でカテゴリを作成することも可能です。自分で作成したものは、ユーザカテゴリ と呼ばれます。これは項目を整理するのに役に立ちます。たとえば、ユーザカテゴリエディタ (タグブラウザ の左下にある 設定 ボタンをクリックし、著者、タグ等を管理 → ユーザカテゴリ を選択) を使用して、お気に入りの著者 というユーザーカテゴリを作成し、お気に入りをそのカテゴリに入れておくことができます。ユーザカテゴリにはサブカテゴリを作れます。たとえば、ユーザーカテゴリ お気に入り.著者` は、``お気に入り のサブカテゴリです。また お気に入り.シリーズ もあるかもしれません。その場合には お気に入り の下に 2 つのサブカテゴリが存在することになります。サブカテゴリを作成するにはユーザカテゴリを右クリックし、...にサブカテゴリを追加`を選択して、サブカテゴリ名を入力します。または上述したお気に入りの例と同様にに名前を入力して、:guilabel:`ユーザカテゴリエディタ を使用します。

ユーザカテゴリはビルトインカテゴリと同様に、クリックして検索ができます。クリックすると 4 種類の検索が繰り返されます:

1. 緑色のプラス記号 1 個で示される「カテゴリ内で項目にマッチするすべて」

2. 緑色のプラス記号 2 個で示される「カテゴリ内またはサブカテゴリ内で項目にマッチするすべて」

3. 赤いマイナス記号 1 個で示される「カテゴリ内で項目にマッチしないものすべて」

4. 赤いマイナス記号 2 個で示される「カテゴリとそのサブカテゴリ内で項目にマッチしないものすべて」

また、タグ、シリーズ、カスタム列などの一部のテキストカテゴリ内に階層を作ることも可能です。このような階層は小さな三角形で表示され、下の階層を隠しておくことができます。カテゴリ内で項目の階層を利用するには、環境設定 → インタフェース → 外観 で "階層化するカテゴリ項目" フィールドにカテゴリ名を入力しておく必要があります。これが完了すると、ピリオドが含まれているカテゴリの項目が、小さな三角形を使って表示されるようになります。たとえば「ジャンル」というカスタム列を作成し、階層カテゴリに指定したとします。それが完了すると、「推理小説.スリラー」や「推理小説.英語」などの項目が、横に小さな三角形が付いた推理小説として表示されます。三角形をクリックすると、サブ項目としてスリラーと英語が表示されます。詳しくは 本のサブグループ、たとえば "ジャンル" の管理 を参照してください。

階層項目 (子を持つ項目) は、ユーザカテゴリと同様に 4 種類の「クリックオン」検索を使用します。子供がいない項目は「マッチするものすべて」と「マッチしないものすべて」の 2 種類の検索を使用します。

タグブラウザにある項目をユーザカテゴリにドラッグ&ドロップすると、そのカテゴリに追加することができます。元の場所がユーザカテゴリであれば、Shift キーを押しながらドラッグすると、その項目は新しいカテゴリに移動します。本の一覧からタグブラウザの項目に本をドラッグ&ドロップすることもできます。項目に本をドロップすると、その項目がドロップされた本に自動的に適用されます。たとえば、ある本を Isaac Asimov にドラッグすると、その本の著者が Isaac Asimov に設定されます。「歴史」タグにドロップすると「歴史」が本のタグに追加されます。

You can easily find any item in the Tag browser by clicking the search button at the lower-right corner. In addition, you can right click on any item and choose one of several operations. Some examples are to hide it, rename it, or open a "Manage x" dialog that allows you to manage items of that kind. For example, the Manage authors dialog allows you to rename authors and control how their names are sorted.

タグブラウザの左下にある 設定 ボタンを使用すると、タグブラウザでの項目を並び順を制御できます。名前、平均評価、または冊数の順で表示することが可能です (冊数は、ライブラリ内にあるその項目を含む本の数です。たとえば、Isaac Asimovの冊数は、著者が Isaac Asimov となっているライブラリ内の本の数です)。

表紙グリッド

本のコレクションを表紙で見たい場合は、本の一覧の代わりに本の表紙を calibre にグリッド表示させることができます。メインの calibre ウィンドウ右下端にある レイアウト ボタンをクリックすると、表紙グリッドが有効になります。表紙サイズと背景は 環境設定 → インタフェース → 外観 → 表紙グリッド でカスタマイズできます。フィールドを指定して calibre にタイトル、作者、評価、カスタム列などを表紙の下部に表示させることも可能です。

表紙ブラウザ

上述した 表紙グリッド に加えて、表紙を一列に並べて calibre に表示させることもできます。これを有効にするには、メインウィンドウの右下端にある レイアウト ボタンを使用します。環境設定 → インタフェース → 外観 → 表紙ブラウザ で、表示する表紙の数を変更できます。表紙ブラウザ自体を別のポップアップウィンドウに表示することも可能です。

Adding notes for authors, series, etc.

You can add notes for an author/series/tag/publisher/etc. to your calibre library. To do so right click on the author name in the Tag browser on the left or the Book details panel on the right and choose Create note or Edit note.

A simple popup window will allow you to enter your notes using basic formatting and supporting links and images. Once a note for an author is created, it can be viewed easily from the Book details panel by clicking the little pencil icon next to the author name.

You can search through all the notes in your library using the Browse notes tool by pressing Ctrl+Shift+N or adding it to the toolbar via Preferences → Toolbars & menus.

クイックビュー

ライブラリの現在のビューを変更することなく、選択した本とカテゴリ (著者、タグ、出版社、シリーズなど) が同じ本を手早く一覧にしたい場合があります。クイックビューを使えば、それが可能です。クイックビューは、本の一覧を表示する 2 つ目のウィンドウまたはパネルを開き、興味のある値に一致する本の一覧を表示します。たとえば、現在選択している本と著者が同じ本をすべて一覧で表示したいとします。表示したい著者セルをクリックして 'Q' キーを押すか、calibre ウィンドウのレイアウトセクションにある クイックビュウー アイコンをクリックします。そうするとウィンドウまたはパネルが開き、左側にその本のすべての著者が表示され、右側には選択した著者によるすべての本が表示されます。

クイックビューの利用例: 次のような本を手早く一覧にできます:

- 現在選択中の本と同じタグを持つ本

- 現在の本と同じシリーズの本

- 現在の本とカスタム列の値が同じ本

- 現在の本の著者のひとりによって書かれた本

- カスタム列の値を共有する本

クイックビューの情報を表示する場所には、次の 2 つの選択肢があります:

1. "分離" で開く: calibre ウィンドウの上に開き、明示的に閉じるまで開いたままになります。

2. "結合" で開く: calibre のメインウィンドウの下にパネルで表示します。

"結合/分離" ボタンを使用すれば、ウィンドウを結合したり分離したりできます。

クイックビューパネルは、開いたままにすることが可能です。その場合には、本の一覧の動きに従います。たとえば calibre のライブラリビューで、ある本のカテゴリ列 (タグ、シリーズ、出版社、著者など) をクリックすると、クイックビューの左側のパネルに選択した本のそのカテゴリの値 (例: その本のタグ) が一覧表示されます。一覧の一番上の値が選択されて、クイックビューの右側のパネルにはその値を使用している本が一覧表示されます。左側のパネルで別の値を選択すると、その値に一致する本が表示されます。

クイックビューウィンドウ上で本をダブルクリックすると、ライブラリビューでその本を選択します。それにより新しく選択された本に合わせて、クイックビューウィンドウの (左側のパネル) に表示されるものも変化します。

クイックウィンドウにある本の上で Shift または Ctrl キーを押しながらダブルクリックすると、メインの calibre ウィンドウ上でその本の書誌編集ダイアログを開きます。書誌の編集ダイアログを閉じると、編集された本のクイックビューが表示されます。

列見出しの上にマウスを置いてその見出しのツールチップを確認すると、その列をクイックビューできるかどうかがわかります。列見出しを右クリックしたときに「クイックビュー」オプションがメニューに表示されるかどうかでもわかります。ここでクイックビューオプションを選択すると、現在のセルで 'Q' を押すのと同じ動きをします。

オプション (環境設定 → 外観 → クイックビュー):

- 仮想ライブラリを適用する (またはしない)。チェックすると、クイックビューは現在の仮想ライブラリ内の本のみを表示します。デフォルト: 仮想ライブラリを適用

- カーソルキーを使って本の一覧上で列が変更されたとき、クイックビューの内容を変更。デフォルト: カーソルキーで行われた変更には従わない

- クイックビューウィンドウのセルがダブルクリックされたときに、クイックビューする列を変更。変更しない場合は、本は変更しますが、クイックビューの対象とする列は変更しません。デフォルト: 列を変更

- クイックビューパネルでリターンキーが押されたとき、クイックビューする列を現在の列に変更。変更しない場合は、クイックビューの対象とする列は変更しません。デフォルト: 列を変更

- クイックビューウィンドウ/パネルに表示する列を選択。

ジョブ

ジョブパネルには、現在実行中のジョブの数が表示されます。ジョブとは、別のプロセスとして実行されるタスクのことです。これには、電子書籍の変換やリーダデバイスとの対話が含まれます。ジョブパネルをクリックすると、ジョブの一覧を表示できます。ジョブが完了すると、一覧でそのジョブをダブルクリックして詳しいログを表示できるようになります。これは、正常に完了しなかった可能性のあるジョブのデバッグに役立ちます。

キーボード ショートカット
calibre has several keyboard shortcuts to save you time and mouse movement. These shortcuts are active in the book list view (when you're not editing the details of a particular book), and most of them affect the title you have selected. The calibre E-book viewer has its own shortcuts which can be customised in the viewer Preferences.

注釈

注意: calibre のキーボードショートカットは、特に記されていない限り修飾キー (コマンドキー、オプションキー、コントロールキーなど) を必要としません。文字キーを押すだけです。例: E を押して編集

メインの calibre プログラムのキーボードショートカット

キイーボードショートカット

アクション

F2 (Enter for macOS)

本の一覧で選択中のフィールドの書誌を編集

A

本を追加

Shift+A

選択した本に形式を追加

C

選択した本を変換

D

デバイスに送信

Del

選択した本を削除

E

選択した本の書誌を編集

G

本を入手

I

本の詳細を表示

K

目次の編集

M

選択したレコードをマージ

Alt+M

選択したレコードをマージし、元のレコードを保持

O

本のあるフォルダを開く

P

本の最適化

S

ディスクに保存

T

書籍編集

V

表示

Shift+V

最後に読んだ本を表示

Alt+V/Cmd+V for 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+右矢印

次の仮想ライブラリタブを選択

Ctrl+左矢印

前の仮想ライブラリタブを選択

N または F3

現在の検索にマッチする次の本を探す (検索設定で検索ハイライトが有効のときのみ機能)

Shift+N または Shift+F3

現在の検索にマッチする前の本を探す (検索設定で検索ハイライトが有効のときのみ機能)

Ctrl+D

書誌と表紙をダウンロード

Ctrl+R

calibre を再起動

Ctrl+Shift+R

calibre をデバッグモードで再起動

Shift+Ctrl+E

calibre に空の本を追加

Ctrl+M

選択した本をマーク/マーク解除

Ctrl+/ or Ctrl+Alt+F

Open the popup to search the full text of all books in the library

Q

関連したシリーズ/タグなどの本を表示するクイックビューポップアップを開く

Shift+Q

開いているクイックビューパネルにフォーカス移動

Shift+S

クイックビューパネルで検索を実行

F5

現在の並べ替えを再適用する

Ctrl+Q

calibre を終了

X

本の一覧の自動スクロールを有効/無効

Ctrl+Alt+Shift+F

Restrict the displayed books to only those books that are in a category currently displayed in the Tag browser

B

Browse annotations (highlights and bookmarks) made in the calibre viewer for all books in the library

Ctrl+Shift+N

Browse notes associated with authors/series/tags/etc.

Alt+Shift+L

Toggle the layout between wide and narrow views

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- お気に入りのニュースサイトを追加

お気に入りのニュースサイトを追加
calibre は、インターネットからニュースをダウンロードして電子書籍に変換するための、柔軟かつ強力で使いやすいフレームワークを備えています。下では例を用いて、さまざまな Web サイトからニュースを取得する方法を説明します。

フレームワークの使い方を理解するには、下に並んでいる順に例を追ってください:

- 完全自動の取得

- calibre ブログ

- bbc.co.uk

- 取得処理のカスタマイズ

- bbc.co.uk のプリント用バージョンを使用

- 記事のスタイルを置換

- スライスとダイス

- 実例

- 新しいレシピを開発するコツ

- 参考文献

- API 取扱説明書

完全自動の取得

もしニュースソースが単純なものなら、calibre は完全に自動で取得することができます。必要なのは URL を与えることだけです。calibre はニュースソースのダウンロードに必要な情報を レシピ に集めています。calibre にニュースソースについての情報を与えるためには、レシピ を作る必要があります。では、いくつか例を見てみましょう。

calibre ブログ
calibre ブログには、calibre の新規ユーザが簡単に情報を入手できるよう、calibre の多くの便利な機能を説明する記事が置かれています。このブログをダウンロードして電子書籍にするのに、ブログの RSS フィードを頼っています。

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

RSS URL はブログページ下部の "Subscribe to" の下にある Posts → Atom を選択します。calibre にフィードをダウンロードして電子書籍に変換させるには、ニュースを取得 を右クリックしてメニュー項目の カスタムニュースソースを追加 を選択し、新規レシピ ボタンを押します。下に示すものと似たようなダイアログが表示されます。

まず レシピのタイトル フィールドに Calibre Blog と入力します。これは上記のフィードの記事から作成される電子書籍のタイトルとなります。

次のふたつのフィールド (一番古い記事 および 記事の最大数) を使って、各フィードからどれだけの記事をダウンロードするかを制御できます。意味は名前のとおりです。

フィードをレシピに追加するには、フィードのタイトルと URL を入力して フィードを追加 ボタンをクリックします。フィードを追加したら 保存 ボタンをクリックすれば完了です。ダイアログを閉じてください。

レシピ をテストするには、ニュースを取得 ボタンをクリックして、サブメニュー カスタムニュースソース の中から calibre Blog をクリックします。数分後、新しくダウンロードしたブログ記事の電子書籍がメインのライブラリビューに (もしリーダを接続していれば、ライブラリの代わりにリーダ上に) 現れます。それを選択して 表示 ボタンを押せば、読めます。

ほとんど手間をかけることなくこれがうまく機能する理由は、ブログが フルコンテンツの RSS フィードを提供しているためです。つまり、記事内容がフィード自体に埋めこまれているのです。ニュースをこの形、すなわち フルコンテンツの フィードで提供するほとんどのニュースソースでは、電子書籍に変換するのにこれ以上の手間は必要ありません。では、フルコンテンツのフィードを提供していないニュースソースについて見てみましょう。そのようなフィードには、記事の全文は Web ページにあり、フィードには短い要約とともにその Web ページへのリンクのみが含まれています。

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 ブログ で概説した手順に沿って The BBC 用のレシピを (上のフィードを使って) 作成します。ダウンロードした電子書籍を見ると、calibre はそれぞれの記事の Web ページから読みたい記事だけを抽出する仕事を立派にこなしていることがわかります。しかし、その抽出の処理は完全ではありません。メニューやナビゲーションなどの望ましくないコンテンツがそのまま残っていたり、記事の見出しのような残しておくべきコンテンツを削除してしまっていることがあります。コンテンツの抽出を完璧に行うには、次の章で説明するように取得処理をカスタマイズする必要があります。

取得処理のカスタマイズ

ダウンロード処理を完璧にしたいときや、特に複雑な Web サイトからコンテンツをダウンロードしたいときこそ、レシピ`フレームワークの威力と柔軟性が役立ちます。これには :guilabel:`カスタムニュースソースを追加 ダイアログで 上級者モードに切替え をクリックします。

最も簡単で、かつ多くの場合最も生産的なカスタマイズは、オンライン記事の印刷用バージョンを使うことです。印刷用バージョンは通例、よけいなものがはるかに少なく、電子書籍への変換もスムーズに行えます。The BBC の記事の印刷用バージョンを使ってみましょう.

bbc.co.uk のプリント用バージョンを使用

最初のステップは、以前 bbc.co.uk からダウンロードした電子書籍を確認することです。各記事の末尾に、電子書籍ではその記事がどこからダウンロードされたものなのかを示す小さな記載があります。その URL をブラウザにコピーして貼り付けてください。では、記事の Web ページ上で "Printable version" へのリンクを探しましょう。それをクリックして記事の印刷用バージョンを見てみます。こちらの方がずっときれいです。さて、ふたつの 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 コードに簡単な方法で変換されていることがわかります。このレシピに、記事のプリント用バージョンを利用するよう指示を加える必要があります。必要なのは、次の 2 行を追加することです:

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/] を参照してください。

次に、保存 ボタンを押すと変更が保存されます。電子書籍を再ダウンロードしてください。電子書籍はかなり改善されているはずです。この新しいバージョンの問題点のひとつは、印刷用バージョンの Web ページのフォントは小さすぎることです。これは電子書籍への変換時に自動的に修正されますが、修正処理の後でも、メニューとナビゲーションバーのフォントサイズは記事のテキストと比較して大きくなりすぎます。これを修正するには、次の章で説明するようにさらにカスタマイズが必要です。

記事のスタイルを置換

前の章では、The BBC の印刷用バージョンの記事のフォントサイズが小さすぎることを確認しました。The BBC を含めほとんどの Web サイトでは、このフォントサイズを CSS スタイルシートを使って設定しています。そのようなスタイルシートの取得は、次の行を加えることでやめさせられます:

no_stylesheets = True

レシピは次のようになります:

新しいバージョンは大変よくなりました。完全主義者であれば、ダウンロードしたコンテンツを実際に変更することを扱う次の章を読むことをお勧めします。

スライスとダイス

ダウンロードしたコンテンツの処理にかけては、calibre には非常に強力で柔軟性のある機能を持っています。いくつか紹介するために、もはやお馴染みとなった The BBC レシピをもう一度見てみましょう。いくつかの記事 (印刷用バージョン) のソースコード (HTML) を見ると、下で示すフッタ部分には有用な情報が含まれていないことがわかります。

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

これはレシピに次を追加することで削除できます:

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

最後に、以前無効にした CSS の一部を、電子書籍への変換に適した自前の CSS に置き換えましょう。

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

こうした追加によって、レシピは「製品レベルの品質」になりました。

この レシピ は calibre の威力に関する氷山の一角に触れたにすぎません。calibre の能力にさらに触れるため、次の章ではより複雑な実例を取り上げます。

実例

BasicNewsRecipe の API を体験できる適度に複雑な実例は、The New York Times の レシピ です。

import string, re from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup class NYTimes(BasicNewsRecipe):

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

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

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

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

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

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

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

この レシピ にはいくつか新しい機能があります。まず、次のものがあります:

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

これは作成する電子書籍のとびらに日付を 曜日, 日 月, 年 の形式で表示します。timefmt を参照してください。

そして、ダウンロードした HTML をきれいにするための指令のかたまりがあるのがわかります。

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

これは最初の <h1> の前にあるすべてと、id が footer である最初のタグの後ろのすべてを削除します。remove_tags, remove_tags_before, remove_tags_after を参照してください。

次の興味深い機能は次のものです:

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

needs_subscription = True はコンテンツにアクセスするためにユーザ名とパスワードが必要であることを calibre に伝えます。これにより、calibre はこのレシピを使うたびにユーザ名とパスワードを要求します。calibre.web.feeds.news.BasicNewsRecipe.get_browser() の中のコードで、実際に NYT の Web サイトにログインしています。ログインすると、同じログイン済みのブラウザインスタンスを使用してすべてのコンテンツを取得します。get_browser のコードを理解するには mechanize [https://mechanize.readthedocs.io/en/latest/] を参照してください。

次の新しい機能は calibre.web.feeds.news.BasicNewsRecipe.parse_index() メソッドです。これ仕事は https://www.nytimes.com/pages/todayspaper/index.html へ行き、 今日の 新聞に掲載されている記事のリストを取得することです。RSS を使用するだけと比べると複雑ですが、このレシピはその日の新聞に非常に近い電子書籍を作成します。parse_index は日刊紙の Web サイトを解析するのに 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 コマンドは、すべての Web ページをダウンロードして myrecipe.epub という EPUB ファイルに保存します。-vv オプションを使うと、最大 2 つのフィードから数記事のみをダウンロードします。さらに、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 のビルトインレシピに含めるに十分なだけの需要があると感じた場合には、 calibre レシピフォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=228] に投稿して他のユーザと共有してください 。

注釈
macOS では、コマンドラインツールは calibre バンドルの中にあります。たとえば calibre を /Applications にインストールしたのであれば、コマンドラインツールは /Applications/calibre.app/Contents/console.app/Contents/MacOS/ にあります。

参考

ebook-convert

すべての電子書籍変換用のコマンドラインインタフェース。

参考文献

BasicNewsRecipe` を利用可能な機能を利用して上級レシピを作成する方法をさらに学ぶには、下のソースを参照してください:

API 取扱説明書

BasicNewsRecipe クラスと、すべての重要なメソッドおよびフィールドについての取扱説明書です。

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

BasicNewsRecipe のソースコード

ビルトインレシピ [https://github.com/kovidgoyal/calibre/tree/master/recipes]

calibre に付属しているビルトインレシピのソースコード

calibre レシピフォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=228]

知識豊富なレシピ作者の多くがここにたむろしています。

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

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- お気に入りのニュースサイトを追加 »
- レシピの API 説明書

レシピの API 説明書

BasicNewsRecipe で定義されるレシピを書くための API です
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はスレッドセーフではないため、クローンブラウザはマルチスレッドダウンロードに使用されます。デフォルトのクローン作成手順は、ほとんどのブラウザのカスタマイズをキャプチャするはずですが、レシピで何か変わったことを行う場合には、レシピでこのメソッドをオーバーライドして、手動でクローンを作成する必要があります。

クローンブラウザインスタンスは、Cookieの処理をカスタマイズしていない限り、デフォルトで同じスレッドセーフな CookieJar を使用します。

default_cover(cover_file)[ソース]

表紙のないレシピ用に汎用的な表紙を作成します

download()[ソース]

このレシピのフィードからすべての記事をダウンロードして前処理します。このメソッドは、特定の Recipe インスタンスで 1 回だけ呼び出す必要があります。これを複数回呼び出すと、予期しない動作が発生します。返り値: index.html へのパス

extract_readable_article(html, url)[ソース]

'html' からメインの記事コンテンツを抽出し、クリーンアップして (article_html, extracted_title) タプルとして返します。 Arc90によるoriginal readability アルゴリズムに基づいています。

get_article_url(article)[ソース]

サブクラスでオーバーライドして、各記事のコンテンツを指す URL の抽出をカスタマイズします。記事の URL を返します。これは、フィードから解析された記事を表すオブジェクトである article とともに呼び出されます。`feedparser <https://pythonhosted.org/feedparser/>`_を参照してください。デフォルトでは元のリンク (FeedBurner や Pheedo などのサービスを介して配信されたフィードの場合) を検索し、見つかった場合はそれを返し、そうでなければ `article.link <https://pythonhosted.org/feedparser/reference-entry-link.html>`_を返します。

get_browser(*args, **kwargs)[ソース]

Webからドキュメントをフェッチするために使用されるブラウザインスタンスを返します。デフォルトでは、Cookieをサポートし、robots.txtを無視し、更新を処理し、mozilla firefoxユーザエージェントを持つ `mechanize <https://mechanize.readthedocs.io/en/latest/>`_ブラウザインスタンスを返します。

レシピで最初にログインする必要がある場合は、サブクラスでこのメソッドをオーバーライドします。たとえば、New York Times のレシピでは、フルアクセスのためのログイン用に次のコードが使用されています:

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 、または None を返します。デフォルトではメンバー self.cover_url の値を返し、これは通常 None です。レシピで電子書籍のマストヘッドをダウンロードしたい場合は、サブクラスでこのメソッドをオーバーライドするか、このメソッドが呼び出される前にメンバ変数 self.cover_url を設定します。マストヘッド画像はKindleMOBIファイルで使用されます。

get_obfuscated_article(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.

この方法は通常、記事のコンテンツに自動的にアクセスすることを困難にしようとするサイトに役立ちます。

get_url_specific_delay(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.

戻り値:

A floating point number, the delay in seconds.

classmethod image_url_processor(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.

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()[ソース]

このメソッドは、フィードの代わりにWebサイトを解析して記事のリストを生成するレシピに実装する必要があります。典型的な用途は、現在の印刷版のすべての記事を一覧表示する "印刷版" の Web ページがあるニュースソースです。この関数を実装すると BasicNewsRecipe.parse_feeds() よりも優先して使用されます。

リストを返す必要があります。リストの各要素は、('feed title', list of articles) の形式の 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 をダウンロードするためのレシピを参照してください。さらに、記事の著者に 'author' を追加できます。

何らかの理由で処理を中止し、calibre にエラーではなく単純なメッセージをユーザに表示させたい場合は、abort_recipe_processing() を呼び出します。

populate_article_metadata(article, soup, first)[ソース]

記事に属する各 HTML ページがダウンロードされるときに呼び出されます。解析された HTML (soup) から著者/要約のような記事の書誌を取得するための使用が想定されています。

パラメータ:

- 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 -- ダウンロードした HTML を含む BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] インスタンス

- first_fetch -- これが記事の最初のページなら True。

preprocess_html(soup)[ソース]

このメソッドは、リンクや画像を解析する前に、ダウンロードした各 HTML ファイルのソースで呼び出されます。 remove_tags などで指定されたクリーンアップの後に呼び出されます。HTML で任意の強力な前処理を行うために使用できます。処理後に soup を返します。

soup: ダウンロードされた HTML を含む BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] インスタンス

preprocess_image(img_data, image_url)[ソース]

ダウンロードした画像データに対して何らかの処理を行います。これは、サイズ変更が行われる前に生データで呼び出されます。処理された生データを返す必要があります。画像をスキップするためには None を返します。

preprocess_raw_html(raw_html, url)[ソース]

このメソッドは、オブジェクトツリーに解析される前に、ダウンロードされた各 HTML ファイルのソースで呼び出されます。 raw_html は、Web からダウンロードされた生の HTML を表すUnicode文字列です。 url は、HTML のダウンロード元の URL です。

このメソッドは preprocess_regexps の*前*に機能します。

このメソッドは、処理された raw_html を unicode オブジェクトとして返す必要があります。

classmethod print_version(url)[ソース]

記事のコンテンツを含むWebページを指す 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 は適用済みです。これは、レシピが広告ページをスキップできるようにすることを目的としています。soup が広告ページを表していれば、実際のページの HTML を返し、そうでなければ None を返します。

soup: ダウンロードされた HTML を含む BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] インスタンス

sort_index_by(index, weights)[ソース]

index 内のタイトルを` weights` に従って並べ替える便利なメソッド。index はその場で並べ替えられます。index を返します。

index: タイトルのリスト。

weights: 重みをタイトルにマップする辞書。インデックス内のタイトルに重みがない場合、重みは 0 と見なされます。

classmethod tag_to_string(tag, use_alt=True, normalize_whitespace=True)[ソース]

BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_の :code:`Tag を取得し、CDATA セクションと alt タグ属性を含むテキストを再帰的に抽出する便利なメソッドです。空の可能性のあるUnicode文字列を返します。

use_alt: Trueのとき、テキストコンテンツを含まないタグに alt 属性を使用してみてください
tag: BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/]
Tag

articles_are_obfuscated = False

True に設定すると、get_obfuscated_article() を実装して、コンテンツのスクレイプを困難にしようとする Web サイトを処理します。

auto_cleanup = False

ダウンロードした記事ページからすべてのテキストを自動的に抽出します。readability プロジェクトのアルゴリズムを使用します。これを 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

画像が指定されたサイズ (KB単位) を超えないようにJ PEG 品質を設定します。設定されると、このパラメーターは 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') は、左右に 10px、上下に 15px の白い余白でカバーを埋めます。色名は こちら [https://www.imagemagick.org/script/color.php] で定義されています。ただし何らかの理由により Windows では white が常に機能するとは限りません。代わりに #ffffff を使用してください

delay = 0

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

description = ''

このレシピがダウンロードするコンテンツの 2、3 行の説明。これは主に、レシピのリストを表示する GUI で使用されます。

encoding = None

文字セットの指定が正しくないサイト用にオーバーライドエンコーディングを指定します。最も一般的なのは latin1 を指定して cp1252 を使用することです。None のときには、エンコーディングを検出してみてください。呼び出し可能であれば、それは 2 つの引数、レシピオブジェクトとデコードされるソースで呼び出されます。デコードされたソースを返す必要があります。

extra_css = None

ダウンロードした HTML ファイルに追加する必要がある追加の CSS を指定します。終了タグ </head> の直前の <style> タグに挿入されるため、個々の HTML タグの style 属性を使用して宣言されているものを除くすべての CSS がオーバーライドされます。ただしプログラムで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 が含まれているすべてのURLを削除します。

BasicNewsRecipe.match_regexps または BasicNewsRecipe.filter_regexps のいずれか1つのみを定義する必要があります。

handle_gzip = True

Set to False if you do not want to use gzipped transfers. Note that some old servers flake out with 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'])]

上記は "content" または "heading" の id 属性を持つタグのみを保持します。

language = 'und'

ニュースの言語。2 文字または 3 文字の長さの ISO-639 コードである必要があります

masthead_url = None

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.

match_regexps = []

フォローするリンクを決定する正規表現のリスト。空の場合、無視されます。 is_link_wanted が実装されていない場合にのみ使用されます。例:

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

上記は page=some number を含むすべての URL に一致します。

BasicNewsRecipe.match_regexps または BasicNewsRecipe.filter_regexps のいずれか1つのみを定義する必要があります。

max_articles_per_feed = 100

各フィードからダウンロードする記事の最大数。これは主に、記事の日付がないフィードに役立ちます。ほとんどのフィードでは、BasicNewsRecipe.oldest_article を使用する必要があります

needs_subscription = False

Trueのとき、ダウンロード中に使用するユーザ名とパスワードを GUI がユーザに要求します。 "オプション" に設定すると、ユーザ名とパスワードの使用はオプションになります

no_stylesheets = False

電子書籍形式への変換に適さない過度に複雑なスタイルシートを持つ Web サイトのスタイルシートの読み込みを無効にするための便利なフラグ。 True なら、スタイルシートをダウンロードして処理しません。

oldest_article = 7.0

このニュースソースからダウンロードする最も古い記事。数日で指定します。

preprocess_regexps = []

ダウンロードした HTML で実行する regexp 置換ルールのリスト。リストの各要素は、2 要素のタプルである必要があります。タプルの最初の要素はコンパイルされた正規表現であり、2 番目の要素は単一の一致オブジェクトを受け取り、マッチを置き換える文字列を返す呼び出し可能オブジェクトである必要があります。例:

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

<!--Article ends here--> から </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 または :meth:`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

指定されたタグの前にあるすべてのタグを削除します。タグを指定するための形式については attr: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 に設定すると、特定の Web サイトの URL スキームを操作するために canonicalize_internal_url() を実装する必要がある場合もあります。

reverse_article_order = False

各フィードの記事の順序を逆にします

scale_news_images = None

画像を拡大縮小する最大サイズ (w,h)。 scale_news_images_to_device が Trueのとき、プロファイルが設定されていない限り、出力プロファイルによって設定されたデバイス画面のサイズに設定されます。プロファイルが設定されていない場合は、割り当てられている値のままになります (デフォルトはNone)。

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

通常、埋め込まれたコンテンツの長さに基づいて、フィードに完全な記事が埋め込まれているかどうかを推測しようとします。 None であれば、デフォルトの推測が使用されます。 True ならば、フィードには常にコンテンツが埋め込まれていると見なされ、False ならば、フィードにはコンテンツが埋め込まれていないと常に見なされます。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍ビューア

電子書籍ビューア
calibre には主要な電子書籍形式をすべて表示可能なビルトインの電子書籍ビューアが付属しています。この電子書籍ビューアはいろいろなカスタマイズが可能で、高度な機能をたくさん備えています。

- 電子ビューアの開始

- 電子書籍のナビゲーション

- テキストをハイライト

- 読み上げ

- Searching the text

- キーボード操作だけでリンクを開く

- 本の外観と操作感をカスタマイズ

- 辞書検索

- テキストや画像をコピー

- 画像を拡大

- キーボード ショートカット

- 非リフロー型のコンテンツ

- calibre ビューア用に本をデザイン

電子ビューアの開始
calibre ライブラリの本は、本を選択して 表示 ボタンをクリックすれば読むことができます。こうすると電子書籍ビューアで本を開きます。Windows のスタートメニューから電子書籍ビューアを直接起動することもできます。macOS では Dock に追加してそこから起動することができます。Linux ではデスクトップメニューのランチャーから、またはコマンド ebook-viewer で利用できます。

電子書籍のナビゲーション

本の「ページめくり」は次のいずれかの方法で行えます:

- 左または右の余白、またはページの上でマウスクリック

- spacebar キー、page up、page down または矢印キーを押す

- タッチスクリーン上でテキストををタップ、または左か右へスワイプ

ビューアコントロールは次のいずれかの方法で表示できます:

- テキスト上で右クリック

- Esc キーまたは Menu キーを押す

- タッチスクリーン上で上から 1/3 の場所をタップ

ビューアには「ページ」と「フロー」というふたつのモードがあります。ページモードでは本の内容は紙の本のようにページごとに表示されます。フローモードでは Web ブラウザでの表示のようにテキストが連続的に表示されます。このふたつのモードを切り替えるには、設定 の下の ページレイアウト へ行くか、Ctrl+M キーを押下します。

ブックマーク

本の途中で電子書籍ビューアを終了するとき、ビューアはどこまで読んだかを記憶し、次にその本を開いたときにそのページから開始します。またビューアコントロールにある ブックマーク ボタンを使うか Ctrl+B キーを押すことにより、本の中にブックマークを設定することもできます。EPUB 形式の本を読むときには、このブックマークは EPUB ファイル自体の中に保存されます。ブックマークを追加して、そのファイルを友人に送ることも可能です。受け取った人がファイルを開くと、保存されたブックマークを見ることができます。この動作は、ビューア設定の その他 セクションで無効にすることができます。

目次

読んでいる本に目次が定義されていれば、目次 ボタンを押して開くことができます。こうすると本の中の章の一覧が表示されます。一覧の上でクリックすると、どこでもその章の位置へ飛ぶことができます。

場所を指定した移動

電子書籍は紙の書籍と異なり、ページという概念がありません。電子書籍内の正確な位置はビューアコントロールにある 移動  →  場所 機能を使って参照できます。

この位置情報を使えば、友人と本の内容について議論するときや、仕事で内容を参照する場合などに正確な位置を参照することができます。こうした場所は、ビューアコントロールの 移動  →  場所 に入力できます。

クリップボードにコピーしたり、他のプログラムやドキュメント内に貼り付けることのできる URL があります。この URL の上でクリックすると calibre 電子書籍ビューアでその本の現在の位置を開きます。

電子書籍の中にあるリンクをクリックして本の中の別の場所、たとえば文末脚注などへ移動したとき、ビューアコントロールの左上にある 次へ や 前へ ボタンを使うことができます。これらのボタンは Web ブラウザの同名のボタンと同じように動作します。

参照モード

また calibre には便利な 参照モード があります。ビューアコントロールにある 参照モード をクリックすると有効にできます。これを有効にすると、最初に割り振られた番号がすべての段落の上に表示されます。この番号は章番号と段落番号の組み合わせでできており、その段落を一意に特定するものです。

この番号を使えば、友人と本の内容について議論するときや、仕事で内容を参照する場合などに正確な位置を参照することができます。この番号はビューアコントロールの 移動機能 に入力して、特定の参照位置へ移動することができます。

テキストをハイライト

ビューアの中でテキストを選択すると、選択範囲の隣に小さなポップアップバーが現れます。このバーの中のハイライトボタンをクリックするとハイライトを作成できます。ハイライトにはメモをつけたり、色を変更したりすることができます。タッチスクリーンでは選択したい単語の上でタップを長押しするとポップアップバーが表示されます。ハイライトモードに入ると、タッチスクリーンで使いやすい選択ハンドルを使って、選択したテキスト範囲を変更できます。また Shift+クリック や 右クリック でも選択範囲を広げられます。ページをまたがった選択をしたいときに便利です。

ビューアコントロールにある ハイライト ボタンを使うと別のパネルで章順に整列したハイライトの一覧を表示できます。

表示 を右クリックして 注釈を表示 を選択すると、calibre ライブラリにある すべてのハイライト を参照できます。

最後に、calibre のコンテンツサーバのブラウザビューアを使用すると、ビューアと注釈を同期することができます。注釈を同期するにはビューア設定の 環境設定  →  その他 でコンテンツサーバビューアと同期したいユーザ名を入力します。特殊な値 * を使用すると匿名ユーザと同期します。

読み上げ

ビューアは本を読み上げることができます。ビューアコントロールにある Read aloud ボタンをクリックするだけで本のテキストを読み上げ始めます。読み上げられている単語はハイライト表示されます。読み上げの音声は OS の読み上げ機能を使って合成されます。使用される音声は、Read aloud の実行中に表示されるバーの中の歯車アイコンをクリックして選択できます。

ハイライトされた部分を読み上げるには、読み上げ ボタンをビューア設定にある 選択動作 で選択バーに追加します。

注釈

ブラウザでのテキスト読み上げは非常に不完全でバグが多いため、読み上げ がどれほどうまく動作するかはもととなるブラウザがテキスト読み上げをどれだけしっかりサポートしているかによって変わります。特に読み上げ中の単語のハイライトがうまく動作しなかったり、速さや音声を変更すると最初から読み上げ直したりすることがあります。

注釈

Linux では 読み上げ を利用するには Speech Dispatcher [https://freebsoft.org/speechd] がインストールされて動作するようになっている必要があります。

注釈

Windows では、テキスト読み上げに使われている SAPI サブシステムから、インストールされている音声がすべて見えるとは限りません。これに関しては instructions to make all voices visible [https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108] を参照してください。

Searching the text

The viewer has very powerful search capabilities. Press the Ctrl+F key or access the viewer controls and click search. The simplest form of searching is to just search for whatever text you enter in the text box. The different forms of searching are chosen by the search mode box below the search input.
Available modes are:

1. Contains - The simplest default mode. The text entered in the search box is searched for anywhere. All punctuation, accents and spaces are ignored.
For example, the search: Pena will match all of the following: penal, pen a, pen.a and Peña. If you select the Case sensitive box then accents, spaces and punctuation are no longer ignored.

2. Whole words - Searches for whole words. So for example, the search pena will match the word Peña but not the word Penal. As with Contains searches above, accents and punctuation are ignored unless the Case sensitive box is checked.

3. Nearby words - Searches for whole words that are near each other. So for example, the search calibre cool will match places where the words calibre and cool occur within sixty characters of each other. To change the number of characters add the new number to the end of the list of words. For instance, calibre cool awesome 120 will match places where the three words occur within 120 characters of each other. Note that punctuation and accents are not ignored for these searches.

4. Regex - Interprets the search text as a regular expression.
To learn more about using regular expressions, see the tutorial.

キーボード操作だけでリンクを開く

電子書籍ビューアには ヒントモード があり、これによってマウスを使うことなくテキスト中のリンクをクリックすることができるようになります。Alt+F キーを押すと現在の表示画面上にあるすべてのリンクが数字または文字つきでハイライト表示されます。リンクをクリックするには、キーボード上のその文字を押します。Esc キーを押すとリンクを指定せずに ヒントモード を中止します。

表示画面上に 35 個以上のリンクがあるとき、リンクの一部には複数文字がつけられます。この場合には 1 文字目と 2 文字目をタイプするか、1 文字目をタイプしてから Enter を押して実行します。Backspace キーを使うとタイプミスを取り消すことができます。

本の外観と操作感をカスタマイズ

フォントサイズをその場で変更するには、ビューアコントロールの フォントサイズ を使うか、Ctrl++ または Ctrl+- を使うか、Ctrl キーを押しながらマウスホイールを使用します。

色は、ビューア設定の 色 セクションで変更できます。

表示画面上に表示するページ数やページ余白は、ビューア設定の ページレイアウト で変更できます。

ビューア設定の ヘッダとフッタ セクションではヘッダやフッタをカスタマイズして、読了までの時間、現在の章タイトル、本の位置などを表示させることができます。

より高度なカスタマイズは スタイル 設定で行えます。ここではテキストの下に表示する背景画像や、すべての本に適用されるスタイルシートを指定したりできます。それを使うと段落スタイルやテキストの配置などを変更できます。calibre のユーザによって使用されるカスタムスタイルシートの例は、 フォーラム [https://www.mobileread.com/forums/showthread.php?t=51500] を参照してください。

辞書検索

現在の本の中にある単語の意味を調べるには、調べたい単語の上でダブルクリックするかタップを長押しして、図書館の形をした検索ボタンをクリックします。

テキストや画像をコピー

テキストや画像はそのコンテンツ上でマウスをドラッグして選択し、さらに右クリックして コピー を選択することによりクリップボードにコピーできます。コピーしたものは他のアプリケーションにプレーンテキストや画像として貼り付けることができます。

画像を拡大

画像を別のウィンドウで最大サイズに拡大表示するには、画像の上でダブルクリックするかタップを長押しします。または画像の上で右クリックして 画像を表示 を選択します。

キーボード ショートカット

The viewer has extensive keyboard shortcuts, like the rest of calibre. They can be customised in the viewer Preferences. The default shortcuts are listed below:

Keyboard shortcuts for the calibre viewer

キイーボードショートカット

アクション

Home, Ctrl+ArrowUp, Ctrl+ArrowLeft

Scroll to the start of the current file in a multi file book

Ctrl+Home

本の先頭までスクロール

Ctrl+End

本の最後までスクロール

End, Ctrl+ArrowDown, Ctrl+ArrowRight

Scroll to the end of the current file in a multi file book

ArrowUp

後方へスクロール。フローモードではなめらかに、ページモードでは画面分だけ。

ArrowDown

前方へスクロール。フローモードではなめらかに、ページモードでは画面分だけ。

ArrowLeft

フローモードでは少し左へ、ページモードでは1ページ分スクロール

ArrowRight

フローモードでは少し右へ、ページモードでは1ページ分スクロール

PageUp, Shift+Spacebar

画面分だけ後方にスクロール

PageDown, Spacebar

画面分だけ前方にスクロール

Ctrl+PageUp

前の章までスクロール

Ctrl+PageDown

次のセクションまでスクロール

Alt+ArrowLeft

戻る

Alt+ArrowRight

進む

Ctrl+T

Toggle Table of Contents

Ctrl+S

読み上げ

Alt+P

Change settings quickly by creating and switching to profiles

Alt+f

キーボードでリンクを開く

Ctrl+C

クリップボードにコピー

Alt+C

現在の場所をクリップボードにコピー

Ctrl+Shift+C

現在の場所を calibre:// 形式のURLとしてクリップボードにコピー

/, Ctrl+f, Cmd+f

検索開始

F3, Enter

次を検索

Shift+F3, Shift+Enter

前を検索

Ctrl+Plus, Meta+Plus

フォントサイズを拡大

Ctrl+Minus, Meta+Minus

フォントサイズを縮小

Ctrl+0

デフォルトのフォントサイズに戻す

Ctrl+]

画面に表示するページ数を増やす

Ctrl+[

画面に表示するページ数を減らす

Ctrl+Alt+C

画面に表示するページ数を自動調整

F11, Ctrl+Shift+F

全画面モードのON/OFF

Ctrl+M

テキストレイアウトをページモードとフローモードで切替え

Ctrl+W

スクロールバーの表示/非表示

Ctrl+X

リファレンスモードの切り換え

Ctrl+B

ブックマークを表示/非表示

Ctrl+Alt+B

新しいブックマーク

Ctrl+N, Ctrl+E

本の書誌情報を表示

Ctrl+Alt+F5, Ctrl+Alt+R

本の再読込

Ctrl+Shift+ArrowRight

現在の選択範囲を1文字分前向きに変更

Ctrl+Shift+ArrowLeft

現在の選択範囲を1単語分後ろ向きに変更

Shift+ArrowRight

現在の選択範囲を1文字分前向きに変更

Shift+ArrowLeft

現在の選択範囲を1文字分後ろ向きに変更

Shift+ArrowDown

現在の選択範囲を1行分前向きに変更

Shift+Home

現在の選択範囲を行頭まで拡張

Shift+End

現在の選択範囲を行末まで拡張

Ctrl+A

すべて選択

Shift+ArrowUp

現在の選択範囲を1行分後ろ向きに変更

Ctrl+Shift+ArrowDown

現在の選択範囲を1段落分前向きに変更

Ctrl+Shift+ArrowUp

現在の選択範囲を1段落分後ろ向きに変更

Esc, MenuKey

E-book ビューア制御の表示

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

E-book ビューア設定の表示

Ctrl+G, ;, :

本の中の指定した場所または位置へ移動

Ctrl+Spacebar

自動スクロールの切り換え

Alt+ArrowUp

自動スクロールの高速化

Alt+ArrowDown

自動スクロールの低速化

Ctrl+I

インスペクタを表示/非表示

Ctrl+L

単語検索パネルの表示/非表示

Ctrl+Q (Cmd+Q on macOS)

終了

Ctrl+P

本をPDFとして印刷

Ctrl+F11

ツールバーのON/OFF

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

Set when displaying a popup footnote

最後に、calibre のカラースキームの色は CSS 変数 [https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties] を通じて利用可能です。calibre ビューアは --calibre-viewer-background-color, --calibre-viewer-foreground-color, そしてオプションでカラーテーマの中でリンク色を定義する --calibre-viewer-link-color を定義します。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍変換

電子書籍変換
calibre には、非常に使いやすいように設計された変換システムがあります。通常、本を calibre に追加し、変換をクリックすると、calibre はできるだけ入力に近い出力を生成しようとします。ただし calibre は数多くの入力形式を受け付けますが、そのすべてが電子書籍への変換に適しているわけではありません。このような入力形式の場合、または変換システムをより細かく制御したい場合、calibreには変換プロセスを微調整するための多くのオプションがあります。ただし、calibreの変換システムは、本格的な電子書籍エディタの代わりにはならないことに注意してください。電子書籍を編集するには、まず calibre を使用して EPUB または AZW3 に変換し、次に 本の編集 機能を使って完璧に仕上げることをお勧めします。それが終われば編集した電子書籍を入力として、calibre の他の形式に変換するために利用できます。

このドキュメントでは主に、下の図に示す変換ダイアログの変換設定について説明します。これらの設定はすべて、コマンドラインインターフェイスからの変換にも使用できます。これについては、ebook-convert に記載されています。calibreでは、それぞれの設定項目の上にマウスを置くと、その設定に関するヘルプがツールチップで表示されます。

目次

- はじめに

- 外観

- ページ設定

- ヒューリスティック処理

- 検索と置換

- 構造検出

- 目次

- HTML入力ドキュメントを変換するときに章のタイトルとして画像を使用

- 目次項目のテキストとしてタグの属性を使用

- 変換のためにオプションを設定/保存する方法

- 形式固有のヒント

はじめに

変換システムについて最初に理解すべきは、パイプラインとして設計されているということです。図にすると、次のようになっています:

入力形式は、まず適切な 入力プラグイン によってXHTMLに変換されます。このHTMLは次に 変形 されます。最後のステップで、この処理された XHTML が、適切な 出力プラグイン によって指定された出力形式に変換されます。変換の結果は、入力形式によって大きく異なる可能性があります。一部の形式は、他の形式よりもずっとうまく変換されます。変換にあたりソースとして最適な形式の一覧は こちら にあります。

XHTML 出力に作用する変形こそが、すべての作業が行われる場所です。変形にはさまざまなものがありますが、たとえば、書誌を本の冒頭にページとして挿入したり、章の見出しを探して目次を自動的に作成したり、フォントサイズを比例的に調整したりするなど、といったことを行います。ただしこうした変形はすべて、入力ファイルそのものではなく、入力プラグイン から出力される XHTML に対して作用します。したがって、たとえば、calibre に RTF ファイルを EPUB に変換するよう求めると、まず内部で XHTML に変換され、さまざまな変形が XHTML に適用されてから、出力プラグイン がすべての書誌や目次などを自動的に生成して EPUB ファイルを作成します。

デバッグオプション を使用すると、この処理が動いているところ確認できます。デバッグ出力用のフォルダへのパスを指定するだけで使用できます。変換中、calibre は変換パイプラインのさまざまな段階で生成された XHTML を別々のサブフォルダに配置します。この 4つのサブフォルダは次のものです:

変換パイプラインのステージ

フォルダ

説明

入力

これには入力プラグインによる HTML 出力が含まれています。入力プラグインをデバッグするには、これを使用します。

解析

入力プラグインからの出力を前処理して XHTML に変換した結果。構造検出をデバッグするために使用します

構築

構造検出後、ただし CSS フラット化とフォントサイズ変換の前。フォントサイズ変換と CSS 変形をデバッグするために使用します。

処理

電子書籍が出力プラグインに渡される直前。出力プラグインをデバッグするために使用します。

calibre で変換する前に入力ドキュメントを少し編集したい場合、最善なのは input サブフォルダ内のファイルを編集してから圧縮し、その ZIP ファイルを変換の入力として使用することです。これを行うには、書誌の編集 ダイアログを使用して、書籍の形式に ZIP ファイルを追加し、変換ダイアログの左上端で、入力形式として ZIP を選択します。

このドキュメントでは、主に中間XHTMLに対して行うさまざまな変形と、それを制御する方法について説明します。最後に、各入出力形式に固有のヒントをいくつか記載します。

外観

目次

- フォント

- テキスト

- レイアウト

- スタイル

- スタイルの変形

- HTML の変形

このオプションのグループは、変換した電子書籍の見た目をさまざまな側面から制御します。

フォント

電子読書体験において最もすばらしい機能のひとつは、個々のニーズや照明条件に合わせてフォントサイズを簡単に調整できることです。calibre には高度なアルゴリズムがあり、入力ドキュメントでどのようなフォントサイズが指定されていようとも、出力される本ではどれもフォントサイズがそのままであることを保証します。

ドキュメントの基本フォントサイズとは、そのドキュメントで最もよく使われているフォントサイズ、つまりそのドキュメントのテキストの大部分で使われるサイズのことです。 基本フォントサイズ を指定すると、calibreはドキュメント内のすべてのフォントサイズを比例して自動的に再スケーリングします。これにより、最もよく使われているフォントサイズが指定された基本フォントサイズになり、他のフォントは適宜サイズ調整されます。基本フォントサイズを大きくすれば、ドキュメント内の他のフォントも大きくなりますし、逆もまた同じです。基本フォントサイズを設定する場合には、最良の結果を得るために、フォントサイズのキーも設定するとよいでしょう。

通常、calibreは、選択した出力プロファイルに適した基本フォントサイズを自動的に選択します (ページ設定 を参照)。ただしデフォルトが適切でない場合は、ここで上書きできます。

フォントサイズのキー オプションを使用すると、基本フォントサイズ以外のフォントをサイズ調整する方法を制御できます。フォントサイズ調整アルゴリズムは、フォントサイズのキーを使用して機能します。フォントサイズのキーは、フォントサイズのコンマ区切りのリストです。フォントサイズのキーは、特定のフォントサイズを基本フォントサイズと比較して、いくつの「ステップ」を大きくしたり小さくしたりするかを calibre に指示します。大事なのは、ドキュメント内のフォントサイズの数を制限する必要があるということです。たとえば、本文テキスト用に 1 個のサイズ、さまざまなレベルの見出し用に 2、3 個のサイズ、上付き/下付き文字と脚注用に 2、3 個のサイズがあります。フォントサイズのキーを使用すれば、calibre が入力ドキュメントのフォントサイズを、さまざまな論理フォントサイズに対応する個別の「ビン」に区分することができるようになります。

例を挙げて説明しましょう。変換しようとしているソースドキュメントが、視力に優れた人によって作成されているため、基本フォントサイズが 8pt であるとします。つまり、ドキュメント内のテキストの大部分のサイズが 8 ポイントで、見出しはやや大きく (たとえば、10 および 12 ポイント)、脚注は 6 ポイントでやや小さくなっています。ここで、次の設定を使用するとします:

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

出力ドキュメントの基本フォントサイズは 12pt、見出しは 14 および 16pt、脚注は 8pt になります。ここで、最も大きな見出しサイズをより目立たせ、脚注も少し大きくしたいとします。これを実現するには、フォントのキーを次のように変更する必要があります:

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

最大の見出しが 18pt になり、脚注は 9pt になります。これらの設定を試して、フォントサイズ調整ウィザードを使用して最適なものを見つけてください。このウィザードには フォントサイズのキー 設定の横にある小さなボタンをクリックすると利用できます。

入力ドキュメントのフォントサイズをそのまま使いたい場合は、変換でフォントのサイズ調整を行わないよう、ここで設定できます。

関連する設定は 行の高さ です。行の高さは、行の垂直方向の高さを制御します。デフォルトでは (行の高さは0)、行の高さの操作は実行されません。デフォルト以外の値を指定すると、行の高さは、独自の行の高さを指定しないすべての場所に設定されます。ただしこれは鈍器のようなものであり、控えめに使用する必要があります。入力の一部のセクションの行の高さを調整する場合は、追加 CSS を使用することをお勧めします。

このセクションでは、参照されているフォントを本に埋め込むように calibre に指示することもできます。これにより、そのフォントがデバイス上にない場合でも、リーダデバイスで機能するようになります。

テキスト

テキストは両端揃えにする場合としない場合があります。両端揃えのテキストには、単語の間に余分な空白があり、右マージンが滑らかになります。両端揃えのテキストを好む人もいれば、好まない人もいます。通常、calibre は元のドキュメントのテキスト揃えの状態をそのまま使用します。これを上書きしたい場合は、このセクションの テキスト揃え オプションを使用できます。

また、calibreに 句読点をスマート化 を指定することもできます。指定すると、プレーンな引用符、ダッシュ、省略記号が、活字で正しい代替記号に置き換えられます。ただしアルゴリズムは完全ではないため、結果を確認する価値があります。逆、つまり スマート化されていない句読点 も使用できます。

最後に、入力文字コード があります。古いドキュメントでは、文字コードが指定されていないことがあります。これを変換すると、英語以外の文字やスマート引用符などの特殊文字が破損する可能性があります。calibre はソースドキュメントの文字コードを自動検出を試みますが、常に成功するとは限りません。この設定を使用すすれば、特定の文字コードを前提とするように強制できます。 cp1252は、Windowsソフトウェアを使用して作成されたドキュメントの一般的な文字コードです。文字コードの問題について詳しくは 英語以外の文字やスマート引用符を含むファイルを変換するにはどうすればよいですか? もお読みください。

レイアウト

通常、XHTML の段落は、段落の間に空白行があり、先頭にテキストのインデントがない状態でレンダリングされます。calibre には、これを制御するためのいくつかのオプションがあります。 段落間の空白を削除 は、すべての段落に段落間の間隔がないことを強制的に保証します。また、テキストのインデントを 1.5em (変更可能) に設定して、すべての段落の開始をマークします。 空行を挿入 は逆のことを行い、段落の各ペアの間に正確に 1 つの空白行があることを保証します。これらのオプションはどちらも非常に包括的で、空白の削除や挿入は すべての 段落に対して行われます (技術的には <p> タグと <div> タグ)。これは入力ファイルがどれほど乱雑であろうとも、オプションを設定するだけで、宣伝どおりに機能することを保証できるようにするためです。唯一の例外は、入力ファイルが段落間の間隔を実装するためにハード改行を使用している場合です。

すべての段落間の間隔を削除したい場合は、一部の例外を除き、これらのオプションを使用しないでください。代わりに、次の CSS コードを 追加 CSS に追加します:

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

次にソースドキュメントで、間隔が必要な段落に class="spacious" とマークします。入力ドキュメントが HTML でない場合は、HTML を取得するための入門書で説明されているデバッグオプションを使用します (input サブフォルダを使用します)。

もうひとつの便利なオプションは、表を展開 です。不適切に設計されたドキュメントの中には、表を使ってページ上のテキストのレイアウトを制御するものがあります。このようなドキュメントを変換すると、ページからはみ出すテキストやその他の歪みが含まれていることがよくあります。このオプションは、表からコンテンツを抽出し、線形に表示します。ただしこのオプションはすべての表を線形化するため、入力ドキュメントが表形式の情報の表示などの正当な目的でテーブルを使用していないことが確実な場合にのみ使用してください。

スタイル

追加 CSS オプションを使用すると、入力内のすべての HTML ファイルに適用される任意のCSSを指定できます。このCSSは非常に高い優先度で適用されるため、 入力ドキュメント 自体に存在するほとんどの CSS を上書きします。この設定を使用すれば、ドキュメントのプレゼンテーション/レイアウトを微調整できます。たとえば、クラスの文末脚注のすべての段落を右揃えにしたい場合は、次を追加します:

.endnote { text-align: right }

または、すべての段落のインデントを変更したい場合には次のようにします:

p { text-indent: 5mm; }

追加 CSS は非常に強力なオプションですが、CSS を最大限に活用するには、CSS がどのように機能するかを理解する必要があります。上記のデバッグパイプラインオプションを使用すると、入力ドキュメントに存在する CSS を確認できます。

より簡単なオプションは、スタイル情報を絞り込み を使用することです。これを使うと、指定したタイプのすべての CSS プロパティをドキュメントから削除できます。たとえば、すべての色またはフォントの削除に利用できます。

スタイルの変形

これは、最も強力なスタイリング関連の機能です。これを使用すると、さまざまな条件に基づいてスタイルを変更するルールを定義できます。たとえば、すべての緑色を青色に変更したり、テキストからすべての太字のスタイルを削除したり、すべての見出しを特定の色に着色したりするのに利用できます。

HTML の変形

変換スタイルに似た機能で、本の HTML コンテンツに変更を加えることができます。あるタグを別のタグに置き換えたり、コンテンツに基づいてクラスやその他の属性をタグに追加したりできます。

ページ設定

ページ オプションは、余白や画面サイズなどの画面レイアウトを制御するためのものです。選択した出力形式がページ余白をサポートしている場合に出力プラグインによって使用される、ページ余白を設定するオプションがあります。さらに、入力プロファイルと出力プロファイルを選択する必要があります。プロファイルのセットは両方とも基本的には、入力/出力ドキュメント、画面サイズ、およびデフォルトのフォントサイズ調整のキーの寸法をどのように解釈するかを扱います。

変換しようとしているファイルが特定のデバイス/ソフトウェアプラットフォームでの使用を目的としていることがわかっている場合は、対応する入力プロファイルを選択します。それ以外の場合は、デフォルトの入力プロファイルを選択します。作成しているファイルが特定のデバイスタイプ用であることがわかっている場合は、対応する出力プロファイルを選択してください。それ以外の場合は、汎用出力プロファイルのひとつを選択します。MOBI または AZW3 に変換するのであれば、ほとんどの場合、Kindle の出力プロファイルのひとつを選択したいと思うでしょう。そうでなければ、最新の電子書籍リーダデバイスに最善なのは、汎用電子ペーパー HD` 出力プロファイルを選択することです。

出力プロファイルは、画面サイズも制御します。これにより、たとえば一部の出力形式では、画像が画面に収まるように自動的にサイズ変更されることになります。ですから、デバイスと同じような画面サイズを持つデバイスのプロファイルを選択してください。

ヒューリスティック処理

ヒューリスティック処理は、書式のくずれた入力ドキュメントにありがちな問題を検出して修正するためのさまざまな機能を提供します。入力ドキュメントの書式がくずれている場合は、これらの関数を使用してください。ただしこれらの関数は一般的なパターンに頼っているため、オプションによっては結果が悪化する可能性があります。例を挙げると、これらのオプションのいくつかは、すべてのノンブレークスペースエンティティを削除したり、関数に関連して誤検知することがあります。

ヒューリスティック処理を行う

このオプションは、calibreの 変換パイプラインの ヒューリスティック処理 ステージを有効にします。さまざまなサブ機能を適用するには、これが有効になっている必要があります。

行折り返し解除

このオプションを有効にすると、calibre は句読点の手がかりと行の長さを使用して、ドキュメント内に存在するハード改行を検出して修正しようとします。 calibreは、まずハード改行が存在するかどうかを検出しようとします。存在しないように見える場合、calibreは行の折り返しを解除しようとはしません。calibre に行の折り返しを解除するように強制したい場合には、行折り返し解除係数を減らすことができます。

行折り返し解除係数

このオプションは、calibre がハード改行の削除に使用するアルゴリズムを制御します。たとえばこのオプションの値が 0.4 の場合、ドキュメント内のすべての行の長さの 40％ より短ければ、その行の末尾から calibre がハード改行を削除することを意味します。ドキュメントに修正が必要な改行が数個しかない場合は、この値を 0.1 〜 0.2 の範囲に減らす必要があります。

書式のない章見出しとサブ見出しを探してマークアップ

ドキュメントの中に、他のテキストと書式が異なる章見出しとタイトルが存在しない場合、このオプションを使用すると calibre はそれを探して見出しタグで囲むことができます。 <h2> タグは、章見出しに使用されます。 <h3> タグは、見つかったすべてのタイトルに使用されます。

この機能が目次を作成するわけではありませんが、たいていの場合はこれによって calibre のデフォルトの章検出設定が正しく章を探して目次を作成するようになります。目次が自動的に作成されない場合は、構造検出で XPath を調整してください。ドキュメント内で他に見出しが使用されていなければ、目次を作成する最も簡単な方法は、構造検出で "//h:h2" を設定することです。

挿入される見出しには書式がありません。書式を適用するには、変換設定の外観の下にある 追加 CSS オプションを使用します。たとえば見出しタグを中央に配置するには、次のようにします:

h2, h3 { text-align: center }

<h1> や <h2> タグ順を再割り当て

一部の出版社は、複数の <h1> または <h2> タグを順番に使用して章の見出しの書式を設定します。 calibre のデフォルトの変換設定では、このようなタイトルは 2 つに分割されます。このオプションは、分割を防ぐために見出しタグに番号を付け直します。

段落間の空行を削除

このオプションを設定すると、calibreはドキュメントに含まれる空白行を分析します。すべての段落に空白行がとじ込みされている場合、calibre はその空白の段落をすべて削除します。連続した複数行の空白はシーン区切りと見なされ、ひとつの段落としてそのまま保持されます。このオプションは 外観`の下の :guilabel:`段落間の空白を削除 オプションとは HTML コンテンツ自体を変更する点が異なります。段落間の空白を削除 オプションはドキュメントスタイルを変更します。このオプションでは、calibreの 空行を挿入 オプションを使って挿入された段落を削除することもできます。

シーン区切りの書式を変更しない

このオプションを使用すると、calibre は一般的なシーン区切りマーカーを探して中央に配置します。 「ソフト」シーン区切りマーカー、つまり余分な空白によってのみ定義されるシーン区切りが、改ページと同時に表示されることのないようにスタイルを設定します。

シーン区切りを置換

このオプションが設定されていると、calibreは検出したシーン区切りマーカーをユーザが指定した置換テキストに置き換えます。ただし一部の装飾用文字は、読み取りデバイスによっては表示できないこともあります。

全般的に HTML タグの使用は避けてください。calibre はタグをすべて破棄し、事前に定義したマークアップを使用します。 <hr /> タグ、つまり水平線、および <img> タグは例外です。オプションで水平線のスタイルを指定できます。自分でスタイルを追加する場合には、必ず '幅' 設定を含めてください。そうしないと、スタイル情報が破棄されます。画像タグは使用可能ですが、calibre は変換中に画像を追加する機能を提供していません。これは、'本の編集' 機能を使用して事後に行う必要があります。

画像タグの例 (変換後に EPUB 内部の 'Images' フォルダに画像を配置)

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

書式つき水平線の例:

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

不要なハイフンを削除

このオプションを有効にすると、calibre はドキュメント内のハイフンでつながれたすべてのコンテンツを分析します。ドキュメントそのものが、分析用の辞書として使われます。これのオプションを使用すれば、calibre は文書内の言語が何であれ、造語であろうとあいまいな科学用語であろうと正しくハイフンを削除できます。主な欠点は、ドキュメントに 1 回しか表示されない単語は変更できないことです。分析は 2 つの工程で行われ、最初の工程では行末を分析します。その単語がドキュメント内にハイフンの有無にかかわらず存在する場合にのみ、行の折り返しが解除されます。 2 番目の工程では、ドキュメント全体でハイフンが付けられたすべての単語が分析されます。単語がマッチすることなくドキュメント内の他の場所に存在する場合、ハイフンは削除されます。

常用句やパターンをイタリック体にする

有効にすると、calibre はイタリックを表す一般的な単語やパターンを探し、それらをイタリック体にします。例としては、~word~ のようによく使われているテキスト慣習や、'etc.' や 'et cetera' などのラテン語の語句のように一般的にイタリック体にされるものが挙げられます。

要素のインデントを CSS のインデントに置換

一部のドキュメントでは、ノンブレークスペースを使用してテキストインデントを定義する手法をとっています。このオプションを有効にすると、calibre はこのような書式を探し出し、CSS を用いて 3％ のテキストインデントに変換しようとします。

検索と置換

これらのオプションは、主に PDF ドキュメントの変換や OCR 変換に役立つものですが、多くのドキュメント固有の問題を修正するためにも使用できます。例として、一部の変換では、テキストにページのヘッダとフッタが残る場合があります。これらのオプションは、正規表現を使用して、ヘッダ、フッタ、またはその他の任意のテキストを探し、削除したり置換したりします。ただしこれが操作する対象は、変換パイプラインによって生成された中間 XHTML です。ウィザードを使うと、ドキュメントの正規表現のカスタマイズを手助けしてくれます。式ボックスの横にある魔法の杖をクリックし、検索式を作成した後、'テスト' ボタンをクリックしてください。マッチに成功したものは黄色で強調表示されます。

検索は、Python 正規表現を使用して機能します。マッチしたテキストはすべて、ドキュメントから削除されるか、置換パターンを使用して置換されます。置換パターンはオプションです。空白にしておくと、検索パターンにマッチするテキストがドキュメントから削除されます。正規表現とその構文について詳しくは calibre で正規表現を利用するにあたって重要なこと を参照してください。

構造検出

構造検出は、入力ドキュメント内の構造要素が適切に指定されていない場合に、caliber がそれ検出するのに最善を尽くすことに関わります。たとえば、章、ページ分割、ヘッダ、フッタなどです。容易に想像できることですが、この処理は本ごとに大きく異なります。幸い、calibre にはこれを制御するための非常に強力なオプションがあります。強力さには複雑さが伴うものですが、時間をかけて複雑さを学んでみれば、努力するだけの価値が十分にあるとわかるでしょう。

章と改ページ
calibreには、章の検出 と:guilabel:改ページの挿入 という 2 つのオプションセットがあります。デフォルトでは、calibre は検出された章の前に改ページを挿入した上に、改ページオプションによって検出された場所にも挿入するため、少々混乱する場合があります。こうする理由は、しばしば章の境界ではない場所に改ページを挿入する必要があるためです。検出された章は、オプションで自動生成された目次に挿入することもできます。

caliberは XPath を使用しています。これはユーザが章の境界/改ページを指定できるようにする、強力な言語です。最初のうち XPath は使い方が難しく感じるかもしれませんが、ありがたいことにユーザマニュアルの中には XPath チュートリアル があります。構造検出が処理するのは、変換パイプラインによって生成された中間 XHTML です。書籍に適した設定を探すには はじめに に説明されているデバッグオプションを使用してください。簡単な XPath 式の生成を支援するために、XPath ウィザードのボタンも用意されています。

デフォルトでは、calibre は章を探すのに次の式を使用します:

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

この式はかなり複雑です。数多くの一般的なケースを同時に処理しようとしているためです。どういう意味かと言うと、calibreは、章が <h1> または <h2> タグのいずれかで始まり、その中に単語 (chapter, book, section または part) のうちいずれかが含まれている、または class="chapter" 属性がつけられているものと想定しています。

関連するオプションに 章のマーク があります。これを使用すると、章を見つけたときの calibre の動作を制御できます。デフォルトでは、章の前に改ページが挿入されます。改ページの代わりに、または改ページに加えて、罫線を挿入することができます。何もしないようにすることもできます

改ページを探すためのデフォルトの設定は次のようになっています:

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

これは、デフォルトで、calibreがすべての <h1> および <h2> タグの前に改ページを挿入することを意味します。

注釈

変換する入力形式によっては、デフォルトの式が変わることがあります。

その他

このセクションにはさらにいくつかのオプションがあります。

本の最初のページに書誌を挿入

calibreの優れた点のひとつに、書籍に関するまさに完璧な書誌を管理できることが挙げられます。評価、タグ、コメントなど、すべてを管理できます。このオプションは、こうした書誌を含むひとつのページを作成し、変換した電子書籍に挿入します。通常は表紙の直後に挿入します。自分でカスタマイズした表紙を作成する方法のひとつと考えてください。

最初の画像を削除

時として変換するソースドキュメントには、表紙として分離されたものではなく、表紙が本の一部に含まれていることがあります。calibre でも表紙を指定した場合、変換された本は 2 つの表紙を持つことになります。このオプションは、単にソースドキュメントから最初の画像を削除します。その結果、変換された本の表紙は calibre によって指定されたものだけになります。

目次

入力ドキュメントの書誌に目次があれば、calibre はそれを使用します。しかし古い形式の多くは書誌ベースの目次をサポートしていなかったり、個々のドキュメントが書誌を持たなかったりします。こうした場合にこのセクションのオプションを使えば、入力ドキュメントの実際のコンテンツに基づいて、変換された電子書籍の中に自動的に目次を生成してくれます。

注釈

このオプションの使い方を正確に理解するのには、少々手こずるかもしれません。目次を手作業で作成/編集したい場合は、EPUB または AZW3 形式に変換し、変換ダイアログの目次セクションの下部にある 変換完了後に目次を手動で調整 にチェックします。こうすると、変換後に目次編集ツールが起動するようになります。このツールを使えば、本の中の目次に入れたい場所をクリックするだけで目次に項目を作成できます。変換せずに目次エディタを単独で使用することもできます。環境変数 → インタフェース → ツールバー へ行き、目次エディタ をメインツールバーに追加してください。それから編集する本を選択して、目次エディタ ボタンをクリックします。

最初のオプションは 自動生成された目次を強制的に使用 です。このオプションをチェックすると、入力ドキュメントの書誌にある目次を自動生成された目次で上書きできます。

自動生成される目次はデフォルトでは、calibreはまず、見つかった章を生成される目次に追加しようとします。章の検出をカスタマイズする方法については、上の 構造検出 の章で詳しく説明しています。見つかった章を生成される目次に含めたくない場合は、見つかった章を目次に追加しない オプションをチェックしてください。

見つけた章の数が 章のしきい値 より少ない場合、calibreは入力ドキュメントで見つかったハイパーリンクを目次に追加します。たいていの場合はこれでうまくいきます。入力ドキュメントには、先頭にハイパーリンクつきの目次が含まれていることが多いからです。リンク数 オプションを使うと、この動作を制御できます。ゼロを設定すれば、リンクは追加されません。ゼロより大きい数を設定した場合には、最大でその数までのリンクが追加されます。

calibre は、生成された目次から重複を自動的に取り除きます。もしそれ以外にも望ましくない項目があれば、目次を除去 オプションを使って取り除くことができます。ここに指定するのは、生成される目次のタイトルとマッチさせる正規表現です。マッチするものが見つかれば、削除します。たとえば "Next" または "Previous" というタイトルをすべて削除するには、次のようにします:

Next|Previous

レベル 1,2,3 目次 オプションを使用すると、洗練されたマルチレベルの目次を作成できます。ここに XPath 式を指定して、変換パイプラインによって生成された中間 XHTML のタグとマッチさせます。この XHTML にアクセスする方法については、はじめに を参照してください。XPath 式の作成方法については、XPath のチュートリアル もお読みください。各オプションの横には、基本的な XPath 式の作成を支援するウィザードを起動するためのボタンがついています。これらのオプションの使用方法を、次の簡単な例で示します。

次のような XHTML を生成する入力ドキュメントがあるとします:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sample document</title>
</head>
<body>
<h1>Chapter 1</h1>
...
<h2>Section 1.1</h2>
...
<h2>Section 1.2</h2>
...
<h1>Chapter 2</h1>
...
<h2>Section 2.1</h2>
...
</body>
</html>

次に、オプションを次のように設定します:

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

これにより、次のような 2 レベルの目次が自動的に生成されます。

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

警告

すべての出力形式がマルチレベルの目次をサポートしているわけではありません。最初に EPUB 出力を試してみてください。それがうまくいったら、実際に使いたい形式を試してください。

HTML入力ドキュメントを変換するときに章のタイトルとして画像を使用

章タイトルとして画像を使用したいが、それでも calibre に章タイトルから目次を自動的に生成させたいとします。これを実現するには、次の HTML マークアップを使用します:

<html>
<body>
<h2>Chapter 1</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2"><img src="chapter2.jpg" /></h2>
<p>chapter 2 text...</p>
</body>
</html>

レベル 1 目次 に //h:h2 を設定します。そうすると第 2 章ではタグにテキストがないため、calibre は <h2> タグの ``title` 属性の値からタイトルを取得します。

目次項目のテキストとしてタグの属性を使用

著しく長い章のタイトルがあり、目次用にはそれを短くしたものを使用したい場合は、title 属性を使用すれば実現できます。たとえば次のようにします:

<html>
<body>
<h2 title="Chapter 1">Chapter 1: Some very long title</h2>
<p>chapter 1 text...</p>
<h2 title="Chapter 2">Chapter 2: Some other very long title</h2>
<p>chapter 2 text...</p>
</body>
</html>

レベル 1 目次 に //h:h2/@title を設定します。そうするとタグ内のテキストを使用する代わりに、calibre は <h2> タグの title 属性の値からタイトルを取得します。 XPath 式の末尾の /@title に注目してください。この形式を使えば、任意の属性からテキストを取得するように calibre に指示できます。

変換のためにオプションを設定/保存する方法
calibre で変換オプションを設定できる場所は2つあります。 1 つ目は、設定->変換 にあります。これらの設定は、変換オプションのデフォルトです。新しい本を変換しようとするとき、ここで設定したものがデフォルトとして使用されます。

本の変換ごとに、変換ダイアログで設定を変更することもできます。本を変換するとき、calibre はその本に使用した設定を記憶します。次にまた変換するときには、環境設定 で設定されたデフォルトよりも、それぞれの本に保存された設定を優先します。本の変換ダイアログの デフォルトに戻す ボタンを使用すれば、個別の設定をデフォルトに戻すことができます。本のグループに保存されている設定を削除するには、すべての本を選択して 書誌を編集 ボタンをクリックし、書誌の一括編集ダイアログを表示します。ダイアログの下部に、保存されている変換を削除するオプションがあります。

本のセットを一括変換する場合、設定は次の順に取得します (最後のものが優先されます)。

- 環境設定->変換で設定したデフォルトから

- 変換されるそれぞれの本の保存された変換設定から (もしあれば)。これは、一括変換ダイアログの左上隅にあるオプションで無効にできます。

- 一括変換ダイアログでで設定した内容から

一括変換するそれぞれの本の最終設定は保存され、再びその本が変換されときには再利用されます。一括変換では、一括変換ダイアログの設定が最も優先されるため、個別の本の設定を上書きします。したがって、一括変換する本は同じような設定が必要な本に限る必要があります。例外は書誌と、入力形式に固有の設定です。一括変換ダイアログにはこれら 2 つのカテゴリの設定がないため、個別の本の設定 (あれば) またはデフォルトから取得されます。

注釈

右下端にある回転アイコンをクリックしてから、個々の変換ジョブをダブルクリックすると、変換中に使用された実際の設定を確認できます。これは使用された実際の設定を含む変換ログを上部に表示します。

形式固有のヒント

ここでは特定の形式の変換に固有な情報を記載します。特定の形式に特有なオプションや、TXT 入力 や EPUB 出力 のような入力と出力がそれぞれの変換ダイアログのもとで利用可能かどうか、といったことです。

Microsoft Word ドキュメントの変換
calibre は、Microsoft Word 2007 以降で作成された .docx ファイルを自動的に変換できます。ファイルを calibre に追加し、変換をクリックするだけです。

注釈
calibre 変換エンジンの機能を示す デモ用 .docx ファイル [https://calibre-ebook.com/downloads/demos/demo.docx] があります。ダウンロードして EPUB または AZW3 に変換するだけで、calibre で何ができるかを確認できます。

Microsoft Wordで Heading 1, Heading 2 などのスタイルで見出しをマークすると、calibre は見出しをもとにして目次を自動的に生成します。出力された電子書籍を calibre の電子書籍ビューアで開き、目次 ボタンをクリックして、生成された目次を表示してください。

古い .doc ファイル

古い .doc ファイルの場合には、Microsoft Word を使用してドキュメントを HTML として保存し、その HTML ファイルを calibre を使って変換できます。 HTML として保存するときは、必ず "Webページ (フィルター後)" を選択してください。こうすると、適切に変換できるクリーンな HTML が生成されます。ただし Wordは非常に厄介なHTMLを生成するため、変換には長い時間がかかる可能性があります。気長に待ちましょう。新しいバージョンの Word を利用できる場合は、それを .docx として直接保存することもできます。

もうひとつの方法は、無料の LibreOffice を使用することです。 LibreOfficeで.docファイルを開き、.docxとして保存します。これは、calibreで直接変換できます。

テキストドキュメントの変換

テキストドキュメントには、太字、斜体などの書式設定や、段落、見出し、セクションなどのドキュメント構造を指定する明確な方法はありませんが、一般的に使用されるさまざまな慣習があります。デフォルトでは calibreは、これらの慣習をもとにして、正しい書式とマークアップの自動検出を試みます。

テキスト入力は、段落の検出方法を差別化するためのいくつかのオプションをサポートしています。

段落スタイル: 自動

テキストファイルを分析し、段落がどのように定義されているかを自動的に判断しようとします。このオプションはたいていうまくいきますが、望ましい結果が得られない場合は手動オプションを試してください。

段落スタイル: ブロック

ひとつ以上の空白行が段落境界であると想定します:

This is the first.

This is the
second paragraph.

段落スタイル: 一行

すべての行を段落とみなします:

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

段落スタイル: 印刷

すべての段落がインデント (タブまたは2 つ以上のスペース) で始まると想定します。その次にインデントで始まる行に到達すると、段落は終了します。

This is the
first.
This is the second.

This is the
third.

段落スタイル: 書式なし

ドキュメントには書式設定がないものとみなしますが、ハード改行は使用します。段落の再現を試みるために、句読点と中線の長さを使用します

書式スタイル: 自動

使用されている書式マークアップのタイプを検出しようとします。マークアップが使用されていない場合は、ヒューリスティックな書式が適用されます。

書式スタイル: ヒューリスティック

一般的な章の見出し、シーン区切り、イタリック体の単語がないかドキュメントを解析し、変換中に適切な HTML マークアップを適用します。

書式スタイル: Markdown

calibre は、Markdownと呼ばれる変換プリプロセッサを介した TXT 入力の実行もサポートしています。Markdown を使用すると、太字、斜体、セクション見出し、表、リスト、目次などの基本的な書式を TXTドキュメントに追加できます。章の見出しには先頭に＃を付けて、章のXPath検出式を "//h:h1" に設定するのが、TXTドキュメントから適切な目次を生成するための最も簡単な方法です。Markdown 構文の詳細については、 daringfireball [https://daringfireball.net/projects/markdown/syntax] を参照してください。

書式スタイル: なし

テキストに特別な書式を適用せず、ドキュメントが HTML に変換される以外の変更は加えられません。

PDF ドキュメントの変換

PDFドキュメントは、変換元としては最悪の形式のひとつです。ページサイズは固定で、テキスト配置形式です。つまり、ある段落がどこで終わり、別の段落がどこから始まるかを判断するのが非常に困難なのです。 calibre は、設定変更可能な 行折り返し解除係数 を使って段落の行折り返し解除を試みます。この係数は、どの長さで行の折り返しを解除をするか判断するために使用する尺度です。有効な値は 0 から 1 までの小数です。デフォルトは 0.45 で、中線の長さのすぐ下です。この値を下げると、折り返し解除した行により多くのテキストが含まれます。増やすとテキストが少なくなります。この値は、PDF 入力 の下の変換設定で調整できます。

また多くの場合、ドキュメントの一部であるヘッダとフッタがテキストに含まれるよう形になっています。 検索と置換 パネルを使用してヘッダとフッタを削除し、この問題を緩和してください。ヘッダとフッタがテキストから削除されていないと、段落の行折り返し解除を放棄する可能性があります。ヘッダとフッタの削除オプションの使い方については calibre で正規表現を利用するにあたって重要なこと を参照してください。

PDF 入力での制約の一部を挙げると:

- 複雑なドキュメント、マルチカラムなドキュメント、画像ベースのドキュメントをサポートしていません。

- ドキュメントからのベクタ画像の抽出と、表の抽出もサポートしていません。

- 一部のPDFは、ll、ff、fiなどを表すのに特別なグリフを使用してます。PDF内部での表現方法次第で、これらのテキストの変換が機能する場合と機能しない場合があります。

- リンクと目次をサポートしていません。

- 埋め込みの非 Unicode フォントを使って英語以外の文字を表現している PDF は、それらの文字の出力が文字化けします。

- 一部の PDF は、ページを写真に写したものに OCR で読み取ったテキストを裏につけたものでできています。このような場合に calibre は OCR で読み取ったテキストを使用しますが、そうすると PDF ファイルを表示したときの見た目とは大きく異なってしまう可能性があります。

- 右から左へ流れる言語や数式のような、複雑なテキストを表示する PDF は正しく変換されません。

何度も繰り返しますが、入力として PDF は本当に、本当にダメな 形式です。何が何でも PDF を使わざるを得ない場合、入力 PDF によっては、まともなものから使い物にならないものまで出力の幅が大きいことを覚悟してください。

コミック本のコレクション

コミック本コレクションとは .cbc ファイルのことです。 .cbc ファイルは、他の CBZ/CBR ファイルを含む ZIP ファイルです。さらに .cbc ファイルには、UTF-8 でエンコードされた comics.txt という簡単ななテキストファイルが含まれている必要があります。comics.txt ファイルには、次のように .cbc ファイル内のコミックスファイルのリストが filename:title の形式で含まれていなくてはなりません:

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

そして .cbc ファイルには次のものが含まれます:

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

caliberは、この .cbc ファイルを comics.txt の各項目を指す目次付きの電子書籍に自動的に変換します。

EPUB の優れた書式のデモ

この デモファイル [https://calibre-ebook.com/downloads/demos/demo.epub] では、EPUBファイルのさまざまな優れた書式を紹介しています。このファイルは calibre を使用して手書きの HTML から作成しました。自分で EPUB を作成するときのテンプレートとして使えるよう作られています。

これを作成した元となる HTMLは demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip] です。この ZIP ファイルから EPUB を作成するにあたって使用した設定は次のとおりです:

ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover

ただしこのファイルでは EPUB の可能性を追求しているため、優れた書式のうちかなりの部分は、calibre のビルトイン EPUBビューアに比べて能力の劣るリーダでは機能しません。

ODT ドキュメントの変換
calibreは、ODT (OpenDocument Text) ファイルを直接変換できます。ドキュメントの書式にはスタイルを使用し、書式を直接使用するのは最小限に抑えるとよいでしょう。ドキュメントに画像を挿入する場合、画像は段落に固定する必要があります。ページに固定した画像はすべて、変換したものの先頭に置かれてしまいます。

章の自動検出を有効にするには、ビルトインスタイルで Heading 1, Heading 2, ..., Heading 6 (Heading 1 は HTML タグ <h1>, Heading 2 は <h2> という具合に相当します) でマークする必要があります。calibre で変換するときには、使用したスタイルを 章を検出 ボックスに入力できます。例:

- 章をスタイル Heading 2 でマークする場合、'章を検出' ボックスは //h:h2

- セクションが Heading 2 でマーク、章を Heading 3 でマークしている階層化された目次には、//h:h2|//h:h3 と入力する必要があります。変換の目次ページでは レベル 1 目次 ボックスに //h:h2 を、そして レベル 2 目次 ボックスには //h:h3 を入力します。

よく知られているドキュメントのプロパティ (タイトル、キーワード、説明、著者) が認識され、calibre は最初の画像 (小さすぎず、縦横比が良好) を表紙画像として使用します。

優れたプロパティ変換モードもあり、このモードを有効にするには ODT ドキュメントでカスタムプロパティ opf.metadata (Yes/No タイプ) を Yesに設定します (ファイル->プロパティ->カスタムプロパティ)。calibre がこのプロパティを検出すると、次のカスタムプロパティが認識されます (opf.authors はドキュメント作成者を上書きします):

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

これに加え、表紙として使用する画像を指定することも可能です。それには ODTで表紙画像に opf.cover と名前を付けます (右クリックして画像->オプション->名前)。この名前の画像が見つからない場合は、スマート方式が使用されます。表紙の検出のせいで特定の出力形式では表紙が二重になってしまう可能性があるため、この処理はその段落をドキュメントから削除します (ただし中身が表紙しかない場合に)。これが機能するのは名前付きの画像に限られます。

表紙検出を無効にするには、上級者モードでカスタムプロパティ opf.nocover (Yes/No タイプ) に Yes を設定します。

PDF への変換

PDF に変換するときに決めなくてはならない最初の、最も重要な設定は、ページサイズです。デフォルトでは calibreは "U.S. Letter" のページサイズを使用します。変換ダイアログの PDF 出力 のセクションで、これを別の標準ページサイズにしたり、または完全にカスタムなサイズに変更したりすることが可能です。特定のデバイス用に PDF を生成しようとしている場合には、その代わりに 出力プロファイル に設定したページサイズを使用するオプションを有効にしてもかまいません。つまり出力プロファイルが Kindle に設定されていれば、calibre は小さな Kindle 画面での表示に適したページサイズの PDF を作成します。

ヘッダとフッタ

ヘッダとフッタのテンプレートを指定することにより、PDF の各ページに任意のヘッダとフッタを挿入できます。テンプレートは、ヘッダとフッタの場所にレンダリングされる HTML コードのスニペットにすぎません。たとえば、すべてのページの下部の中央にあるページ番号を緑色で表示するには、次のフッタテンプレートを使用します:

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

caliberは、_PAGENUM_ を現在のページ番号に自動的に置き換えます。偶数ページと奇数ページに異なるコンテンツを配置することもできます。たとえば、次のヘッダテンプレートでは、奇数ページにタイトルが表示され、偶数ページに著者名が表示されます:

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

caliberは _TITLE_ と _AUTHOR_ を、変換されるドキュメントのタイトルと著者名に自動的に置き換えます。 justify-content を flex-end に設定すると、テキストは右揃えになります。

このヘッダテンプレートで示されているように、左端と右端にテキストを表示したり、フォントサイズを変更したりすることもできます:

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

こうすると左側にタイトル、右側に著者名が本文よりも小さいフォントサイズで表示されます。

以下に示すように、テンプレートで現在のセクションを使用することもできます:

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

_SECTION_ は、現在のセクションの名前に置き換えられます。これらの名前は、ドキュメントの書誌の目次 (PDF アウトライン) から取得されます。ドキュメントに目次がなければ、空のテキストに置き換えられます。 ひとつの PDF ページに複数のセクションがある場合、ページの最初のセクションが使用されます。同様に、現在の最上位セクションの名前を取得するには _TOP_LEVEL_SECTION_ という名前の変数を使用します。

ヘッダテンプレートとフッタテンプレート内では、JavaScript を使用することさえ可能です。たとえば、次のテンプレートを使用すると、ページ番号が 1 ではなく 4 から始まります:

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

さらにヘッダとフッタで利用可能な変数がいくつかあります。次に説明します:

- _TOTAL_PAGES_ - PDF ファイルの総ページ数。たとえば、進行状況カウンタの実装に役立ちます。

- _TOP_LEVEL_SECTION_PAGES_ - 現在のトップレベルセクションの総ページ数

- _TOP_LEVEL_SECTION_PAGENUM_ - 現在のトップレベルセクション内にある現在のページのページ番号

注釈

ヘッダとフッタを追加するときは、変換ダイアログの PDF 出力 セクションで、必ずページの上部と下部の余白を十分な大きさに設定してください。

印刷可能な目次

PDFの最後に、すべてのセクションのページ番号を一覧にした、印刷可能な目次を挿入することもできます。PDFを紙に印刷する場合に、非常に便利です。電子デバイスでPDFを使用する場合は、PDFアウトラインがこの機能を提供し、デフォルトで生成されます。

変換ダイアログの外観セクションにある 追加 CSS 変換設定を使用して、生成される目次の見た目をカスタマイズできます。デフォルトの CSS を次に示すので、これをコピーして必要な変更を加えてください。

.calibre-pdf-toc table { width: 100%% }

.calibre-pdf-toc table tr td:last-of-type { text-align: right }

.calibre-pdf-toc .level-0 {
font-size: larger;
}

.calibre-pdf-toc .level-1 td:first-of-type { padding-left: 1.4em }
.calibre-pdf-toc .level-2 td:first-of-type { padding-left: 2.8em }

HTML ファイルごとのカスタムページ余白

複数の HTML ファイルを含む EPUB または AZW3 ファイルを変換していて、特定の HTML ファイルのページ余白を変更したい場合には、calibre の電子書籍エディタを使用して HTML ファイルに次のスタイルブロックを追加できます:

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

次に、変換ダイアログの PDF 出力セクションで、変換するドキュメントのページマージンを使用 オプションを有効にします。ここうすると、この HTML ファイルから生成されたすべてのページに 10pt の余白ができます。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍の編集

電子書籍の編集
calibre には EPUB や AZW3 (Kindle) 形式の書籍を編集するための統合的な電子書籍エディタがあります。エディタには本の内部で使用される HTML と CSS が表示されます。ファイルに変更を加えると、ライブプレビューでの表示も更新されます。また一般的なクリーンアップやタスク修正を実行する自動ツールも含まれています。

エディタを使用するには、calibre 内の本の上で右クリックして 本の編集 を選択します。

目次

基本的なワークフロー

ファイルブラウザ

- ファイルの名前を変更

- ファイルのマージ

- テキストファイルの並び順を変更

- 表紙をマーク

- ファイルの削除

- ファイルのエクスポート

- 新しい画像/フォントなどを追加、または新しい空のライルを作成

- ファイルの置換

- スタイルシートを HTML ファイルに効率的にリンク

検索と置換

- 保存済み検索条件

- 関数モード

- HTML タグを無視して検索

自動化ツール

- 目次の編集

- 本のチェック

- 表紙の追加

- 参照されているフォントの埋め込み

- 埋め込みフォントのサブセット化

- 句読点のスマート化

- CSS プロパティの変換

- 使用されていない CSS 規則の削除

- HTML の修正

- ファイルの整形

- インライン目次の挿入

- セマンティクスの設定

- スタイル情報の絞り込み

- 本の内部のアップグレード

チェックポイント

ライブプレビューパネル

- HTML ファイルの分割

ライブ CSS パネル

その他のツール

- 目次ビュー

- 本の中身のスペルチェック

- 特殊文字の挿入

- コードインスペクタビュー

- 外部リンクのチェック

- 外部リソースのダウンロード

- ファイルをタイプ別にフォルダに配置

- その他の電子書籍形式から EPUB にインポート

- レポートツール

コードエディタの特殊な機能

- 構文の強調表示

- 状況依存のヘルプ

- 自動補完

- スニペット

基本的なワークフロー

注釈
calibre 電子書籍エディタの動画ツアーは :website:`こちら <demo#tutorials>`で視聴できます。

本の編集ツールで初めて本を開くと、左側にファイルの一覧が表示されます。これは本のコンテンツを構成する個々の HTML ファイル、スタイルシート、画像などです。ファイルの上でダブルクリックするだけで編集を開始できます。ただしちょっとした調整を行う以上の高度なことを行いたい場合には、 HTML チュートリアル [http://html.net/tutorials/html/] および CSS チュートリアル [http://html.net/tutorials/css/] の知識が必要となります。

HTML または CSS にエディタで変更を行うと、変更内容を右側にあるプレビューパネルに、ライブのプレビューが表示されます。満足するまで変更を加えたら、保存 ボタンを使用するか、ファイル → 保存 を使って変更内容を電子書籍に保存してください。

便利な機能のひとつは、チェックポイント です。何か野心的な編集をいくつも行う前に、チェックポイントを作成することができます。チェックポイントは、本の現在の状態を保持します。後で変更内容が気に入らずに元に戻したくなったとき、チェックポイントを作成したときの状態まで戻ることが可能です。チェックポイントを作成するには、編集 → チェックポイントを作成 を使用します。チェックポイントはまた、グローバルな検索と置換のような自動化ツールを実行したときにも自動的に作成されます。チェックポイント機能は、個々のファイル編集を行うときに通常の「取り消す/やり直す」とは別に追加されます。変更が本の中の複数のファイルにわたっている場合に必要となります。

これが本の編集の基本的なワークフローです。ファイルを開き、変更を加え、プレビューして保存。このマニュアルの残りの部分では、仕事を効率的に行えるようにしてくれるさまざまなツールや機能について説明します。

ファイルブラウザ

ファイルブラウザ を使うと、編集している本の中にあるさまざまなファイルの概要を見せてくれます。ファイルはカテゴリ別に並べられており、テキスト (HTML) ファイルが最上部にあり、次にスタイルシート (CSS)、画像などが続きます。ファイルの上でダブルクリックするだけで編集が始められます。編集できるのは HTML、CSS、および画像ファイルです。テキストファイルの並び順は本を読むときに表示される順です。それ以外のファイルはアルファベット順に並べられます。

エントリの上にマウスを置くと、サイズが表示され、さらに画面の下部に本の中のファイルへのフルパスが表示されます。ただし電子書籍内のファイルは圧縮されているので、最終的な本のサイズは個々のファイルサイズの合計にはなりません。

本の中で、多くのファイルには特別な意味があります。これらには通常、名前の隣に特殊な意味を表すアイコンがあります。たとえば左の図では、ファイル cover_image.jpg と titlepage.xhtml には表紙のアイコンが隣につけられ、このファイルが本の表紙の画像とタイトルページであることを示しています。同様に content.opf には書誌のアイコンが隣についていて、本に書誌があることを示し、toc.ncx ファイルには T アイコンが隣についており、目次であることを示しています。

ファイルの上で右クリックすると、個々に対していろいろな動作を実行できます。

ファイルの名前を変更

個々のファイル名はファイルの上で右クリックして ファイル名を変更 を選択して変更できます。ファイルの名前を変更すると、そのファイルへのすべてのリンクが本全体で自動的に更新されます。したがって新しい名前を与えてやるだけで、それ以外のことはすべて calibre が面倒を見てくれます。

多くのファイルの名前をまとめて一度に変更することも可能です。ファイルに単純なパターンの名前をつけたいときに便利です。たとえばすべての HTML ファイルに Chapter-1.html, Chapter-2.html のように名前をつけたいとします。まとめて名前を変更したいファイルを Shift または Ctrl キーを押しながらクリックして選択します。次に右クリックして 名前をまとめて変更 を選択します。プレフィックスと、自動的な番号づけの開始番号を指定して、OK を押せばおしまいです。名前をまとめて変更ダイアログは、選択した順ではなく表示されている順に名前を変更するので、たとえばすべての画像ファイルを表示順に名前をつけたい場合などに便利です。

最後に、選択したファイルすべての拡張子をまとめて変更することも可能です。複数のファイルを上記の手順で選択し、右クリックして 選択したファイルの拡張子を変更 を選択します。

ファイルのマージ

2 つの HTML ファイルまたは 2 つの CSS ファイルを連結してひとつにしたい場合があります。すべてがひとつのファイルにまとまっていると便利なこともあるでしょう。ただし、多くのコンテンツをひとつのファイルに入れると、一般的な電子書籍リーダで読むときに性能上の問題を引き起こすことがあるので、気を付けてください。

複数のファイルをひとつに連結するには、Ctrl キーを押しながらファイルを選択します。(HTML ファイルだけ、CSS ファイルだけ、のように必ず同じ種類のファイルだけを選択してください) 次に右クリックして連結を選択します。これで完了です。calibre はファイルを連結し、連結されたファイルへのすべてのリンクと参照を自動的に処理します。ただしファイルを連結するとテキストのスタイルが変わってしまうことがあります。これは個々のファイルが別々のスタイルシートを使用していることがあるためです。

テキストファイルを選択してそれを別のテキストファイルの上にドラッグ&ドロップして、ドロップしたファイルを対象ファイルに連結させることも可能です。

テキストファイルの並び順を変更

You can re-arrange the order in which text (HTML) files are opened when reading the book by simply dragging and dropping them in the File browser or clicking on the file to move and then pressing the Ctrl+Shift modifiers with the Up, Down, Home or End keys. For the technically inclined, this is called re-ordering the book spine.

Note that you have to drop the items between other items, not on top of them, this can be a little fiddly until you get used to it. Dropping on top of another file will cause the files to be merged.

表紙をマーク

電子書籍には通常、表紙画像があります。この画像は、ファイルブラウザ の中では画像名の隣に茶色の本のアイコンで示されます。もし別の画像を表紙として指定したい場合、そのファイルの上で右クリックして 表紙画像としてマーク を選択します。

さらに、EPUB ファイルには タイトルページ という概念があります。タイトルページは HTML ファイルで、本のタイトルページ/表紙として機能します。EPUB の編集中に HTML ファイルを右クリックしてタイトルページにマークすることもできます。ただしマークするファイルには表紙の情報しか含まれないようにしてください。もしそれ以外のコンテンツが含まれていると、その EPUB ファイルを別の形式に calibre で変換したときに失われてしまうからです。これは変換のときに、calibre はマークしたタイトルページには表紙以外の情報が何もないことを前提としているためです。

ファイルの削除

ファイルを削除するには、ファイルの上で右クリックするか、ファイルを選択してから Delete キーを押します。ファイルを削除すると OPF ファイルからそのファイルへのすべての参照を削除し、自分でそれを行う手間を省いてくれます。ただし他の場所にあるリファレンスは削除されません。本のチェックツールを使用すれば、それを簡単に見つけて削除/置換できます。

ファイルのエクスポート

本の中にあるファイルは、コンピュータ上のどこか別の場所にエクスポートできます。そのファイルを分離して、専用ツールで編集したい場合に便利です。これを行うには、ファイルを右クリックして エクスポート を選択します。

エクスポートしたファイルの作業が完了したら、本の中に再インポートできます。それにはファイルの上でもう一度右クリックして、ファイルを置換 を選択すると、本の中のファイルをさきほどエクスポートしたファイルで置換できます。

複数のエディタ画面間でファイルをコピーすることも可能です。ファイルブラウザ の中からコピーしたいファイルを選択し、次に右クリックして: guilabel:選択したファイルを別のエディタ画面にコピー を選択します。次に、別のエディタ画面を開いて ファイルブラウザ の中で右クリックし、別のエディタ画面からファイルを貼り付け を選択します。

新しい画像/フォントなどを追加、または新しい空のライルを作成

新しい画像、フォント、スタイルシートなどをコンピュータから追加することができます。これには ファイル → 新規ファイル をクリックします。これにより リソースファイルをインポート ボタンをクリックしてファイルをインポートするか、または新規ファイルのボックスに新しいファイル名を入力して、新しい空の HTML ファイルかスタイルシートを作成することが可能です。

ファイル->本にファイルをインポートを使って一度に複数のファイルをインポートすることもできます。

ファイルの置換

ファイルの上で右クリックして置換を選択すれば、本の中の既存のファイルを簡単に置き換えることができます。置換するフィルが置換されるファイルと名前が異なる場合に備えて、すべてのリンクと参照は自動的に更新されます。

スタイルシートを HTML ファイルに効率的にリンク

利便性のために、ファイルブラウザで複数ファイルを選択し、右クリックして "スタイルシートをリンク" を選択すると、選択したすべての HTML ファイルに calibre が自動的に <link> タグを挿入します。

検索と置換

本の編集には、非常に強力な検索と置換インタフェースがあります。これを使用すれば、検索と置換を現在のファイルのテキスト、すべてのファイル、あるいは現在のファイルの選択した範囲内で行うことができます。検索は通常の検索、または正規表現を使って行えます。正規表現を使用して高度な検索を行う方法については、calibre で正規表現を利用するにあたって重要なこと を参照してください。

検索と置換は、メニューの 検索 → 検索/置換 から開始してください。(編集できるのは HTML または CSS ファイルです)

検索したいテキストを検索ボックスに入力し、それと置き換えるものを置換ボックスに入力します。適切なボタンをクリックして、次の一致を検索したり、現在のマッチを置換したり、すべてのマッチを置換することができます。

ボックスの下部にあるドロップダウンを使って、検索範囲を現在のファイル、すべてのテキストファイル、すべてのスタイルファイル、またはすべてのファイルの中から選択できます。また検索モードを、通常 (文字列) 検索と正規表現の中から選択できます。

検索式にマッチした回数は、検索 → 出現回数を取得 で得られます。ドロップダウンボックスで選択したファイル/範囲での出現回数を数えます。

現在開いているエディタの中の指定した行へ移動することもできます。移動は 検索 → 指定行へ移動 を使用します。

注釈

ただし検索と置換を最大限に活用するには、正規表現が欠かせません。calibre で正規表現を利用するにあたって重要なこと を参照してください。

保存済み検索条件

頻繁に利用する検索/置換の式を保存して (関数モードの式を含む) 何度でも再利用することができます。検索を保存するには、検索ボックスの中で右クリックして 現在の検索を保存 を選択します。

保存した検索は 検索 → 保存済み検索 を使用して表示できます。これは利用可能な検索と置換の式を一覧表示します。Ctrl キーを押しながらクリックすることで、一度に複数のエントリを選択して、一回の操作で複数の検索と置換の式を実行することもできます。

関数モード

関数モードを使うと、すべての検索/置換で利用できる任意の強力な Python 関数を作成できます。関数モードでは、テキスト操作でできないことはほぼありません。さらに詳しくは エディタの検索と置換用の関数モード を参照してください。

HTML タグを無視して検索

間にある HTML を無視してテキスト検索を行うために、専用ツールがあます。たとえば本に Empahisis on a <i>word</i>. という HTML があるとします。<i> タグが真ん中にあっても on a word を検索できます。このツールを使用するには、メニューの 検索 → HTML マークアップを無視して検索 を使用します。

自動化ツール

本の編集には、一般的なタスクに役立つさまざまなツールがあります。これらは ツール メニューからアクセスできます。

目次の編集

目次の編集を容易にするための専用ツールがあります。ツール → 目次 → 目次の編集 から起動してください。

目次の編集ツールは現在の目次を (目次があれば) 左側に表示します。テキストを変更するには、変更したいテキストの上でダブルクリックします。エントリの順番を変更するには、ドラッグ&ドロップするか、マウスの右ボタンを使用します。

既存の目次がない本では、ツールは目次をテキストから自動生成するためのさまざまなオプションを提供します。ドキュメント内の見出し、リンク、個別のファイルごとなどから生成できます。

個々の項目をクリックして編集できます。次に この項目が指す場所を変更 ボタンをクリックします。そうすると本のミニプレビューが開くので、マウスカーソルを本の表示パネル上に移動して、項目が指してほしい場所をクリックするだけです。緑色の太線で、位置が示されます。その位置でよければ、OK をクリックします。

本のチェック

本のチェック ツールは実際のリーダデバイス上で意図したとおりに機能しない可能性のある問題を、本の中から探し出します。ツール → 本のチェック から使用してください。

見つかった問題はすべて、使いやすい便利な一覧として表示されます。一覧の中でエントリをクリックすると、そのエラーに関するヘルプが表示され、自動的に修正可能なエラーであれば自動修正のためのオプションが表示されます。エラーの上でダブルクリックすると、エラーの発生した場所をエディタで開くので、自分で修正することもできます。

実行されるチェックには次のようなものがあります:

- 不正な形式の HTML マークアップ。整形式 XML として解析されない HTML マークアップがすべて報告されます。これを修正すれば、すべてのコンテキストでマークアップが意図したとおりに機能するようになります。calibre は自動修正することもできますが、自動修正はときに予期しない影響を及ぼすこともあるので、利用には注意が必要です。いつものように自動修正の前にチェックポイントが作成されるので、変更を元に戻すのは簡単です。自動修正はマークアップを HTML5 アルゴリズムを使用して解析します。これはフォールトトレラント性が高く、整形式 XML に変換します。

- 不正な形式、または不明な CSS スタイル。無効な CSS または CSS 2.1 標準 (および CSS 3 のいくつか) で定義されていないプロパティを持つ CSS がすべて報告されます。CSS はすべてのスタイルシート、インラインのスタイル属性、および HTML ファイル内の <style> タグでチェックされます。

- リンク切れ。本の中へのリンクで、リンク先が存在しないものが報告されます。

- 参照されないファイル。他のどのファイルからも参照されていない、またはスパインの中にないファイルが報告されます。

- OPF ファイルのさまざまな一般的な問題。スパインまたはマニフェスト項目の重複、idref または meta cover タグの破損、必要な章の欠落など。

- さまざまな互換性チェック。リーダデバイス上で本が誤動作する可能性のある既知の問題についてチェック。

表紙の追加

本の表紙は ツール → 表紙を追加 から簡単に追加できます。本の中にすでにある画像を表紙として選択するか、新しい画像を本にインポートして表紙とするか、いずれかの方法で表紙が追加できます。EPUB ファイルを編集するときには、表紙の HTML ラッパーが自動的に生成されます。本にすでに表紙が存在していれば、置き換えられます。ツールはまた、OPF で表紙ファイルを正しく表紙としてマークすることも自動的に処理します。

参照されているフォントの埋め込み

ツール → 参照されているフォントを埋め込む からアクセスできるこのツールは、本の中で参照しているすべてのフォントを探し出し、それがすでに埋め込まれているかどうかを確認します。まだ埋め込まれていなければ、コンピュータ上でフォントを探して本に埋め込みます。これを行う前に、商用ライセンスのフォントを埋め込むにあたっては著作権上の問題がないことを十分確認してください。

埋め込みフォントのサブセット化

ツール → 埋め込みフォントのサブセット化 からアクセスできるこのツールは、本で実際にテキストに使用している文字のグリフだけを残すよう、本の中のフォントを削減します。通常この処理を行うと、フォントファイルのサイズを最大 50% ほど削減します。ただしフォントをサブセット化してしまうと、その後に新しいテキストを追加してその中にフォントに含まれていない文字があると、新しいテキストにはフォントが適用されません。ですからこれは必ずワークフローの中で最後のステップとして行ってください。

句読点のスマート化

プレーンテキストのダッシュ、省略記号、引用符、複数のハイフンなどを、タイポグラフィ的に正しい同等の文字に変換します。ただしこのアルゴリズムは誤った結果を生成することがあります。特に、一重引用符が短縮語の先頭で使われているときにそうなりがちです。ツール → 句読点のスマート化 からアクセスできます。

CSS プロパティの変換

本のスタイルを変えるためのルールを作成します。たとえば赤字のテキストをすべて緑に変換したり、本の中のすべてのテキストのフォントサイズを 2 倍にしたり、ある特定のフォントファミリのテキストを斜体にしたりするようなルールを作成します。

ルールの作成は簡単です。ルールは自然言語の形式に従い、次のようになります:

- If the property color is red change it to green

- If the property font-size is any value multiply the value by 2

ツール → スタイルを変換 からアクセスできます。

使用されていない CSS 規則の削除

使用されていない CSS 規則をすべてスタイルシートおよび <style> タグから削除します。製品テンプレートから作成された一部の書籍には、実際のコンテンツには適用されない余分な CSS 規則が大量に含まれていることがあります。こうした余分なルールは、すべてを処理する必要のあるリーダの性能を落とします。ツール → 使われていない CSS 規則を削除 からアクセスできます。

HTML の修正

このツールは、ただ単に XML として解析できない HTML を整形式の XML に変換するだけです。電子書籍の XML が整形式でないことは非常によくあることですが、このツールはそうした HTML を修正する処理を自動化しただけのものです。ツールは HTML を HTML5 アルゴリズム (すべての最新のブラウザで使用されているアルゴリズム) で解析し、その結果を XML に変換します。ただし自動修正では、直感に反する結果をもたらすことがあります。必要に応じて、上で紹介した本のチェックツールを使って 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 を指定できます。たとえば color, background-color, line-height と指定すると出現したすべての場所から削除されます。スタイル情報を削除した後、すべての変更の概要が表示されるので、何が変更されたのか正確に確認できます。このツールは ツール → スタイル情報を絞り込み からアクセスできます。

本の内部のアップグレード

このツールを使うと、可能であれば本の内部をアップグレードできます。たとえば EPUB 2 の本を EPUB 3 にアップグレードします。このツールは 本の内部をアップグレード からアクセスできます。

チェックポイント

チェックポイント は、本の現在の状態を "特別" なものとしてマークする方法です。マークしたら、好きなように本を変更し、その結果が気に入らなければチェックポイントした状態に戻せます。チェックポイントは、上で説明した自動化ツールを使用するたびに自動的に作成されます。

チェックポイントは 編集 → チェックポイントを作成 から作成できます。前のチェックポイントに戻すには、編集 → 元に戻す ... を実行します。

チェックポイント機能は、個別のファイルを編集するときに通常の「元に戻す/やり直す」メカニズムに追加されます。チェックポイントは変更が本の中の複数のファイルにわたっている場合や、大規模な変更をまとめて元に戻したい場合などに必要です。

利用可能なチェックポイントは 表示 → チェックポイント から確認できます。本の現在の状態と、指定したチェックポイントとは 電子書籍を比較 ツールを用いて比較することが可能です。それには比較したいチェックポイントを選択して、比較 ボタンをクリックします。元に戻す ボタンを使用すると、本を指定したチェックポイントに戻し、そのチェックポイント以降に行われたすべての変更が取り消されます。

ライブプレビューパネル

ファイルプレビュー を使用すると、内部のさまざまなファイルの概要が表示されます。ライブビューパネルには、行った変更がライブで表示されます (ただし 1 秒か 2 秒の遅延があります)。HTML や CSS ファイルを編集すると、ライブビューパネルはその変更を自動的に反映させます。エディタ内でカーソルを動かすと、プレビューパネルはその位置を追跡し、本の対応する場所を表示します。プレビューパネル内でクリックすると、エディタ内のカーソルはクリックした要素のある場所に移動します。本の中の別のファイルへのリンクをクリックした場合には、エディタとプレビュー内にそのファイルを自動的に開きます。

位置の自動的な同期と変更のライブビューは、プレビューパネルの下にあるボタンで無効に設定できます。プレビューパネルのライブ更新はエディタ内で入力中でないときにしか行われないため、気を散らされたり、プレビューのレンダリングを待つせいで遅くなったりすることはありません。

プレビューパネルには、本を読むときテキストがどのように見えるのかを表示します。しかしプレビューパネルは、本を実際のリーダデバイス上でテストすることの代わりにはなりません。実際のリーダに比べて能力が高い部分も低い部分も両方あります。たいていのリーダデバイスに比べると、エラーやずさんなマークアップにも耐えられます。また、ページ余白、改ページ、フォント名のエイリアスを使用する埋め込みフォントを表示しません。プレビューパネルは本の作業中に利用し、それが終わったら実際のリーダデバイスやソフトウェアエミュレータで確認してください。

注釈

フォントファイル内のフォントの名前がCSS@font-face規則にある名前と一致しない場合、プレビューパネルは埋め込みフォントをサポートしません。本のチェックツールを使用すれば、このような問題のあるフォントをすばやく見つけて修正できます。

HTML ファイルの分割

おそらく意外に思われるでしょうが、プレビューパネルの利用法のひとつは、長い HTML ファイルを分割することです。分割するファイルを表示しているときに、プレビューパネルの下にある 分割モード ボタン をクリックします。次に、ファイルを分割したい場所にマウスを移動してクリックします。マウスの動きに合わせて、分割が発生する正確な場所を緑の太線で表示されます。分割したい場所が決まったら、クリックするだけで分割が実行されます。

ファイルを分割すると、ファイルの下半分を指すすべてのリンクと参照が自動的に更新され、新しく分割されたファイルがエディタで開かれます。

ひとつの HTML ファイりうを複数の位置で自動的に分割することも可能です。それにはエディタでファイル内を右クリックし、複数の位置で分割 を選択します。これにより巨大なファイルを、見出しタグだけのファイルや、特定のクラスを持つタグだけ、などに簡単に分割できます。

ライブ CSS パネル

Live CSS パネルには、現在編集しているタグに適用されるすべてのスタイル規則が表示されます。タグの名前とその行番号がエディタに表示され、その後に一致するスタイル規則の一覧が表示されます。

これは、タグに適用されているスタイル規則がどれかを手早く確認できる、優れた方法です。ビューにはクリック可能なリンク (青色) もあり、そのスタイル規則を変更したい場合には、スタイルを定義している場所に直接移動できます。タグに直接適用されるスタイル規則と、親タグから継承される規則が表示されます。

パネルには、そのタグの最終的に計算されたスタイルも表示されます。リスト内のプロパティが優先度の高い規則で上書きされた場合には、打消し線とともに表示されます。

ライブ CSS パネルは 表示 → ライブ CSS で有効にできます。

その他のツール

本の編集に役立つツールは、他にもいくつかあります。

目次ビュー

目次ビューには、本の現在の目次が表示されます。エントリをダブルクリックすると、そのエントリが指す場所がエディタで開きます。右クリックして目次を編集したり、ビューを更新したり、すべての項目を展開したり折りたたんだりできます。目次ビューには 表示 → 目次 でアクセスできます。

本の中身のスペルチェック

スペルチェッカは ツール → スペルチェック からアクセスできます。

単語は、本の中での出現回数とその単語が属する言語とともに表示されます。言語情報は、書誌と HTML ファイルの lang 属性から取得されます。そのおかげで複数の言語のテキストを含む本でもスペルチェッカがうまく機能します。たとえば、次の HTML 抽出では、color という単語はアメリカ英語としてチェックされ、colour という単語はイギリス英語としてチェックされます。

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

注釈

単語をダブルクリックすると、エディタ内でその単語が次に出現する箇所をハイライト表示できます。これは、単語を手動で編集したい場合や、単語がどのような文脈で使われているのかを確認したい場合に便利です。

単語を変更するには、右側に提案されている代替スペルのひとつをダブルクリックするか、自分で修正したスペルを入力して、選択した単語を変更 ボタンをクリックします。これで、本の中にあるその単語がすべて置き換えられます。メインの単語リストで単語を右クリックして、右クリックメニューから単語を簡単に変更することもできます。

無視 ボタンをクリックすると、スペルチェッカに現在のセッションの単語を無視させることができます。 辞書に追加 ボタンをクリックして、ユーザ辞書に単語を追加することもできます。スペルチェッカは複数のユーザ辞書をサポートしており、どの辞書に単語を追加するかを選択できます。

スペルチェッカでは、単にスペルの間違った単語を表示させるだけでなく、本の中のすべての単語を表示させることも可能です。本の中でどの単語が最もよく使われているのか確認して、それぞれの単語に対して検索と置換を実行するときに役に立ちます。

注釈

スペルチェックツールが開いているときにファイルを編集して本に変更を加えた場合は、スペルチェックツールの 更新 ボタンをクリックする必要があります。これをせずにスペルチェックツールを使い続けると、エディタで行った変更が失われる可能性があります。

注釈

スペルチェックツールの実行時にファイルを対象から除外するには、ファイルを除外 ボタンを使用するか、ファイルの開始タグのすぐ下に次のコメントを追加します:

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

新規辞書の追加

このスペルチェッカには、英語とスペイン語のビルトイン辞書が付属しています。自分で辞書を追加するには 環境設定 → エディタ → スペル辞書の管理 から行います。スペルチェッカは、LibreOffice プログラムの辞書 (.oxt 形式) を使用できます。これらの辞書は、 The LibreOffice Extensions repository [https://extensions.libreoffice.org/?Tags%5B%5D=50] からダウンロードできます。

特殊文字の挿入

入力が難しい文字を挿入するには、編集 → 特殊文字を挿入 ツールを使用します。このツールはすべての Unicode 文字を表示するので、入力したい文字をクリックします。Ctrl キーを押しながらクリックすると、選択した文字を挿入した後にウィンドウが勝手に閉じます。このツールを使用すれば、メインテキストや検索と置換ツールなどに特殊文字を挿入できます。

膨大な数の文字があるので、お気に入り の文字を定義して、それが最初に表示されるようにすることができます。文字の上で右クリックするだけで、お気に入りとしてマークできます。お気に入りの文字を右クリックして、お気に入りから削除することもできます。最後に、お気に入りの文字の並び順を変更するには、お気に入りの再配置 ボタンをクリックして、お気に入りの文字をドラッグ&ドロップします。

キーボードを使用して特殊文字を直接入力することもできます。それには文字の Unicode コードを (16進数で) 入力してから、Alt+X キーを押すと、前に入力したコードが対応する文字に変換されます。たとえば、ÿ と入力するには、ff と入力してから Alt+X を押します。ノンブレークスペースを入力するには a0 と入力してから Alt+X を使用し、水平方向の省略記号を入力するには 2026と入力してから Alt+X を押します。他も同様です。

最後に、HTML の名前付きエンティティを使用して特殊文字を入力することもできます。たとえば &nbsp; と入力するとノンブレークスペースに置き換わります。置換が起きるのはセミコロンを入力したときだけです。

コードインスペクタビュー

このビューには、要素に適用される HTML コーディングと CSS が表示されます。このビューを開くには、プレビューパネル内で右クリックして 検査 を選択します。これでその要素の HTML コーディングが見られるようになり、さらに重要なことに、適用される CSS スタイルも確認できます。スタイルを動的に編集すれば、その変更がどのような影響を与えるかをその場で確認できます。ただしスタイルを編集しても、実際には本のコンテンツは変更されません。手軽に試すことができるだけです。インスペクタ内でライブ編集する機能は、現在開発中です。

外部リンクのチェック

このツールを使用すると、本の中にあるすべての外部サイトへのリンクを確認できます。このツールはリンクされている外部サイトにアクセスを試み、アクセスに失敗すると、すべてのリンク切れを修正に便利な形式で報告します。

外部リソースのダウンロード

このツールを使用すると、本に付属していない (つまりインターネット上の場所を指す URL が含まれている) 画像/スタイルシートなどを自動的に本の中にダウンロードできます。ツールはそのようなすべてのリソースを見つけて自動的にダウンロードし、本に追加し、ダウンロードしたファイルを使用するようにすべての参照を書き換えます。

ファイルをタイプ別にフォルダに配置

どこかで拾った EPUB ファイルを編集すると、EPUB 内のファイルがさまざまなサブフォルダに無秩序に配置されていることがよくあります。このツールを使用すれば、タイプに基づいてすべてのファイルをサブフォルダに自動的に移動できます。このツールへは ツール → フォルダに整理 からアクセスできます。ただしこのツールが変更するのは EPUB 内でのファイルの置き場所だけで、ファイルブラウザでどのように表示されるかは変更しません。

その他の電子書籍形式から EPUB にインポート

エディタには、全変換を行うことなく何か別の形式の本にファイルを直接インポートして新しい EPUB とする機能が含まれています。これは、自分で編集した HTML ファイルから EPUB ファイルを直接作成したい場合に特に有用です。これを行うには、ファイル → HTML または DOCX を新しい本としてインポート を実行します。

レポートツール

エディターには優れたレポートツール（ツール → レポート）が含まれています。このツールは本で使用されているファイル、画像、リンク、単語、文字、スタイルの概要を表示します。レポート内の行はすべてホットリンクです。行をダブルクリックすると、本の中でその項目が使われている、または定義されている (と思われる) 場所に移動します。たとえば リンク ビューでは、ソース 列のエントリをダブルクリックすればリンクが定義されている場所に移動し、ターゲット 列のエントリをダブルクリックすればリンク先に移動できます。

コードエディタの特殊な機能
calibre の HTML エディタは非常に強力です。HTML (および CSS) の編集を容易にする機能がいくつも備わっています。

構文の強調表示

HTML エディタが持つ構文の強調表示は、非常に洗練されています。この機能には次のものが含まれています:

- 太字、斜体、見出しタグ内のテキストは太字/斜体になります

- As you move your cursor through the HTML, the matching HTML tags are highlighted, and you can jump to the opening or closing tag with the keyboard shortcuts Ctrl+{ and Ctrl+}. Similarly, you can select the contents of a tag with Ctrl+Alt+T or Ctrl+Shift+T.

- 無効な HTML は赤い下線で強調表示されます

- HTML タグ内のテキストのスペルミスや、title などの属性が強調表示されます。スペルチェックは言語を認識しますが、それは現在のタグの lang 属性の値と全体的な本の言語に基づいて判断しています。

- <style> タグの内側に埋め込まれた CSS を強調表示します

- ノンブレークスペース、さまざまな種類のハイフンなど、識別しにくい特殊文字を強調表示します

- <a> タグ、<img>、および <link> タグ内にある他のファイルへのリンクはすべて、ファイル名が強調表示されます。リンク先のファイルが存在しない場合、そのファイル名を赤い下線でマークします。

状況依存のヘルプ

HTML タグ名または CSS プロパティ名を右クリックすると、そのタグまたはプロパティのヘルプを表示できます。

リンクタグ内のファイル名を Ctrl キーを押しながらクリックすると、そのファイルをエディタで自動的に開きます。同様にクラス名を Ctrl キーを押しながらクリックすると、そのタグとクラスに一致する最初のスタイル規則に移動します。

HTMLファイルの中のクラス名を右クリックすると、クラスの名前を変更できます。このとき、本およびスタイルシートの中でそのクラスが使われていればすべて名前が変更されます。

自動補完

電子書籍を編集するにあたって最も面倒な作業のひとつは、書籍内の他のファイル、CSSスタイルシート、または画像へのリンクを作成することです。正しいファイル名と、ファイルへの相対パスを把握する必要があります。エディタには自動補完があり、作業が簡単になります。

ファイル名を入力すると、自動的にエディタが候補をポップアップ表示します。 タブ キーを使うだけで、正しいファイル名を選択できます。別の HTML ファイル内のアンカもリンク先の候補として挙がります。 # 文字を入力した後、エディタはリンク先ファイル内のすべてのアンカを一覧表示します。一覧には、アンカを正しく選択できるよう小さなテキストのスニペットが表示されます。

ただし他のほとんどの補完システムとは異なり、このエディタの補完システムはサブシーケンスマッチングを使用しています。つまりファイル名のどこか 2 文字または 3 文字を入力するだけで、ファイル名を補完できます。たとえば、ファイル名 ../images/arrow1.png が必要な場合は、ia1と入力してから タブ キーを押せば、ファイル名が補完されます。一致するものを検索するとき、この補完システムは単語の先頭またはパス区切り文字の直後にある文字を優先します。このシステムに慣れると、多くの時間と労力を節約できることがわかるでしょう。

スニペット
calibre 電子書籍エディタは、スニペットをサポートしています。スニペットとは、頻繁に再利用されるか、あるいは冗長なテキストが多数含まれているテキストのことです。エディタを使用すると、数回のキーストロークだけでスニペットを挿入できます。スニペットは非常に強力で、間を行き来できるプレースホルダ、繰り返されるテキストの自動ミラーリングなど、多くの機能を備えています。詳細については、スニペット を参照してください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍の編集 »
- エディタの検索と置換用の関数モード

エディタの検索と置換用の関数モード

エディタの 検索 & 置換 ツールは 関数モード をサポートしています。このモードでは、正規表現 (calibre で正規表現を利用するにあたって重要なこと を参照) と任意の強力な Python 関数を組み合わせて利用することで、あらゆる種類の高度なテキスト処理を行えます。

検索と置換のための 正規表現 モードでは、検索のための正規表現とあわせて見つかったマッチをすべて置換するために使用するテンプレートの両方を指定します。関数モードでは、固定のテンプレートを使用する代わりに、 Python プログラミング言語 [https://docs.python.org] の任意の関数を指定します。これにより単純なテンプレートでは不可能だった多くのことを実行できるようになります。

関数モードを使用するテクニックと構文を、例を用いて説明します。どのように関数を作成するかを、実行するタスクを少しずつ複雑にしながら説明します。

ドキュメントの見出しの大文字と小文字を自動修正

ここではエディタのビルトイン関数を利用して、見出しタグ内のすべてのテキストの大文字と小文字を自動的にタイトルケースに変更します:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

関数にはビルトイン関数 Title-case text (ignore tags) を選択するだけです。これは <h1>some TITLE</h1> のようなタイトルを <h1>Some Title</h1> に変更します。これは見出しタグの内側に別のタグがあっても動作します。

最初のカスタム関数 - ハイフンをスマート化

関数モードの真の力は、テキストを任意の方法で処理する独自の関数を作成できる点にあります。エディタの Smarten Punctuation ツールは個々のハイフンをそのままにしておくため、このツールを使用して em-dash に置換できます。

新しい関数を作成するには、作成/編集 ボタンをクリックして新しい関数を作成し、下の 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() は、マッチしたテキストをすべて返します。そのテキスト内のハイフンを em-dash に置換すればよいのですが、その際にまずダブルハイフンを置換してから、次にシングルハイフンを置換します。

この関数を次の検索正規表現とともに使用してください:

>[^<>]+<

そしてこれはすべてのハイフンを em-dash に置換しますが、置換するのは実際のテキストだけで、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

この関数は以前と同じ検索式で使用します。つまり次のようになります:

>[^<>]+<

そして、これは本のテキストの中にある誤ってハイフンでつながれたすべての単語を魔法のように修正します。主な秘訣は、replace 関数の便利な追加引数のひとつである dictionaries を利用することです。これはエディタ自身が本のテキストのスペルチェックに使用する辞書を参照します。この関数が行うのは、ハイフンで区切られた単語を探し、ハイフンを削除して合成した単語を辞書が認識するか確認し、認識した場合にはもとの単語をハイフンなしの合成語で置換します。

ただしこのテクニックの限界は、単一の言語で書かれた本にしか使えないことです。なぜならデフォルトでは 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 を使用しています。すべて置換 を実行するとき、number はマッチが成功するたびに自動的に数が増やされます。

もうひとつの新しい機能は、replace.file_order の利用です。これを 'spine' に設定すると、この関数を複数の HTML ファイルに対して実行したときに、本に表示される順番でファイルを処理します。詳しくは 複数の HTML ファイルに実行する際にファイル順を選択 を参照してください。

目次の自動生成

最後に、もう少し大がかりなことを試してみましょう。本の中に h1 と h2 タグの見出しがあり、<h1 id="someid">Some Text</h1> のようになっているとします。これらの見出しにもとづいて HTML の目次を自動生成します。下に示すカスタム関数を作成してください:

from calibre import replace_entities from calibre.ebooks.oeb.polish.toc import TOC, toc_to_html from calibre.gui2.tweak_book import current_container from calibre.ebooks.oeb.base import xml2str def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
if match is None:
# All matches found, output the resulting Table of Contents.
# The argument metadata is the metadata of the book being edited if 'toc' in data:
toc = data['toc']
root = TOC()
for (file_name, tag_name, anchor, text) in toc:
parent = root.children[-1] if tag_name == 'h2' and root.children else root parent.add(text, file_name, anchor)
toc = toc_to_html(root, current_container(), 'toc.html', 'Table of Contents for ' + metadata.title, metadata.language)
print(xml2str(toc))
else:
print('No headings to build ToC from found')
else:
# Add an entry corresponding to this match to the Table of Contents if 'toc' not in data:
# The entries are stored in the data object, which will persist
# for all invocations of this function during a 'Replace All' operation data['toc'] = []
tag_name, anchor, text = match.group(1), replace_entities(match.group(2)), replace_entities(match.group(3))
data['toc'].append((file_name, tag_name, anchor, text))
return match.group() # We don't want to make any actual changes, so return the original matched text

# Ensure that we are called once after the last match is found so we can
# output the ToC replace.call_after_last_match = True
# Ensure that when running over multiple files, this function is called,
# the files are processed in the order in which they appear in the book replace.file_order = 'spine'

そしてこれを次の検索式で使用します:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

検索を すべてのテキストファイル に実行すると、"関数からのデバッグ出力" のウィンドウがポップアップ表示され、そこに HTML の目次が出力されているので、そのまま toc.html に貼り付けて利用できます。

The function above is heavily commented, so it should be easy to follow. The key new feature is the use of another useful extra argument to the replace() function, the data object. The data object is a Python dictionary that persists between all successive invocations of replace() during a single Replace All operation.

もうひとつの新機能は 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 クラスのオブジェクトです。便利な属性には、title, authors (著者のリスト) および language (言語コード) があります。

引数 dictionaries

現在の本のスペルチェックに使われた辞書のコレクションを表します。その最も便利なメソッドは dictionaries.recognized(word)` で、渡された単語が現在の本の言語用の辞書で認識さたら True` を返します。

引数 data

This a simple Python dictionary. When you run Replace all, every successive match will cause replace() to be called with the same dictionary as data. You can thus use it to store arbitrary data between invocations of replace() during a Replace all operation.

引数 functions

引数 functions を使うと他のすべてのユーザ定義関数にアクセスできます。コードの再利用に役立ちます。ユーティリティ関数を 1 か所で定義して、他のすべての関数からそれを再利用することができます。たとえば 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']
...

functions オブジェクトを使って、他の関数から再利用できる永続データを格納することもできます。たとえば すべて置換 で実行されたときにデータを収集し、後で別の関数が実行されたときにそれを使用するといったことが可能です。次の 2 つの関数のようなものを考えてみてください:

# 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 の出力は検索/置換が完了した後にポップアップウィンドウに表示されます。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 という 2 つの値を受け取り、検索が複数ファイルを処理するのに本の中に現れた順で、それぞれ降順または昇順に処理するようにします。

最後のマッチが見つかった後にもう一度関数を呼び出させる

上で説明した目次の自動生成の例のように、最後のマッチが見つかった後に関数をもう一度呼び出させると便利な場合があります。これを行うには、次のように、関数の 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] にあります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍の編集 »
- スニペット

スニペット
calibre の電子書籍エディタは スニペット をサポートしています。スニペットとは頻繁に再利用されたり冗長なテキストを多数含むテキストのひとつです。エディタを使用すると、ほんの数回のキーストロークだけでスニペットを挿入できます。たとえば HTML ファイルを編集しているときに頻繁にリンクタグを挿入しているとします。このとき <a とだけエディタに入力して Control+J を押します。するとエディタはそれを次のように展開します:

<a href="filename"></a>

それだけでなくカーソルが置かれた状態で filename という単語が選択されるため、エディタの優れた自動補完機能を使って実際のファイル名を簡単に入力することができます。ファイル名の入力が終わったらもう一度 Control+J を押せばカーソルは <a> タグの間に移動するので、リンクのテキストを簡単に入力できます。テキストの入力が終わったら再び Control+J を押せば、タグを閉じた後の場所に移動します。

エディタのスニペットシステムは非常に洗練されています。いくつかのビルトインスニペットがあり、編集スタイルに合わせて自分で作成することもできます。

ビルトインスニペットに関する以下の解説で、スニペットシステムがいかに強力であるかが明らかになるでしょう。

注釈

スニペットは 検索 & 置換 パネルのエントリフィールドでも利用できますが、プレースホルダ (Control+J を使った移動) は機能しません。

ビルトインスニペット

ビルトインのスニペットを以下に説明します。ただし同じトリガテキストを用いて自分でスニペットを作成することにより、上書きすることができます。

フィラーテキストを挿入 [Lorem]

最初のスニペット、そしてもっとも単純なスニペットはドキュメントにフィラーテキストを挿入するのに使用されるものです。フィラーテキストにはキケロによる哲学的な作品である De finibus bonorum et malorum [https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum] (英訳版) を使用します。これを使用するには HTML ファイルに Lorem とだけ入力して Control+J を押します。こうすると、いくつかの段落で置き換わります。

このスニペットの定義は非常に簡単で、トリガーテキストに Lorem を定義し、テンプレートにはただ単に挿入される直訳テキストだけが定義されています。好みのフィラーテキストを使うよう、簡単にカスタマイズ可能です。

HTML の自己終了タグを挿入 [<>]

では、強力な概念である プレースホルダ の簡単な例を見てみましょう。自己終了タグの <hr/> を挿入したいとします。<> とだけ入力して Control+J を押せば、エディタはスニペットを次のように展開します:

<|/>

ここで、 | 記号が現在のカーソル位置を表します。次に hr` を入力してから Control+J を押すとカーソルがタグの終了位置の後ろに移動します。このスニペットは次のように定義されます:

Trigger: <>
Template: <$1/>$2

プレースホルダは、ドル記号 ($) の後ろに数字がついたものです。スニペットが Control+J を押すことにより展開されると、カーソルは最初のプレースホルダーの位置 (最も数字の小さいプレースホルダ) に移動します。再度 Control+J を押すとカーソルは次のプレースホルダ (その次に数字の小さいプレースホルダ) に移動します。

HTML リンクタグを挿入 [<a]

HTML のリンクタグはすべて同じ構造を共有しています。どれにも href 属性と、開始タグと終了タグの間にテキストがあります。それをもっと効率的に入力するためのスニペットで、プレースホルダの機能をいくつかさらに紹介します。このスニペットを使用するには、<a とだけ入力してから Control+J を押します。エディタはこれを次のように展開します:

<a href="filename|"></a>

それだけでなくカーソルが置かれた状態で filename という単語が選択されるため、エディタの優れた自動補完機能を使って実際のファイル名を簡単に入力することができます。ファイル名の入力が終わったらもう一度 Control+J を押せばカーソルは <a> タグの間に移動するので、リンクのテキストを簡単に入力できます。テキストの入力が終わったら再び Control+J を押せば、タグを閉じた後の場所に移動します。このスニペットは次のように定義します:

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

いくつか新しい機能が出てきました。まず $1 がより複雑になりました。ここではデフォルトのテキスト (filename という単語) を含んでいます。プレイスホルダがデフォルトのテキストを含むとき、スニペットが展開されるときにそのデフォルトテキストが使用されます。また、デフォルトテキストのまま Control+J で使って移動すると、デフォルトテキストが選択されます。テンプレートの重要な部分を埋めるときには、このようにすればデフォルトテキストをリマインダーとして利用するすることが可能です。プレースホルダのデフォルトテキストは次の構文で指定できます: ${<number>:default text}

もうひとつの新しい機能は、2 番目のプレースホルダの後ろにアスタ理数がついていること (${2*}) です。これはテンプレートを展開する前に選択されていたテキストがプレースホルダに使用されることを意味します。この動作を見てみるには、エディタの中で何かテキストを選択し、Control+J を押してから <a と入力し、もう一度 Control+J を押します。するとテンプレートは次のように展開されます:

<a href="filename">whatever text you selected</a>

HTML 画像タグを挿入 [<i]

これは上で説明した HTML リンクの挿入と非常によく似ています。これにより <img src="filename" alt="description" /> タグを簡単に入力し、src および alt 属性の間に飛びます:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

任意の HTML タグを挿入 [<<]

これにより任意の HTML タグを挿入できます (または以前選択したテキストをタグで囲みます)。これを使うには、<< と入力してから Control+J を押してください。エディタが次のように展開します:

<|></>

タグ名、たとえば span を入力してから Control+J を押すと、次のようになります:

<span>|</span>

終了タグが自動的に span となったことにお気づきでしょう。これはプレースホルダのさらに別の機能である ミラーリング により実現されます。ミラーリングは、サンプルのプレースホルダをテンプレート中で複数回指定したとき、2 回目以降の位置は Control+J を押したときに自動的に最初に入力したもので埋められることを意味します。このスニペットの定義は次のようになります:

Trigger: <<
Template: <$1>${2*}</$1>$3

ご覧のとおり、最初のプレースホルダ ($1) は 2 回指定されており、2 回目は終了タグの中で指定されているため、開始タグに入力したものがそのままコピーされます。

クラス属性つきの任意の HTML タグを挿入 [<c]

これは任意のタグを挿入する上記の例と非常によく似ていますが、タグにクラスを指定することを前提としている点が異なります:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

これにより最初にタグ名を入力し、Control+J を押し、クラス名を入力し、Control+J を押してタグの内容を入力し、最後にもう一度 Control+J を押してタグの外側に移動します。終了タグは自動で入力されます。

スニペットを作成

スニペットは非常に優れていて、編集スタイルに合わせて独自のものを作成できます。自分のスニペットを作成するにはエディタの 編集 → 環境設定 → エディタ設定 → スニペットの管理 へ行きます。これにより使いやすいダイアログがポップアップ表示され、自分のスニペットを作れます。スニペットを追加 ボタンをクリックすると、次のようなダイアログが表示されます:

まずスニペットの名前を入力します。後で何のスニペットか識別しやすいよう、わかりやすい名前をつけましょう。次に トリガ を指定します。トリガは、スニペットを展開するために Control+J を押す前に入力する必要のあるテキストのことです。

次にスニペットのテンプレートを指定します。上記の例を必要に応じて変更することから始めるとよいでしょう。最後にスニペットを有効にしたいファイルタイプを指定します。このようにすることで、ファイルタイプごとに同じトリガで違う動作をするスニペットを作成できます。

次のステップは新しく作成したスニペットをテストすることです。下部にある テスト ボックスを利用してください。トリガテキストを入力してから Control+J を押し、スニペットを展開してプレースホルダの間に移動します。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- calibre コンテンツサーバー

calibre コンテンツサーバー
calibre の コンテンツサーバ を使うと、お好きなスマートフォンやタブレット上のブラウザで直接 calibre ライブラリにアクセスして本を読むことができるようになります。その結果、スマホ上に専用の読書/書籍管理アプリをインストールする必要がなくなります。ただブラウザを使用するだけでよいのです。サーバは本をダウンロードして、インターネットに接続されていないときでも本を読めるよう、読んでいる本をオフラインキャッシュに格納します。

目次

- 別のデバイスからコンテンツサーバにアクセス

- ホームネットワーク上のデバイスからサーバにアクセス

- インターネット上のどこかからサーバにアクセス

- サーバのインタフェース

- 本のリスト

- 書籍リーダ

- ブラウザのサポート

- オフラインサポートの有効化

- ユーザアカウントの管理をコマンドラインでのみ行う

- calibre コンテンツサーバを他のサーバと統合

- 仮想ホスト全体の利用

- URL プレフィックスを使用

- 最新の Linux システムで calibre サーバ用のサービスを作成

サーバを開始するには、接続/共有 ボタンをクリックして コンテンツサーバを開始 を選択します。コンピュータのファイアウォールやウィルス対策プログラムから calibre.exe へのアクセスを許可してよいかと尋ねるメッセージが表示されることがあります。許可 または OK をクリックしてください。そしてブラウザ (できれば 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 を実行しているコンピュータ上でのみ機能します。別のコンピュータ/スマートフォン/タブレットなどからサーバにアクセスするためには、次の章で説明するように、もう少し作業を行う必要があります。

別のデバイスからコンテンツサーバにアクセス

一般的に必要となるリモートデバイスのアクセスには 2 つのタイプがあります。より単純な種類である 1 番目は、ホームネットワーク内からのものです。calibre をホームネットワーク上で実行していて、別のデバイスを同じホームネットワーク上に接続している場合、そのデバイスから calibre へは簡単にアクセスできるはずです。

ホームネットワーク上のデバイスからサーバにアクセス

上で説明したように calibre のサーバを開始した後、もう一度 接続/共有 ボタンをクリックします。コンテンツサーバを開始 の代わりに、コンテンツサーバを終了 が表示されるはずです。この項目の右側には IP アドレスとポート番号が記されています。これはピリオドで区切った一連の数字のように見えます。たとえば次のようになります:

Stop Content server [192.168.1.5, port 8080]

この数字はデバイスからサーバにアクセスするのに使用するアドレスを示しています。上の例に従うと、アドレスは次のようになります:

http://192.168.1.5:8080

アドレスの最初の部分は常に http:// です。次の部分は IP アドレスで、カンマの前にある数字です。最後に IP アドレスにコロン (:) をつけた後ろにポート番号をがあります。運がよければ必要なものはこれですべてで、calibre ライブラリをデバイス上で確認できるでしょう。そうでない場合には、先を読み進めてください。

ホームネットワーク接続のトラブルシュート

デバイスからサーバにアクセスできない場合は、次の手順を試してください:

1. サーバと同じコンピュータ上でブラウザからアドレス http://127.0.0.1:8080 を開き、サーバが実行されているかを確認

2. ファイアウォール/ウィルス対策がコンピュータ上のポート 8080 および calibre プラグラムへのアクセスを許可しているかを確認。問題の原因となっているファイアウォール/ウィルス対策を排除する最も簡単な方法は、一時的に両方とも無効にしてから接続を試すことです。コンピュータの安全のため、ファイアウォールを無効にするときにはインターネットから切断しておきましょう。

3. デバイスとコンピュータが同じネットワーク上にあることを確認。つまり両方とも同じ無線ルータに接続されていることを意味します。特に、携帯電話会社または ISP の提供する無線接続はどちらも利用してはいけません。

4. 標準的でないネットワーク設定を使用している場合、接続/共有 に正しくない IP アドレスが表示されることがあります。そのような場合には、正しい IP アドレスを自分で探し出さなくてはなりません。残念なことに、ネットワーク設定には無限の多様性があることを考えると、そのための手順を示すことができません。

5. ユーザ名とパスワードを設定してある場合、最初にそれを使わず試して、問題が発生しないか確認してください。一部の e-ink デバイスでは、ブラウザが認証を扱えません。このようなとき、ユーザ名とパスワードを URL に含めると回避できることがあります。例えば http://username:password@192.168.1.2:8080 のように指定します。

6. 行き詰まってしまったら、いつでも `calibre user forums`_で助けを求めることができます。

インターネット上のどこかからサーバにアクセス

警告

これを行う前に、サーバでユーザ名/パスワード保護を有効にする必要があります。そうしないと世界中の誰でも本にアクセスできてしまいます。環境設定 → 共有 → ネット共有 へ行き、コンテンツサーバへのアクセスにユーザ名とパスワードを要求 オプションを有効にしてください。

インターネットアクセスの設定に関する具体的な詳細はネットワーク設定やコンピュータのタイプによって異なりますが、基本的な概要は次のとおりです。

1. サーバを実行しようとしているコンピュータの外部 IP アドレスを調べます。それには、そのコンピュータ上のブラウザで What is my IP address [https://www.whatismyip.com/] にアクセスします。

2. コンピュータがルータの内側にあるなら、ルータ上でポートフォワードを有効にしてポート 8080 (または calibre コンテンツサーバを実行するポートとして選択したもの) をコンピュータにフォワードします。

3. コンピュータ上でファイアウォール/ウィルス対策プログラムの通過を許可するよう、よく確認してください。

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 のサーバはひとつだけでなく、前と同じようにすべてのライブラリへのアクセスが可能です。

本のリスト

サーバの本のリストは、単純に表紙をグリッド表示します。表紙をタップすれば本の詳しい書誌情報を表示したり、本を読んだりできます。さらに詳細なリストが必要な場合には、右上に置かれた縦に点が 3 個並んだものをクリックすればデフォルトのビューを変更できます。

本の並べ替えや検索は、calibre ユーザにはお馴染みのはずです。それには右上のアイコンをクリックするとアクセスできます。どちらもメインの calibre プログラムと同様に動作します。検索ページでは著者/タグなどをクリックして検索クエリを構築することもできます。メインプログラムでタグブラウザを利用するのとまったく一緒です。

メインプログラムの非常に愛されている機能である 仮想ライブラリ はサーバインタフェース上にも存在します。右上にある縦の 3 つの点をクリックして仮想ライブラリを選択してください。

書籍リーダ

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 サーバでは HTML 5 や CSS 3 の高度な機能をたくさん利用しています。このため、利用するには最新のブラウザが必要です。これは Android Chrome, iOS Safari およびデスクトップ版の Chrome と Firefox でテストされています。

このサーバは、すでに標準化済みの機能または標準化されつつある機能を慎重に使用しています。したがってもしお気に入りのブラウザでうまく表示できなかったとしても、ブラウザが追いつけば表示できるようになるでしょう。

非常に古いブラウザや制限のあるブラウザを使用しているか、JavaScript を実行したくない場合には、サーバのアドレスに /mobile` を追加して モバイル 端末用のビューを利用することができます。

注釈
iOS では、Apple は単一のブラウザエンジンのみを許可しているため、Firefox, Chrome, Safari はいずれも中身は同じブラウザです。新しいサーバインタフェースには、iOS 10.3.2 以降が必要です。Android では、サーバはバージョン 58 以降の Chrome でテストされています。

オフラインサポートの有効化

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-server プログラムのオプションに --manage-users を使ってユーザアカウントを管理できます。ユーザデータベースをフォルダ /srv/calibre の下格納したい場合、次のように作成して実行します:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

プロンプトの指示に従ってユーザアカウントを作成したり、権限を設定したりしてください。それが終わったら、サーバを次のように実行します:

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

これで、前の手順で作成されたユーザアカウントを使用します。

calibre コンテンツサーバを他のサーバと統合

ここでは、calibre コンテンツサーバを他のサーバに統合する方法を紹介します。これを行う最もよくある理由は SSL を利用したい、あるいはより大きなサイトの一部として calibre のライブラリを提供したい場合です。基本的な手法は、calibre サーバを実行し、メインサーバから calibre サーバへのリバースプロキシを設定することです。

リバースプロキシは、通常のサーバが着信要求を受け取ったときに、それを calibre に渡します。そして calibre サーバからの黄湯を読取り、クライアントに転送します。これは calibre をメインサーバと緊密に統合することなく、calibre サーバを単純に実行すればよいことを意味します。

仮想ホスト全体の利用

最も簡単な設定は、仮想ホスト全体を calibre サーバ専用にすることです。この場合、calibre サーバを次のように実行します:

calibre-server

次に、メインサーバで仮想ホストの設定をします。たとえば ngix では次のようにします:

http {
client_max_body_size 64M; # needed to upload large books
}

server {
listen [::]:80;
server_name myserver.example.com;

location / {
proxy_pass http://127.0.0.1:8080;
}
}

または Apache では次のようにします:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

<VirtualHost *:80>
ServerName myserver.example.com
AllowEncodedSlashes On
ProxyPreserveHost On
ProxyPass "/" "http://localhost:8080/"
</VirtualHost>

URL プレフィックスを使用

仮想ホスト全体を calibre 専用にしたくない場合には、URL プレフィックスを使うことができます。calibre のサーバを次のように開始します:

calibre-server --url-prefix /calibre --port 8080

ここでの重要なパラメータは --url-prefix /calibre です。これは /calibre で始まるすべての URL をコンテンツサーバに提供します。これの動作を確認するには、ブラウザで http://localhost:8080/calibre を開いてください。通常のコンテンツサーバの Web サイトが表示されますが、ただし /calibre の下で実行されます。

ngix で必要な設定は次のとおりです:

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/

これで、メインサーバの URL の /calibre の下で calibre のコンテンツサーバにアクセスできるようになります。上記の設定により /calibre の下のすべての要求はポート 8080 で実行している calibre サーバに渡されます。そして --url-prefix のおかげで calibre はそれを透過的に処理します。

注釈

リバースプロキシを使用する場合には、--listen-on 127.0.0.1 を使用して calibre コンテンツサーバにローカルホストでのみ要求を受け付けるよう指示する必要があります。そうすれば、サーバは同じコンピュータ、すなわちリバースプロキシから来た接続要求のみを受け付けるようになります。

注釈

メインサーバに SSL を設定している場合、ダイジェスト認証ではなく基本認証を使用するよう calibre に指示する必要があります。その方が早いからです。それには calibre-server に --auth-mode=basic オプションを渡してやります。

最新の Linux システムで calibre サーバ用のサービスを作成

最新の Linux ベースのシステム (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) ではブート時に calibre を実行するためのサービスを簡単に作成できます。/etc/systemd/system/calibre-server.service を作成して次の内容を入力しておくだけです:

[Unit]
Description=calibre Content server
After=network.target

[Service]
Type=simple
User=mylinuxuser
Group=mylinuxgroup
ExecStart=/opt/calibre/calibre-server "/path/to/calibre library folder"

[Install]
WantedBy=multi-user.target

mylinuxuser と mylinuxgroup の部分はサーバ上で使用したいユーザとグループに置き換えてください。このユーザとグループは calibre ライブラリフォルダの中にあるファイルの所有者でなくてはなりません。ただしサーバを root で実行するのはお勧めできません。calibre ライブラリフォルダへのパスもシステムに合わせて変更してください。必要なら複数のライブラリを追加することができます。calibre-server のヘルプを参照してください。

そして次を実行してサーバを起動します:

sudo systemctl start calibre-server

状態を次のようにして確認してください:

sudo systemctl status calibre-server

ブート時に開始させるには、次を実行します:

sudo systemctl enable calibre-server

注釈
calibre サーバは実行中の X サーバ を必要としませんが、一部コンポーネントが X ライブラリにリンクしているため X ライブラリをインストールしておく必要があります。

注釈
calibre サーバは systemd socket アクティベーションをサポートしているため、必要に応じて利用できます。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍を比較

電子書籍を比較
calibre には統合された電子書籍比較ツールが含まれており、電子書籍を編集あるいは変換した後に電子書籍内で何が変更されたかを確認するために使用できます。EPUB 形式と AZW3 形式の書籍を比較できます。

これを使用するには、ツールで 編集 の電子書籍を開いて ファイル  →  他の書籍と比較 をクリックするか、本の詳細 パネルを使用します。EPUB から EPUB への変換を行うと、元の EPUB ファイルがORIGINAL_EPUBとして保存されます。本の詳細パネルで ORIGINAL_EPUB エントリを右クリックし、EPUB 形式と比較 を選択してください。

表示される比較ツールは、以下のスクリーンショットのようになります。選択した書籍のテキストやスタイル、画像の違いが表示されます。

比較ビューを解読

上のスクリーンショットでわかるように、比較ビューには 2 冊の本の差違が並べて表示されます。表示されるのは、違いのある部分およびその前後数行のみです。こうすることにより本のような大きな文書の中で、何が変更されたかだけが一目でわかりやすくなります。

追加されたテキストは緑色の背景で、削除されたテキストは赤色の背景で、変更されたテキストは青色の背景で表示されます。

変更されたすべてのテキストの行番号が横に表示されるため、エディタで特定の変更に簡単に移動できます。エディタ内から比較ツールを開くと、右側のパネルの行をダブルクリックして、エディタでその行に自動的に移動することもできます。

書籍を比較するときに便利な手法の 1 つは、違いを判断する前にテキストとスタイルファイルをきれいに整えるよう比較ツールに指示することです。これにより、多くの場合、よりクリーンで違いをたどりやすくなります。これを行うには、右下の オプション ボタンをクリックし、比較する前にファイルを整形 を選択します。ただし整形すると、無効なマークアップを有効に変更してしまう可能性があり、望ましくない効果をもたらすことがあります。また、違いのある部分と一緒に表示する前後の行数は、オプション ボタンで変更することができます。

見つかった違いの中のテキストは、下部の検索バーから検索できます。検索するパネル (左 または 右 ) を指定する必要があります。

比較ツールを起動

比較ツールが最も役立つのは、同じ書籍に 2 つのバージョンがあって、それらの間で何が違うのかを確認したい場合です。そのため、ツールを起動する方法はいくつか用意されています。

2 つの電子書籍ファイルを比較

最初のファイルを 編集 ツールで開きます。次に、ファイル → 別の本と比較 をクリックし、2番目のファイルを選択します(最初のファイルと同じ形式である必要があります)。比較ビューが開き、右側に編集中のファイル、左側に2番目のファイルが表示されます。

ORIGINAL_FMT と FMT を比較
calibre で FMT から FMT へ変換を行うと、元のファイルが ORIGINAL_FMT として保存されます。変換によって何が変更されたかを確認するには、メインの calibre ウィンドウの 本の詳細 パネルの ORIGINAL_FMT エントリを右クリックし、FMTと比較 を選択します。比較ビューが開き、左側にORIGINAL_FMT、右側にFMTが表示されます。

編集中のチェックポイントと本の現在状態を比較

編集 ツールには チェックポイント と呼ばれる非常に便利な機能があります。これにより、本の現在の状態を名前付き checkpoint として保存し、チェックポイントの作成以降に行った変更が気に入らない場合は元に戻すことができます。チェックポイントは、エディタでさまざまな自動アクションを実行するときにも自動的に作成されます。チェックポイントのリストを表示するには、表示 → チェックポイント に移動し、次に 比較 ボタンを使用して、選択したチェックポイントのブックを現在の状態と比較します。比較ツールは、左側にチェックポイントを表示し、右側に現在の状態を表示します。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- 電子書籍の書誌の編集

電子書籍の書誌の編集

目次

- 書誌を一冊ずつ個別に編集

- 書誌のダウンロード

- 本の形式を管理

- カバーに関するいろいろ

- 書誌を複数冊まとめて編集

- 検索と置換

- 書誌の一括ダウンロード

- Adding extra data files to a book

電子書籍にはありとあらゆる形とサイズのものがあり、そしてしばしば書誌 (タイトル/著者名/シリーズ名/出版社のような情報) が不完全であったり不正確だったりします。calibre で書誌を変更する最も簡単な方法は、エントリの上でダブルクリックして正しい情報を入力して置き換えることです。もっと洗練されて "強力な" 編集を行うには、以下に説明する書誌編集ツールを使ってください。

書誌を一冊ずつ個別に編集

編集したい本をクリックし、書誌を編集 ボタンをクリックするか、E キーを押してください。ダイアログが開き、書誌に関するすべての情報を編集できるようになります。効率を上げて素早く編集するための機能も豊富です。よく利用されるコツには次のようなものがあります。

- タイトルと著者名の間にあるボタンをクリックすると自動的に入れ替えることができます。

- 著者読みの隣にあるボタンをクリックすると、それぞれの著者ごとに保存されている読みの値を使って calibre が自動的に入力します。著者名の読みを変更するには 著者を管理 ダイアログを利用してください。このダイアログを開くには、著者名読みの隣にあるボタンをクリックして長押しします。

- タグの隣のボタンをクリックすると、本に関連付けられたタグを管理するために タグエディタ を利用できます

- "ID" のボックスにはISBN (およびその他の ID) を入力するのに利用できます。無効な ISBN を入力すると背景が赤くなり、有効な ISBN であれば緑になります。

- 著者名読みの値が calibre が本来あるべきと考えるものと異なる場合、著者名読みの欄が赤くなります

書誌のダウンロード

書誌編集ダイアログの機能の中で最も優れているのは、いろいろな Web サイトから書誌を収集して自動的に書誌フィールドを埋めることができる点です。現在のところ calibre はGoogle ブックスと Amazon を利用しています。書誌ダウンロードで入力できるのは、タイトル、著者名、シリーズ名、タグ、評価、説明、ISBN です。

ダウンロードを使用するには、タイトルと著者名の欄を埋めてから 書誌を取得 ボタンをクリックします。calibre はそのタイトルと著者名に最も近い本の一覧を提供します。最初に ISBN を入力すると、タイトルと著者名の情報よりも優先して利用します。一致するものが見つからない場合には、タイトルをいくつかのキーワードに絞ったり、著者名に姓のみを指定したりして条件を少し緩めてみてください。

本の形式を管理
calibre では、一冊の本にたくさんの異なる 形式 を関連付けることができます。たとえばシェイクスピア全集を EPUB 形式で入手し、その後 Kindle で読むために MOBI に変換したとします。calibre は自動的に複数の形式を管理してくれます。書誌編集ダイアログの 利用可能な形式 でこうした形式を管理できます。新しい形式を追加したり、既存の形式を削除したり、別の形式にある書誌や表紙を設定したりすることが可能です。

カバーに関するいろいろ

本の ISBN がわかっていれば、calibre に表紙をダウンロードさせることができます。またはコンピュータ上のファイルを指定して表紙として使用することもできます。基本的な書誌をもとにして calibre がデフォルトの表紙を生成することも可能です。表紙を変更するには画像を表紙の上にドラッグ&ドロップするか、右クリックして画像をコピー/貼り付けを行います。

さらに、表紙画像に見苦しい境界線がある場合のために、表紙の境界線を自動的にトリミングするボタンがあります。

書誌を複数冊まとめて編集

まず、編集したい本を選択するために Ctrl または Shift を押しながら本の上でクリックしてください。複数の本を選択しして 書誌を編集 ボタンをクリックすると、書誌の 一括 編集ダイアログを開きます。このダイアログを使用すると、複数の本の著者/出版社/評価/タグ/シリーズなどに手早く同じ値を設定できます。書誌の一部に共通な値を持つ本を複数まとめてインポートした場合などには、特に有用です。このダイアログはとても強力で、例えば 検索と置換 タブでは書誌の一括操作を行ったり、書誌のある欄を別の欄にコピーしたりすることができます。

通常の書誌編集ダイアログには 次 および 前 ボタンがあり、本の書誌を次々と編集できるようになっています。

検索と置換

書誌を一括編集 ダイアログでは、選択した本に対して強力な検索と置換を実行することができます。デフォルトでは単純なテキスト検索と置換を行いますが、正規表現を使用することも可能です。正規表現に関して詳しくは calibre で正規表現を利用するにあたって重要なこと を参照してください。

前述のように検索と置換には、文字列検索と正規表現という二つのモードがあります。文字列検索では 検索フィールド で指定した列を、検索語 に指定した文字列で検索し、置換 に指定した文字列で置き換えます。検索語が見つかるたびに置き換えられます。例えば検索される文字列として a bad cat があったとします。これに対して a を検索して HELLO に置き換えるよう指定して実行すると、結果は HELLO bHELLOd cHELLOt となります。

もし検索している対象がタグのように 複数 のフィールドの場合、タグはそれぞれ個別に扱われます。例えばタグに Horror, Scary と設定されていて検索語に r, を指定したとすると、検索に一致するものは見つかりません。なぜならまず Horror を検索し、次に Scary を検索することになるからです。

検索で大文字と小文字の違いを無視したい場合には、大文字・小文字を区別 のチェックを外してください。

結果 (置換を実行した後の情報) の大文字と小文字は、置換後に変換操作を行う から機能を選択して calibre に変更させることができます。利用可能な操作は、次のとおりです。

- 小文字 -- フィールド内のすべての文字を小文字に変更します

- すべて大文字 -- フィールド内のすべての文字を大文字にします

- 先頭を大文字 -- 結果の中の単語ごとに先頭の文字を大文字にします

テスト 欄は、検索/置換がやりたいことをきちんとやれるかを確認するために提供されています。多くの場合本のテストボックスで十分な確認ができますが、ここに表示されていない例でテストしたい場合が出てくることもあり得ます。こうしたときに テスト を入力してください。

正規表現モードでは、正規表現を使用するという以外にも文字列検索モードと異なる部分が何点かあります。1 点目は、フィールド全体ではなく正規表現にマッチした部分にのみ適用されるという点です。2 点目は、フィールド全体ではなく置換文字列に適用される点です。

そして最も重要である 3 点目は、置換文字列は逆参照を使用することによって検索文字列の一部を参照することが可能である点です。逆参照は \\n です。n は整数で、検索式の中の n 番目にカッコで囲まれた部分を参照します。例えば上記の例で a bad cat に対して検索式に a (...) (...) を指定し、置換に a \2 \1 と指定したとすると、結果は a cat bad となります。逆参照について詳しくは calibre で正規表現を利用するにあたって重要なこと を参照してください。

便利なパターンの例: フィールド全体を変更したいとします。一番簡単なのは文字列検索モードを使用することですが、ここでは正規表現モードを使用したかったとします。検索式を (^.*$) として、置換を \1 とすれば、選択した大文字・小文字の変更機能を望んだとおりに適用できます。

最後に、正規表現モードでは値を別のフィールドにコピーすることができます。検索フィールドと対象フィールドに別のものを選択するだけで、そうすることが可能です。コピーすると対象フィールドの内容を置換するか、フィールドの前に追加するか、フィールドの後ろに追加することができます。'カンマを使用' にチェックするともとのテキストと追加されるテキストの間にカンマを追加します。対象フィールドが複数 (例: タグ) の場合にはこのボックスのチェックは外せません。

検索と置換は、書誌に対する他のタブでの変更がすべて終わった後に実行されます。これによって混乱が引き起こされる可能性があります。なぜならテスト欄に表示される内容は変更が適用される前のものであるにもかかわらず、操作は変更された後のものに対して適用されるからです。何が起こるかわからない場合には、他の変更と検索/置換を混在させないでください。

書誌の一括ダウンロード

複数の本の書誌を一括してダウンロードしたい場合、書誌を編集 ボタンを右クリックして 書誌をダウンロード を選択します。ダウンロードするものは、書誌のみ、表紙のみ、または両方の中から選択できます。

Adding extra data files to a book
calibre can store any number of extra data files associated to a book. These can be alternate covers, supplementary material, etc. They cannot be viewed directly or used as conversion sources. Nor are they indexed by the Full text search engine in calibre. To view/add/delete them select the book and right click the Edit metadata button and choose Manage data files. This will pop-up a window where you can perform operations on these files. Alternately, you can right click the Add books button and choose Add data files to selected book records to more quickly add data files.

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

注釈

RPC は一般的なフォーマットで、calibre は TextRead および MOBIBook ヘッダを持つ RPC ファイルをサポートしています。PDB も一般フォーマットです。calibre は eReader, Plucker (入力のみ), PML および zTxt PDB ファイルをサポートしています。DJVU のサポートは、埋め込みテキストを含む DJVU ファイルを変換するためのものです。通常、OCR ソフトウェアによって生成されます。MOBI 書籍には Mobi6 および KF8 という 2 種類があります。calibre はどちらも完全にサポートしています。MOBI ファイルはしばしば .azw または .azw3 という拡張子を持ちます。Microsoft Word 2007 以降による DOCX ファイルがサポートされています。

変換に最も適したフォーマットは何ですか?

好ましい順に: LIT, MOBI, AZW, EPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

PDFファイルを変換したものの、結果に問題がたくさんあります

PDF は入力フォーマットとしては最悪です。PDF を変換するときに遭遇する問題点の一覧は PDF ドキュメントの変換 を参照してください。

英語以外の文字やスマート引用符を含むファイルを変換するにはどうすればよいですか?

この問題には 2 つの側面があります:

1. ソースファイルのエンコーディングを知ること: calibre はソースファイルが使用する文字コードを推測しようとしますが、それが不可能なこともよくあるため、エンコーディングに何を使っているのかを教えてやる必要があります。これは変換ダイアログの 外観 → テキスト にある 入力文字コード を通して GUI から指定ができます。コマンドラインツールであれば、 ebook-convert-txt-input --input-encoding があります。

2. HTML ファイルを calibre に追加するとき、そのファイルの文字コードが何かを calibre に教えてやる必要があるかもしれません。これを行うには 環境設定 → 高度な設定 → プラグイン → ファイル形式 へ行き、HTML to ZIP プラグインをカスタマイズしてファイルに使用している文字コードを指定します。これで HTML ファイルを追加するときに正しく処理されるようになります。ソースの異なる HTML ファイルは異なる文字コードを使用していることも多いため、この設定は繰り返し変更する必要があるかもしれません。Web 上のファイルの多くで一般的に使用しているのが cp1252 なので、最初にこの文字コードを試してみることをお勧めします。ただし HTML ファイルを変換するときには、上で述べた入力文字コードの設定は空欄にしておいてください。これは、HTML to ZIP プラグインは自動的に HTML ファイルを標準エンコーディング (UTF-8) に変更するためです。

MOBI ファイルの目次は一体どうなっているんですか？

最初に気づくことは、ほとんどの電子書籍には目次が 2 つあることです。ひとつは紙の本で見るような伝統的な目次です。この目次はメインのドキュメントフローの一部であり、好きなようにスタイル変更することが可能です。この目次は コンテンツ目次 と呼ばれています。

そしてもうひとつ、書誌目次 というものがあります。metadata ToC は本のテキストの一部ではなく、通常はリーダーの特別なボタンからアクセスされる目次です。たとえば calibre 電子書籍ビューアでは、この目次を表示するのには "目次を表示" ボタンを使用します。この目次は本の作成ツールでスタイルを変更することができません。どのように表示されるかはビューアプログラム次第になります。

MOBI 形式においては、状況は少し混乱しています。これは MOBI 形式が、主流となる電子書籍フォーマットの中で唯一、書誌目次をまともに サポートしていない ためです。MOBI の本は末尾に 余分の コンテンツ目次を配置することによって書誌目次が存在するかのように振る舞います。Kindle 上で "目次へ行く" をクリックすると、Kindle が飛んでいく先はこの余分なコンテンツ目次です。

さて、MOBI の本にはまるで 2 のまったく同じ目次があるかのように見えるかもしれません。しかしたとえまったく同じエントリを持ち、同じに見えたとしても、意味の上では片方はコンテンツ目次であり、もう一方は書誌目次なのです。片方は Kindle のメニューから直接アクセスすることができますが、もう一方はできません。

MOBI に変換するとき、calibre が入力ファイルの中に 書誌目次 を検出すると、出力する MOBI ファイルの末尾に目次を生成します。これは MOBI 出力設定の中のオプションで無効にできます。この目次は意味の上では 書誌目次 であり、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 ファイルを GUI に追加して 変換 ボタンを使って電子書籍を作成します。変換ダイアログの目次セクションにあるオプションを使って、目次の生成方法を制御することができます。

注釈

デフォルトでは、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 の順に並べたいのであれば 名前の順 とするよう指示する必要があります。これを行うには 環境設定 → 高度な設定 → プラグイン → ファイル形式 へ行き、HTML to ZIP をカスタマイズします。

calibre で作成した EPUB は非適合ではありませんか？

calibre は作成する EPUB の適合性を保証することはできません。calibre に保証できるのは、XHTML 1.1 + CSS に適合したファイルが与えられば出力する EPUB は適合したものになるということだけです。calibre は作成する EPUB がさまざまなデバイス上で意図したとおりに動作するするよう出来る限りの努力を行いますが、その目標は適合した EPUB を作ることとは両立せず、大多数のユーザにとってはこちらの方が重要です。必ず適合した EPUB を作成するツールを必要としているなら、calibre はそれには適していません。つまり、もしも calibre で作成した EPUB を EPUB 適合性チェッカーを使うオンラインストアに送信したい場合には、EPUB が適合していることを自分で確認する必要があります。calibre が適合性を確認することはありません。言い換えると、適合した XHTML + CSS を入力として calibre に与える必要があります。

変換ツールの高度な機能を利用するにはどうすればよいですか？

GUI の中でコンバータの個々の機能の上にマウスカーソルを置くか、ターミナルから ebook-convert dummy.html .epub -h を実行することでヘルプを表示できます。まず手始めに、高度な機能を実演するデモファイル html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip] を参照してください。

デバイスの統合

目次

- calibre はどのデバイスに対応していますか？

- 自分のデバイスを calibre でサポートしてもらうようにするにはどうすればよいですか？

- calibre がデバイスを検出してくれません

- デバイスが非標準品または普通ではないものです。どうすれば接続できますか？

- どうすれば iPad/iPhone/iPod touch で calibre が利用できますか？

- How do I use calibre with my Android phone/tablet or Kindle Fire?

- Kindle や他の読書デバイス上の Web ブラウザから calibre の本にアクセスできますか？

- calibre からメールが送れません

- デバイスが Linux で読み取り専用でマウントされていて、calibre に接続できません

- どうして calibre は Kindle のコレクションや Nook の本棚をサポートしないのですか？

- calibre を Kobo Touch/Glo などと一緒に使おうとするとエラーが出ます

- e-ink Kindle に送信した本の表紙が一瞬表示されてすぐに一般の表紙に置き換わってしまいます

- MOBI ファイルの表紙が Kindle for PC や Kindle for Android/iPad などで表示されなくなりました

- calibre で Kindle に本を送信したのに表示されません

calibre はどのデバイスに対応していますか？

calibre はすべてのメジャーな (そしてほとんどのマイナーな) 電子書籍リーダ、スマートフォン、タブレットなどに直接接続することができます。加えて フォルダに接続 機能を使えば、電子書籍をエクスポートして USB ディスクとして利用することもできます。さらに、Web ブラウザを持つデバイスであれば calibre のコンテンツサーバを利用すれば無線で接続することも可能です。

自分のデバイスを calibre でサポートしてもらうようにするにはどうすればよいですか？

もしデバイスが OS に対して USB ディスクとして見えるなら、calibre にサポートを追加するのは簡単です。必要となるのは次のような情報だけです:

- デバイスがサポートする電子書籍のフォーマットの完全な一覧

- すべての電子書籍を置かなくてはならない特別なフォルダがデバイス上にあるか。デバイスはサブフォルダに置かれたファイルを検出するか。

- calibre が自動的に収集するデバイス情報も必要です。まず、デバイスが SD カードをサポートしているなら挿入します。それからデバイスをコンピュータに接続してください。calibre で 環境設定 → その他 へ行き、"デバイス認識をデバッグ" をクリックしてください。これによりデバッグ出力が生成されます。これをファイルにコピーして、次はデバイスをコンピュータから取り外した状態で同じことを繰り返してください。

- 上の出力を両方とも他の情報とともに我々に送ってくだされば、デバイス用のデバイスドライバを作成します。

特定の OS の出力を送ってくだされば、calibre の次のリリース時にその OS 上でデバイスがサポートされます。出力を送るには、バグレポートを開いてその出力を添付してください。バグの報告方法 [https://calibre-ebook.com/ja/bugs] を参照してください。

calibre がデバイスを検出してくれません

問題を特定するために次の手順に従ってください:

- コンピュータに一度に接続しているデバイスはひとつだけであることを確認してください。 iPhone/iPad などのデバイスをサポートする別の calibre を同時に起動していないこと。

- もし Apple iDevice (iPad, iPod touch, iPhone) と接続中であれば、Apple はサードパーティ製のソフトウェアによる USB ケーブルを使ったデバイス接続を認めなくなりました。代わりにコンテンツサーバ経由で無線接続を利用してください。

- もし Kindle Fire や別の Android デバイスと接続中であれば、USB ケーブルを使用 にある注意書きを読んでください。

- macOS でデバイスを calibre に接続するときにアクセス拒否エラーがでる場合、システム環境設定 > セキュリティとプライバシー > プライバシー > ファイルとフォルダ を調べて修正することができます。

- 最新版の calibre (現在は 7.10.0) を実行していることを確認してください。最新版はいつでも calibre の Web サイト [https://calibre-ebook.com/ja/download] からダウンロードできます。現在実行中の calibre のバージョンは、メインの calibre ウィンドウの下部で確認できます。

- OS に デバイスが見えていることを確認してください。つまりデバイスは Windows エクスプローラ (Windows上) または Finder (macOS上) に表示されるはずです。

- calibre で 環境設定 → 無視したデバイス へ行き、無視したくないデバイスをすべてチェックします

- 上の手順すべてに失敗した場合は、環境設定 → その他 へ行き、デバイスを接続した状態で デバイス認識をデバッグ をクリックし、その出力を calibre バグトラッカ [https://bugs.launchpad.net/calibre] にチケットとして投稿してください。

デバイスが非標準品または普通ではないものです。どうすれば接続できますか？

接続/共有 ボタンの下にある フォルダに接続 に加え、calibre は ユーザ定義 デバイスプラグインを提供しており、OS のディスクドライブとして表示される USB デバイスを接続するのに利用できます。注意: Windows 上では calibre がドライブを利用するのにドライブレターが必要です。デバイスプラグインは 設定 -> プラグイン -> デバイスプラグイン -> User defined にあります。詳しくは 環境設定 -> その他 -> ユーザ定義デバイス設定用情報` を参照してください。ユーザ定義プラグインを calibre がビルトインプラグインで普通に認識するデバイスに対して使用するには、代わりにユーザ定義プラグインが利用されるよう、まずビルトインプラグインを無効する必要があります。

どうすれば iPad/iPhone/iPod touch で calibre が利用できますか？

Apple のデバイスから calibre のコレクションを見るのに一番簡単な方法は calibre コンテンツサーバー を使うことです。こうすればネット上でコレクションを利用できるようになります。まず calibre で次の手順に従ってください

- calibre の "優先する出力フォーマット" を EPUB に設定 (出力フォーマットは 環境設定 → インタフェース → 動作 で設定できます)

- 出力プロファイルを iPad に設定 (iPhone/iPod でも動作します)。この設定は 環境設定 → 変換 → 共通オプション → ページ設定 にあります。

- iDevice で読みたい本を選択し、変換 ボタンをクリックしてEPUB 形式に変換

- 接続/共有 ボタンをクリックしてコンテンツサーバを起動し、calibre を実行したままにしておきます。環境設定 → 共有 → ネット共有 でコンテンツサーバを自動起動するよう指定することも可能です。

コンテンツサーバにより、本を Safari で直接読むことができるようになります。さらに iDevice には calibre のコンテンツサーバに接続できるアプリケーションがたくさんあります。例としては Marvin, Mapleread, さらには iBooks などがあります。

コンテンツサーバの利用

Safari ブラウザを開始して、現在 calibre のサーバを実行中のコンピュータの IP アドレスとポート番号を次のように入力します:

http://192.168.1.2:8080/

192.168.1.2 の部分は calibre を実行中のコンピュータのローカル IP アドレスに置き換えてください。サーバの実行や正しい IP アドレスの見つけ方などについて詳しくは calibre コンテンツサーバー を参照してください。

Safari の上で本の一覧が表示され、本の上でタップするとダウンロードするかブラウザ上でそのまま読むかを選択できます。ダウンロードを選択すると、Safari は それを iBooks で開くかどうかを尋ねます。

多くのリーダアプリは OPDS をサポートすることにより calibre ライブラリを直接見ることができます。そのようなアプリではオンラインのカタログスクリーンへ行き calibre サーバの IP アドレスを追加して、アプリの中から calibre ライブラリの本を読んだりダウンロードしたりできるようになります。

How do I use calibre with my Android phone/tablet or Kindle Fire?

Android デバイスを calibre に接続するには、2 種類の方法があります。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 デバイスは同時にひとつしか接続できません。OS のファイルマネージャや Android のファイル転送ユーティリティでデバイスを開きっぱなしにしないよう注意してください。

注釈

新しい Android デバイスでは、うまく接続させるためにいくつか障害を乗り越える必要があります。なぜなら Google は利用者が彼らのクラウドから独立することを望んでいないからです。まず、USB ケーブルをつなぐ前にスクリーンのロックを解除してください。USB ケーブルをつなぐとポップアップ通知が表示されます。その通知内容が「メディアファイルの転送」や「MTP (Media Transfer mode)」のようなものであることを確認してください。そうでない場合には通知をタップしてモードをメディア転送 (MTP) に変更してください。デバイスを認識させるためにこの時点で calibre を再起動する必要があることもあります。そして実際に calibre や OS に接続しようとするたびに許可を求めるポップアップが表示されることがありますが、その都度 OK をタップしてください。

注釈

Kindle Fire 8 以降では USB ケーブルをつないでいるときに充電中であることを示すアイコンが表示されます。それをタップし、デバイスをデータ転送モードに変更して calibre を起動すれば、認識されるようになります。

無線接続
calibre にはビルトインの Web サーバである calibre コンテンツサーバー があります。これを使うとネット上で 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 コンテンツサーバー を参照してください。

これで本のコレクションを閲覧したり、calibre からダウンロードした本を何らかの電子書籍リーダソフトを使って Android デバイス上で本を開くことができるようになります。

多くのリーダアプリは OPDS をサポートすることにより calibre ライブラリを直接見ることができます。そのようなアプリではオンラインのカタログスクリーンへ行き calibre サーバの IP アドレスを追加して、アプリの中から calibre ライブラリの本を読んだりダウンロードしたりできるようになります。

Kindle や他の読書デバイス上の Web ブラウザから calibre の本にアクセスできますか？

calibre には本を Web ページとしてエクスポートできる コンテンツサーバ があります。詳しくは calibre コンテンツサーバー を参照してください。

Kindle (1/2/DX) のような一部のデバイスはポート 8080 のアクセスを許していません (コンテンツサーバが実行されるデフォルトのポート)。この場合には calibre の環境設定でポートを 80 に変更してください。(一部の OS ではセキュリティの設定により 1024 より若いポート番号ではサーバを実行できないことがあります。この場合に一番簡単な解決方法はルータにポート 80 への要求をポート 8080 に転送させるよう調整することです)

また一部のデバイスでは、コンテンツサーバが使用するアプリ風インタフェースを実行できるだけの高度な機能を持っていません。そのようなデバイスでは、サーバの URL の後ろに /mobile を追加して、JavaScript を使用しない簡易的なインタフェースを使用してください。

calibre からメールが送れません

電子メールでの大量のスパムを防ぐためにメールをブロックする戦略は、メールサーバごとに異なっているので、メール送信にはちょっとしたコツがいる場合があります。最も一般的な問題は、calibre から (メール中継なしで) 直接電子メールを送信しようとする場合です。多くのサーバ (たとえば Amazon など) は、よく知った中継装置からのメール以外ブロックします。calibre からメール送信する最も安全な方法は次のようにします:

- GMX [https://www.gmx.com] で GMX アカウントを作成

- calibre で 環境設定 → 共有 → メール共有 へ行き GMX を利用 ボタンをクリックして必要項目を入力

- Web サイト上で GMX アカウントにログインして SMTP 送信を有効にします (Settings->POP3 & IMAP->Send and receive emails via external program)

- これで calibre は メール送信に GMX を使えるようになります。

- Kindle 宛てに送信する場合、忘れずに Amazon の Kindle ページのメール設定で GMX のメールアドレスから送信されたメールを許可してください。そして Amazon は AZW3 および新しいスタイルの MOBI ファイル (KF8) をメールで配信するのを許可していません。さらに Amazon は最近、本を実際に配信する前にクリックする必要のある確認メールを GMX アカウント宛てに送信するようになりました。

これを行った後でもまだ、問題が起きる可能性があります。よくある原因のひとつとしては、出来の悪いウィルス対策プログラムが calibre のメール送信をブロックしていることがあります。ウィルス対策プログラムの除外リストに calibre を追加してみてください。

注釈

Microsoft/GMX は大量のメール送信を行うとアカウントがブロックされることがあります。したがってこのサービスを利用して本を送信する場合、 calibre は 5 分間に 1 冊ずつ送信するよう自動で制限しています。アカウントがブロックされるリスクを気にしないのであれば、calibre の 環境設定 → 高度な設定 → Tweaks へ行き、送信間隔の時間を短く設定することもできます。

注釈

Google は最近、より多くの広告を見せられるようユーザに Web インタフェースの利用を強制するため、電子メール送信プロトコル (SMTP) のサポートを意図的に打ち切りました。彼らは SMTP が安全ではないと主張していますが、単なる言い訳にすぎずその主張は間違っています。代わりに別のメールプロバイダを利用してください。

注釈
calibre にメールアカウントへのアクセスを許可することに不安を感じるのであれば、GMX または Outlook に calibre だけが使用する新しいフリーメールのアカウントを作成してください。

デバイスが Linux で読み取り専用でマウントされていて、calibre に接続できません

Linux のカーネルはデバイスのファイルシステムにエラーがあると読み取り専用でマウントします。ファイルシステムの修復は次のように行います:

sudo fsck.vfat -y /dev/sdc

/dev/sdc の部分はデバイスのデバイスノードへのパスに置き換えてください。デバイスノードは必ず /dev の下にあり、次の出力を調べることで見つけられます:

mount

どうして calibre は Kindle のコレクションや Nook の本棚をサポートしないのですか？

Kindle も Nook も USB 接続を介してコレクションを操作する方法を提供していないためです。もしも心の底からコレクションを使いたいと思っているなら、Kindle/Nook を売り払って Kobo を入手することを強くお勧めします。e-ink スクリーン上で一冊ずつコレクションに本を加えていくなんてことをやってられほど人生が長くないことを理解しているのは、どうやら Kobo だけのようなのです 😇

Kindle に関して言えば、コレクションを USB 経由で操作する方法はあります。しかし変更を認識させるためにはコンピュータから 切断するたびに 再起動する必要があります。そんなありさまなので calibre の開発者がサポートしようと思うだけのモチベーションがまずわきません。ただし、calibre の書誌からコレクションを作成するプラグインなら存在します。 こちらから [https://www.mobileread.com/forums/showthread.php?t=244202] 入手できます。

注釈

Kindle Touch や Kindle Fire のような新しいモデルでは Amazon はコレクションを操作する機能を完全に削除してしまいました。Kindle を root 化してカスタムファームウェアをインストールない限り、上記のプラグインさえ使い物にならなくなっています。

calibre を Kobo Touch/Glo などと一緒に使おうとするとエラーが出ます

Kobo のファームウェアはバグだらけです。接続がランダムに失敗することが知られています。マザーボードと USB ポート/ケーブル/Hub の特定の組み合わせで、その傾向は悪化します。 touch の calibre への接続中にエラーが出るなら、次のことを試してみてください。運がよければ 問題が解決するかもしれません。

- USB Hub を使わず Kobo をコンピュータに直接接続

- 別の USB ケーブルと、コンピュータの別の USB ポートを使ってみる

- Kobo からログアウトして再度ログイン。こうするとデータベースを再構築して壊れたデータベースのエラーを修正します。

- Kobo Touch のファームウェアを最新にアップグレードしてみてください

- Kobo をリセットしてみる (これにより問題がしばらく解決するものの再び発生することがあります。その場合は、何度も何度もリセットする必要があります)

- Kobo に一度に 1 〜 2 冊の本だけを置き、大量のコレクションを保管しない

e-ink Kindle に送信した本の表紙が一瞬表示されてすぐに一般の表紙に置き換わってしまいます

これは Amazon のバグによって起こります。サーバから本の表紙をダウンロードしようとして失敗したとき、calibre が生成したすでにある表紙を一般的な表紙で置き換えてしまいます。詳しくは フォーラムのこのスレッド [https://www.mobileread.com/forums/showthread.php?t=329945] を参照してください。4.17 からは calibre が回避策を用意しました。表紙が Amazon によって破壊された後に Kindle を calibre に接続したとき、calibre は自動的に表紙を復元します。したがって表紙を Kindle で表示させるには次のようにします:

1. calibre で本を Kindle に送信

2. Kindle を切断し、Amazon が表紙を破壊するのを待つ

3. calibre に Kindle を再接続

ただしこの回避策は 4.17 以降の calibre で本を送信したときにしか動作しません。別の方法としては、読んだ本をいちいち Amazon に知らせたいわけではないでしょうから、Kindle を機内モードにしておきます。Amazon のカスタマサポートに連絡してこのバグについて激しく苦情を言うことをお薦めします。もしかしたら Amazon は耳を貸すかもしれません。

注釈

もし回避策がうまくいかない場合には、Kindle のファームウェアバージョンが 5.12.5 (2020年4月にリリース) 以上であることを確認してください。

MOBI ファイルの表紙が Kindle for PC や Kindle for Android/iPad などで表示されなくなりました

これは Amazon のソフトウェアのバグが原因です。これを回避するには 環境設定 → 変換 → 出力オプション → MOBI 出力 へ行き、Facebook などで書籍内容の共有を許可する オプションを設定してください。一度変換したファイルを再度変換しようとしている場合には、その個別の本の変換ダイアログでもこのオプションを有効にする必要があります。(個別の本の変換設定は保存され、優先して使用されるため)

これを行うことにより、生成された MOBI ファイルは Kindle Fire の本としてではなくパーソナルドキュメントの下に表示されるようになり、Amazon の whispersync は動作しなくなりますが、表紙は機能します。どちらの機能を重要とするかは、皆さんが選択することです。Amazon に連絡してこのバグを修正するよう依頼することをお薦めします。

Amazon のソフトウェアのバグは、Kindle に MOBI ファイルを置いたとき、そのファイルがパーソナルドキュメントとマークされていない限り Amazon はその本を購入した本とみなしてサーバから表紙のサムネイルをダウンロードしようとします。ダウンロードに失敗しても、MOBI ファイルの中に定義されている表紙を使用しようとはしません。おそらくこの動作は Amazon 側の意図的なもので、著作者たちに Amazon 経由で本を販売することを強制する狙いがあるものと見られます。言い換えれば、Kindle はパーソナルドキュメントとマークされた本と Amazon から直接購入した本の表紙しか表示しません。

MOBI ファイルを USB 接続を使って calibre から e-ink Kindle に送信する場合には、calibre はこの不具合を表紙のサムネイル自体をアップロードすることで回避します。ただしこの回避策は、USB 接続を使って calibre から送信する場合に限って有効です。電子メールを使って送信した場合には、Amazon は MOBI ファイルを自動的にパーソナルドキュメントとマークして表紙は機能しません。ただし本はパーソナルドキュメントの中に表示されます。

calibre で Kindle に本を送信したのに表示されません

Kindle に送信された本は Kindle 上で 目録に追加 されてからでないと表示されません。これには時間がかかることがあります。しばらくたっても本が表示されない場合には、Kindle のインデクサがクラッシュしていると考えられます。特定の本によりインデクサがクラッシュすることがあります。残念なことに Amazon はどの本がインデクサをクラッシュさせるのか調べる手段を提供していません。できることと言ったら Kindle をリセットするか、Windows エクスプローラ (またはなにがしかのファイルマネージャ) を使ってメモリからすべてのファイルを削除し、Kindle 上でどの本が問題となっているのか発見するまで一冊ずつ順番に本を再送信するかです。問題の本を見つけたら Kindle から削除してください。そしてその本を calibre で MOBI から MOBI、または MOBI から AZW3 に変換してから送信して戻します。これでおそらく問題が解決するでしょう。

ライブラリ管理

目次

- 本のファイルはどこに保存されていますか？

- calibre は著者名や著者名の読みをどのように管理しますか？

- どうして calibre は本を好きなフォルダに保存してくれないのですか？

- どうして calire には×××の欄がないのですか？

- フォーマットや ISBN を列に表示できますか？

- どうすれば calibre のデータを別のコンピュータに移動できますか？

- calibre の本リストが空っぽです

- ネットワークドライブ/NAS 上の calibre ライブラリでエラーが出ます

本のファイルはどこに保存されていますか？

初めて実行するとき、calibre は本を格納するフォルダを尋ねます。calibre に本を追加すると、そのフォルダの中に本がコピーされます。フォルダ内の本は著者とタイトル別のサブフォルダにきれいに整理されています。このフォルダのコンテンツは calibre が自動的に管理しているので、本やフォルダを手動で追加したりは 決して しないでください。自動で削除される可能性があります。もし特定の本に関連したファイルを追加したい場合には、右上にある 書誌を編集 ダイアログを利用してください。そうすれば calibre は自動的にファイルを正しいフォルダに置き、タイトルや著者名が変更されればそれに合わせて移動します。

本の書誌はライブラリフォルダの一番上にある metadata.db というファイルに格納されます。このファイルは sqlite データベースです。ライブラリのバックアップをとるときには、必ずすべてのサブフォルダを含めフォルダ全体をコピーしてください。

ライブラリフォルダとそのコンテンツが calibre ライブラリと呼ばれるものを構成します。そのようなライブラリを複数持つことができますライブラリを管理するには、ツールバー上の calibre アイコンをクリックします。新規ライブラリを作成したり、削除したり、既存のライブラリの名前を変更したり、ライブラリ間を簡単に行き来したりすることができます。

本を異なるライブラリとの間でコピーしたり移動したりするには (2 つ以上のライブラリを設定したら) 本の上で右クリックして ライブラリにコピー を選択します。

calibre は著者名や著者名の読みをどのように管理しますか？

著者名は、特に文化をまたがると複雑です。どのように複雑かについては こちら [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en] を参照してください。calibre は著者名の管理には非常にフレキシブルな戦略をとっています。最初に理解すべきは、calibre では本と著者は別々のエンティティだということです。ある本には複数の著者がいる可能性がありますし、ある著者には複数の本がある可能性があります。本の著者は "書誌を編集" ダイアログで管理できます。個々の著者を管理するにはメインの calibre ウィンドウの左側にあるタグブラウザの中の著者の上で右クリックして 著者を管理 を選択します。このダイアログを使って、著者の名前と読みを変更できます。これは、その著者の本すべての著者名を自動的に変更します。本に複数の著者がいる場合は、名前を & で区切ります。

では著者名の読みについて説明しましょう:

- 新しい著者が追加されると (新しい著者による本が追加されるたびに追加されます) calibre は自動的に本と著者の読みを計算します。

- タグブラウザの著者名は 著者 の読みの順に並べられます。これは本の著者読みの欄とは違います。

- デフォルトではこの並べ替えアルゴリズムは著者の名前は 名 姓 の形式と仮定し、読みを 姓, 名 として生成します。

- このアルゴリズムは 環境設定 → 高度な設定 → Tweaks へ行き author_sort_copy_method を設定することで変更可能です。

- 著者ごとに著者名の読みを強制的に変更するには、著者名の上で右クリックして 著者の管理 を選択し、著者読みの値を再計算 ボタンをクリックします。この操作は Tweak で author_sort_copy_method tweak にやりたいことを設定した後で行ってください。

- すべての本の著者名の読みを calibre に強制的に再計算させるには書誌を一括編集ダイアログ (すべての本を選択して "書誌を編集" をクリックし、自動的に著者読みを設定 にチェックして OK を押す) を使用します。

- 本の著者名読みを再計算するとき、calibre は著者名読みを個々の著者に使用します。ですから本の著者名読みを再計算する前に、必ず個々の著者名読みには正しい値を設定しておいてください。

- タグブラウザが表示する著者名に名前と読みのどちらを使用するかは 環境設定 → 高度な設定 → Tweaks の categories_use_field_for_author_name で設定できます。

著者の管理 を使えば個々の著者名読みを好きなように変更できます。calibre が正しく解釈できないような名前を扱うときに役に立ちます。たとえば Miguel de Cervantes Saavedra のように複雑に組み合わさった名前や、Sun Tzu のようなアジアの名前を扱う場合に便利です。

こうした柔軟性により、calibre は著者名の読みを好きなように管理することが可能なのです。たとえば、よくある要求のひとつに calibre に著者名を「姓, 名」と表示させたいというものがあります。もし下のメモが当てはまらない場合、これを実現するには次のようにします:

- 上で説明したように author_sort_copy_method tweak を copy に設定します。

- calibre を再起動。残りの手順を完了するまで本の書誌は変更しないでください。

- 著者の管理ダイアログを使ってすべての著者名を「姓, 名」に変更します。

- すべての著者の変更が終わったら、著者名読みの値を再計算 ボタンを押します。

- OK を押します。この時点で calibre はすべての本の著者名を変更します。これにはしばらく時間がかかることがあります。

注釈

「名 姓」を「姓, 名」に変更するにあたって、author_sort の値がすでに「姓, 名」になっていることがよくあります。この場合には、次のようにしてください:

- 上で説明したように author_sort_copy_method tweak を copy に設定します。

- calibre を再起動。残りの手順を完了するまで本の書誌は変更しないでください。

- 著者の管理ダイアログを開き、すべての著者名読みを著者にコピー を押します。

- 著者名が望んだとおりになっているか確認します。この時点ならキャンセルボタンを押せばまだ変更を破棄できます。OK を押してしまうと元に戻すことはできません。

- OK を押します。この時点で calibre はすべての本の著者名を変更します。これにはしばらく時間がかかることがあります。

どうして calibre は本を好きなフォルダに保存してくれないのですか？

calibre のライブラリ管理機能の価値は、インタフェース上で検索と並べ替えを提供している点に尽きます。これにより、フォルダを使ってコレクションを整理するどのような方法よりも 遥かに 効率的に本を探し出すことができます。実際、calibre のインタフェースを使ってコレクションを探したり、整列したり、読んだりすることに慣れれば、もう二度とディスク上の本を探し回る必要はなくなります。本を独自のフォルダ構造 著者 -> タイトル -> 書籍ファイル で管理することにより、calibre は高いレベルの信頼性と標準化を達成できています。検索やタグベースのインタフェースがフォルダベースのもの比べて優れているのは、次の例を考えてみてください。本のコレクションがフォルダ内に次のようにきれいに並べて格納されているとしましょう:

Genre -> Author -> Series -> ReadStatus

こうすると、たとえばアイザック・アシモフによるファウンデーションシリーズの SF の本を探すのは簡単です。しかし、SF の未読の本を探したいとしましょう。このフォルダ方式では簡単に見つけることができません。簡単に見つけたいなら、代わりに次のようなフォルダ方式にしなくてはならなくなります:

ReadStatus -> Genre -> Author -> Series

calibre ではその代わりにジャンルを表すタグと読書状況を示すタグを使い、tag:scifi and not tag:read という簡単な検索式を使うことができます。さらに calibre には立派なグラフィカルインタフェースがあるので、検索言語を学ぶ必要もなく、検索に含める/除外するタグをクリックするだけでよいのです。

ネットワークから本にアクセスできるようファイルシステムにアクセスする必要があると主張する人のために、calibre には優秀なコンテンツサーバがあり、ネットから calibre ライブラリへのアクセスが可能です。

calibre の開発がいつか止まり、本がすべてフォルダ構造に取り残されてしまうことが心配なら、calibre の強力な ディスクに保存 機能を試してみてください。これはすべての本を書誌に基づいて任意のフォルダ構造にエクスポートする機能です。

そしてすべてのタイトルフォルダの末尾に数字がついているのは、堅牢性 のためです。この数字は calibre データベースの本レコードの ID 番号です。この番号があるおかげで、同じ著者の同じタイトルの本を複数持つことができます。そしてまた、たとえデータベースファイルが破損したとしても、これがあるおかげで calibre は全ての書誌を持つデータベースを手品のように再構築することができます。calibre の使命が書誌をファイル名に含めるのをやめ、何かを探すのにファイルシステムを使うのをやめることであると考えると、フォルダ名に余計なものがついたとしても、それで堅牢性が向上がするのであれば十分に価値があります。

それでもまだ納得できないなら、calibre はお薦めできません。どこか他の場所で本をカタログするものを探してください。はっきりしているのは、これが変更されることはありません。変更させようと連絡してくるのは、どうかやめてください。

どうして calire には×××の欄がないのですか？

calibre は最も広くよく使われているフィールドの列を持つよう設計されています。さらに任意の列を追加することも可能です。列は 環境設定 → インタフェース → 表示列の追加 から行えます。列を作成する方法は UI Power tips [https://calibre-ebook.com/ja/demo#tutorials] にあるチュートリアル、または こちらのブログ記事 [https://blog.calibre-ebook.com/calibre-custom-columns/] を参照ししてください

また、別の列の書誌を組み合わせた "仮想列" を作成することも可能です。列の追加ダイアログで クイック作成 のリンクをクリックすれば、本の ISBN や形式を表示する列を簡単に作成できます。calibre の強力なテンプレート言語を使えば、列に関してさらにいろいろなことができます。詳しくは calibre テンプレート言語 を参照してください。

フォーマットや ISBN を列に表示できますか？

はい、できます。上の回答の中にあるカスタム列の追加の手順に従ってください。

どうすれば calibre のデータを別のコンピュータに移動できますか？

すべての calibre データ (本、設定やプラグイン) はエクスポート可能で、そしてまた別のコンピュータ上でインポート可能です。まずデータをエクスポートする方法を見てみましょう:

- メインの calibre ツールバーの calibre アイコンの上で右クリックして すべての calibre データをエクスポート/インポート を選択します。ただしこのメニューは、デバイスを接続中だと表示されません。接続したデバイスがあれば、切断してください。次に calibre データをすべてエクスポート とラベルのついたボタンをクリックします。calibre のライブラリがすべて一覧表示されます。OK をクリックしてコンピュータ上のどこか空のフォルダを選択します。エクスポートされるデータはこのフォルダに保存されます。このフォルダを新しいコンピュータにコピーして、下の指示に従ってデータをインポートしてください。

- 新しいコンピュータに calibre をインストールすると ウェルカムウィザード が実行されますが、ここで設定する内容は何でもかまいません。次の手順で、以前の設定をインポートするからです。これで、ライブラリに Getting Started ガイドがある以外空っぽの calibre が出来上がりました。もう一度 calibre ボタンの上で右クリックして すべての calibre データをエクスポート/インポート を選択します。そして 以前エクスポートしたデータをインポート とラベルの付いたボタンをクリックしてください。コピーしておいたエクスポートデータの入ったフォルダを選択します。インポートできるライブラリの一覧が表示されます。一覧をひとつずつ確認して、ライブラリごとに新しい場所 (コンピュータ上のどこか空のフォルダ) を選択します。OK をクリックしてくだｄさい。インポートが完了すると、以前のライブラリ、設定、そしてプラグインがある状態で calibre は再起動します。

注釈

このインポート/エクスポート機能は calire のバージョン 2.47 以降でのみ利用可能です。それ以前の calibre を利用している場合、またはインポート/エクスポートで問題があった場合には、下に説明するように calibre のライブラリフォルダを手動でコピーすることができます。

calibre ライブラリフォルダを古いコンピュータから新しいコンピュータにただコピーしてください。ライブラリフォルダが何かは、ツールバーの calibre アイコンをクリックすると確認できます。ライブラリの切替え/作成 を選択して、現在の calibre ライブラリへのパスを確認してください。

では新しいコンピュータで、calibre を初めて起動しましょう。calibre は ウェルカムウィザード を実行して calibre ライブラリの場所を尋ねます。先の説明の中でコピーしたフォルダを指定してください。もし転送先のコンピュータにすでに calibre がインストールされている場合には、ウェルカムウィザード は実行されません。その場合、ツールバーにある calibre アイコンの上で右クリックして新しくコピーしたフォルダを指定してやります。これでコンピュータ上に 2 つの calibre ライブラリが置かれ、ツールバー上の calibre アイコンをクリックして切り替えられるようになりました。この方法でライブラリを転送すると、すべての書誌、タグ、カスタム列などが維持されます。

calibre の本リストが空っぽです

どうしてこのようなことが起きるのか理解するために、calibre のライブラリとは何かを理解する必要があります。最も基本的なレベルでは、calibre ライブラリとは単なるフォルダです。本を calibre に追加するたびに、その本のファイルはこのフォルダの中に (著者とタイトルごとのフォルダに配置されて) コピーされます。calibre ライブラリフォルダの中のトップレベルには、metadata.db と呼ばれるファイルがあるのがわかるでしょう。このファイルに calibre ライブラリにある すべての 本のタイトル/著者/評価/タグなどの情報が格納されています。calibre が表示する本の一覧は、この metadata.db ファイルを読んで作成されています。

calibre が空の本一覧を表示する理由は 2 つ考えられます:

- calibre ライブラリフォルダの場所が変更された。ライブラリが外部ディスクに置かれていて、ディスクのドライブレターが変更された場合などに起こります。または間違ってフォルダを移動してしまった場合です。この場合、calibre がライブラリを見つけられず、空のライブラリを表示することになります。これを修正するには、ツールバーの calibre アイコンの上で右クリックして "ライブラリの切替え/作成" を選択します。小さな青いアイコンをクリックして calibre ライブラリ用の新しい場所を選択し、OK をクリックしてください。もし新しい場所がわからなければ、metadata.db という名前のファイルをコンピュータ上で探してください。

- metadata.db ファイルが削除されたか破損した。この場合、calibre にバックアップから metadata.db を再構築するよう依頼できます。calibre ツールバーにある calibre アイコンを右クリックして ライブラリの管理->データベースの復元 を選択してください。calibre は自動的に metadata.db を再構築します。

ネットワークドライブ/NAS 上の calibre ライブラリでエラーが出ます
calibre ライブラリはネットワークドライブに置かないでください。

ファイルシステムは複雑な獣です。ほとんどのネットワークファイルシステムには calibre が利用するさまざまなファイルシステム機能がありません。ハードリンクをサポートしていないものもあれば、ただ単に不具合の多いものもあります。さらに calibre はシングルユーザ用のアプリケーションであり、同じネットワークライブラリに対して誤って 2 つの calibre を実行してしまった場合には、良からぬことが起きるでしょう。そして異なる OS はファイルシステムに異なる制限を課すので、ネットワークドライブを OS にまたがって共有すると、重ねて言いますが 良からぬことが起こります。

calibre のコンテンツサーバを使って別のコンピュータで本を利用できるようにすることを考えてください。calibre をひとつのコンピュータ上で実行し、コンテンツサーバ経由またはリモートデスクトップでアクセスします。

もし実際のライブラリを共有する必要があるなら、ネットワークドライブの代わりに DropBox や rsync などの同期ツールを使ってください。ファイル同期ツールを使う場合には、calibre とファイル同期ツールが同時に calibre ライブラリにアクセスしないようにすることが 必須 です。言い換えると、ファイル同期システムと calibre は 絶対に同時に実行しないでください。

このようなツールを使用してもデータ破損/損失の危険性があるため、そのリスクに耐えられる場合にのみこの方法をとってください。特に Google ドライブ は calibre と互換性がないため、calibre ライブラリを Google ドライブに置くと データが消えます。詳しくは このスレッド [https://www.mobileread.com/forums/showthread.php?t=205581] を参照してください。

その他

目次

- Amazon is stopping email delivery of MOBI files?

- お気に入りのニュースサイトから calibre にニュースをダウンロードしたい

- どうして calibre という名前なんですか？

- calibre はどうして macOS ではフォントの一部しか表示しないのですか？

- calibre が Windows 上で起動しません

- calibre がときどきフリーズ/クラッシュします

- calibre の電子書籍ビューアと本の編集ツールが Windows 上で動きません

- ビューアを使用したり変換を行ったりすると Windows でアクセスが拒否されたとエラーが出ます

- macOS 上で calibre が開始しない、またはクラッシュします

- I get only a black or white screen when running the calibre E-book viewer?

- インストーラをダウンロードしましたが、動きません

- ウィルス対策プログラムが calibre がウィルス/トロイの木馬だと主張しています

- calibre のバックアップをとるにはどうすればいいですか？

- どうすれば calibre を使って EPUB 書籍を購入できますか？ (あるいは、.acsm ファイルで何をすればいいですか？)

- 「許可がありません」というエラーが出ます

- リーダ上にコメント書誌を表示させることはできますか？

- どうすれば calibre に HTTPプロキシを使わせることができますか？

- calibre に機能追加したいのですが、どうすればよいですか？

- どうして calibre には自動更新機能がないのですか？

- calibre のライセンスはどうなっていますか？

- どうすれば calibre を USB メモリから実行できますか？

- どうすれば自前の Linux サーバ上でニュースダウンロードのような calibre のパーツとコンテンツサーバを実行できますか？

Amazon is stopping email delivery of MOBI files?

Amazon have announced [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/] that they will stop accepting MOBI files emailed to @kindle.com email addresses. You can instruct calibre to send EPUB instead of MOBI by going to Preferences → Sharing books by email and then removing MOBI from the list of formats to send to your @kindle.com email address and adding EPUB instead.

Note however, that Amazon's EPUB intake is very flawed, they will reject a number of EPUB files that work everywhere else. In such cases you can try the following trick:

1. Convert the EPUB file to MOBI

2. Then convert the MOBI file back to EPUB and send the resulting EPUB file

This will remove all advanced formatting, embedded fonts, etc., but greatly increase the chances of Amazon accepting the EPUB.

注釈

If you were previously using email delivery of periodicals downloaded by calibre, you will be better off sending those by USB cable or downloading them from the calibre Content server via the Kindle's built-in browser.
However, if you want to continue using email delivery you can try changing the output format in Preferences->Behavior to EPUB, then calibre will download the news in EPUB format. Whether Amazon will accept the EPUB or not is a whole other question.

お気に入りのニュースサイトから calibre にニュースをダウンロードしたい

あなたがコンピュータに精通しているなら、お好きな Web サイトからニュースをダウンロードするよう calibre に指示することができます。やり方については お気に入りのニュースサイトを追加 を参照してください。

それ以外の場合、 calibre レシピフォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=228] に投稿して特定のニュースサイトをリクエストできます。

どうして calibre という名前なんですか？

お好きなものをどうぞ:

- Converter And LIBRary for E-books (電子書籍用のコンバータおよびライブラリ)

- calibre (水準) が高い製品

- 最初の e-ink ベースの電子書籍リーダーであった SONY 製 Librie のオマージュ

- うちの奥さんが選びました ;-)

calibre の発音はCa-li-bre (キャリブレ) ではなく Cal-i-ber (キャリバー) です。不思議に思うかもしれませんが、calibre は英国式のつづりです。作者はインド人なので、このつづりが自然なのです。

calibre はどうして macOS ではフォントの一部しか表示しないのですか？

calibre は作成する電子書籍の中にフォントを埋め込みます。電子書籍では TrueType と OpenType (.ttf および .otf) のフォントのみ埋め込みをサポートしています。macOS 上のほとんどのフォントは .dfont 形式のため、埋め込むことができないのです。calibre はシステム上に見つかった TrueType フォントと OpenType フォントのみを表示します。そのようなフォントは Web 上でたくさん入手できます。.ttf/.otf ファイルをダウンロードしてホームフォルダの下の Library/Fonts フォルダに追加すればいいだけです。

calibre が Windows 上で起動しません

これにはいくつかの原因が考えられます:

- If you get no errors but the calibre window does not appear, it has probably just appeared off screen. You can gather all windows onto the current screen using one of the techniques described here [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 and C:\Program Files\Calibre2 を (もし存在していれば) 削除します。そして再インストールすれば、解決するはずです。

- calibre の最初の実行時に ウェルカムウィザード でエラーが出る場合、calibre のライブラリに C:\library のようなフォルダを選択してください。(パスに英文字以外が含まれていたり数字のみだったりしたときに calibre に問題が発生する場合があります)

- 管理者として実行してみてください (アイコンを右クリックして 管理者として実行 を選択)

もしそれでも開始できないなら、コマンドプロンプトを開いてください (Windows キーを押しながら R を押し、現れた実行ダイアログに cmd.exe と入力)。コマンドプロンプトに次のコマンドを入力して Enter を押します:

calibre-debug -g

ヘルプメッセージに表示される出力を フォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=166] に投稿してください。

calibre がときどきフリーズ/クラッシュします

私の知る限り、これを引き起こし得る原因はいくつかあります:

- 最近、外部モニターまたは TV をコンピュータに接続した。この場合、書誌の編集ウィンドウや変換ダイアログのような新しいウィンドウを calibre が開くたびに 2 番目のモニターに表示され、それに気づかず calibre がフリーズしたと考えてしまいます。2 番目のモニタを切断し、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-bit calibre をきちんと動かなくしてしまうセキュリティソフトウェア/設定があります。64-bit 版の calibre を使用している場合は、32-bit 版に切り替えてみてください。

- テキストを calibre 電子書籍ビューアからコピーしようとしているときにクラッシュが起こる場合、実行しているクリップボード監視/管理アプリケーションが原因とほぼ考えられます。無効にすれば解決するでしょう。

- ファイルダイアログを使用しているときに、たとえば 本の追加 ボタンや ディスクに保存 ボタンをクリックするような場合に特にクラッシュが発生しやすい場合、壊れたシェル拡張機能をコンピュータにインストールしたソフトウェアが存在しています。既知の原因としては、SpiderOak, odrive sync, Dell Backup and Recovery, NetDrive などがあります。この中にインストールしているものがあるなら、アンインストールすれば解決します。プログラム全体をアンインストールしたくない場合には、 NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html] を使ってシステム上にインストールされているシェル拡張機能を確認し、個別に無効にすることができます。シェル拡張機能を無効にした後は、必ず "Restart Explorer" を使うかコンピュータを再起動してください。

上記のいずれにも当てはまらない場合、コンピュータ上に calibre に干渉している何か別のプログラムが存在しています。まず実行するプログラムを可能な限り減らすためにセーフモードでコンピュータを再起動し、それでもまだクラッシュが発生するかを確認します。もし発生しなくなれば、問題を引き起こしているのが何らかのプログラムであることがわかります。そうした原因となるものとしてありがちなのが他のプログラムの「動作」を変更するプログラム、すなわちウィルス対策、デバイスドライバ、RoboForm のような何か (自動でフォームに入力するアプリ) や Voice Control や Screen Reader のような支援技術です。

原因を特定する唯一の方法は、プログラムをひとつずつ削除し、どのプログラムが問題の原因となっているのか確認することです。基本的には、プログラムを停止、calibre を実行、クラッシュの確認を行います。それでもまだ発生するなら、別のプログラムを停止して繰り返します。

calibre の電子書籍ビューアと本の編集ツールが Windows 上で動きません

These two programs use hardware acceleration as they embed a version of the
Chrome browser to render HTML. If they do not work it will be because of incompatibility with your system's GPU (graphics) drivers. Try updating these first, and reboot. If that does not fix it, you can set the QTWEBENGINE_CHROMIUM_FLAGS environment variable to the value --disable-gpu to turn off hardware acceleration. See this page [https://doc.qt.io/qt-6/qtwebengine-debugging.html] for details.

ビューアを使用したり変換を行ったりすると Windows でアクセスが拒否されたとエラーが出ます

コンピュータ上の何かが calibre が一時ファイルにアクセスするのを妨げています。たいていの場合は、Temp フォルダのアクセス権が間違って設定されています。Windows Explorer で file:C:\Users\USERNAME\AppData\Local フォルダへ行き、file:Temp フォルダを右クリックして プロパティ を選択し、セキュリティ タブへ行きます。このフォルダのフルコントロールをユーザアカウントに与えてください。

一部のユーザは管理者コマンドプロンプトで次のコマンドを実行すると権限を修正できると報告しています。管理者コマンドプロンプトを開くにはスタートメニューを cmd.exe で検索し、コマンドプロンプトのエントリ上で右クリックして 管理者として実行 をクリックします。コマンドプロンプトで次のコマンドを入力して Enter を押してください:

icacls "%appdata%\..\Local\Temp" /reset /T

または calibre を管理者として実行することもできますが、そうすると一部の機能、たとえばドラッグ&ドロップなどが機能しなくなります。

最後に、一部のユーザは UAC を無効にすると問題が解決すると報告しています。

macOS 上で calibre が開始しない、またはクラッシュします
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 上で確認する簡単な方法は、"Font Book" を開いてすべてのフォントを選択し、ファイルメニューから "フォントを検証" を選択することです。

I get only a black or white screen when running the calibre E-book viewer?

This will be because of an incompatibility between Qt WebEngine, which the viewer uses to render and the GPU drivers on your system. First try upgrading the GPU drivers. If that does not help, you can try turning off hardware acceleration in Qt WebEngine by setting the environment variable QTWEBENGINE_CHROMIUM_FLAGS to the value --disable-gpu.
See 環境変数 for how to change environment variables.

インストーラをダウンロードしましたが、動きません

インターネットからのダウンロードは、ファイルが破損している場合があります。ダウンロードした calibre のインストーラが開かない場合には、もう一度ダウンロードしてみてください。ダウンロードし直しても動かない場合には、 代替の場所 [https://github.com/kovidgoyal/calibre/releases/latest] からダウンロードしてみてください。それでもまだインストーラが動かない場合、コンピュータ上の何かが実行を妨げています。

- 一時的にウィルス対策プログラム (Microsoft Security Essentials, Kaspersky, Norton, McAfee など) を無効にしてみてください。アップグレードプロセスが途中でハングする場合、たいていはこれが原因です。

- 同様に、もしインストーラが失敗またはロールバックしていて Microsoft PowerToys が実行中の場合は、Microsoft PowerToys を終了してください。

- コンピュータを再起動して、 Wise registry cleaner [https://www.wisecleaner.com] のようなレジストリクリーナを実行してみてください。

- クリーンインストールしてみてください。つまり calibre をアンインストールし、C:\Program Files\Calibre2 (または calibre のインストール先として選択した場所) を削除します。そして calibre を再インストールしてください。アンインストールしても本や設定には影響がありません。

- 別のブラウザからインストーラをダウンロードしてみてください。たとえば、もし Microsoft Edge を使っているなら Firefox や Chrome を代わりに使ってみてください。

- もし Windows 上で DLL が見つからないというエラーが出るなら、おそらく一時ファイルの権限が間違っています。Windows Explorer で C:\Users\USERNAME\AppData\Local へ行き、Temp の上で右クリックして プロパティ を選択し、セキュリティ タブへ行きます。ユーザアカウントにこのフォルダのフルコントロールを与えてください。

それでもまだインストーラが動かず、Windows を使用している場合には、:website:`calibre portable install <download_portable>`が利用できます。これならインストーラが不要です。(単なる ZIP ファイルです)

ウィルス対策プログラムが calibre がウィルス/トロイの木馬だと主張しています

まず最初に、calibre を 公式サイト [https://calibre-ebook.com/ja/download] からダウンロードしていることを確認してください。右側の広告ではなく、必ず左側のダウンロードリンクをクリックしてください。calibre は大変人気のあるプログラムのため、悪意のある人々は不注意な人を騙すためにダウンロード用の Web サイトを用意したりします。

公式サイトからダウンロードしたにもかかわらずウィルス対策が calibre がウィルスだと主張するなら、ウィルス対策プログラムが間違っています。ウィルス対策プログラムはウィルスを検出するのにヒューリスティック、つまり "怪しく見える" コードのパターンを使用しています。言ってみれば人種的プロファイリングのようなものです。calibre は完全にオープンソースな製品です。ソースコードを実際に自分の目で見て (または人を雇ってやってもらって) ウィルスではないと確認することができます。アンティウィルスソフトウェアを購入した会社に誤検出を報告してください。ウィルス対策プログラムが calibre のダウンロード/インストールを妨げているのであれば、一時的に無効にして、calibre をインストールしてから再度有効にしてください。

calibre のバックアップをとるにはどうすればいいですか？

最もバックアップが必要な重要なものは calibre ライブラリフォルダで、ここにはすべての本と書誌が含まれています。これは、初めて calibre を実行したときに calibre ライブラリとして選択したフォルダです。ライブラリフォルダへのパスを調べるには、メインツールバーにある calibre アイコンをクリックします。このフォルダの中のすべてのファイルとサブフォルダをバックアップする必要があります。

ツールバー上の calibre アイコンをクリックしてバックアップライブラリフォルダを選択するだけで、簡単に calibre がバックアップフォルダを使うよう切り替えることができます。バックアップしたライブラリフォルダには、カスタム列や保存済み検索だけでなくすべての本と書誌がバックアップされています。

calibre の設定やプラグインをバックアップしたい場合は、設定フォルダをバックアップする必要があります。設定フォルダはは 環境設定 → その他 で確認できます。ただし設定フォルダの復元は公式にはサポートされていません。が、たいていはうまくいきます。バックアップフォルダの中身を現在の設定フォルダにコピーして復元してください。

どうすれば calibre を使って EPUB 書籍を購入できますか？ (あるいは、.acsm ファイルで何をすればいいですか？)

ほとんどの購入した EPUB 書籍には DRM がかけられています。これのせいで、calibre からは本を開くことができません。それでも本を格納したり電子書籍リーダに転送するのに calibre を使うことは可能です。まず Adobe Digital Editions のあるWindows マシン上でリーダを承認します。これが終われば、calibre で転送した本はリーダ上できちんと動作するでしょう。EPUB 書籍を Web サイトから購入した場合には、".acsm" ファイルが手に入ります。このファイルは Adobe Digital Editions で開く必要があり、そうすると実際の ".epub" 電子書籍がダウンロードされます。電子書籍は "My Digital Editions" フォルダに格納されるので、そこから calibre に追加することができます。

「許可がありません」というエラーが出ます

アクセス拒否のエラーが起きる原因はいろいろなものが考えられますが、いずれも calibre とは関係がありません。

- 書き込み保護が有効になっている SD カードを使っていると、アクセス拒否のエラーが出ることがあります。

- macOS でデバイスを calibre に接続するときにアクセス拒否エラーがでる場合、システム環境設定 > セキュリティとプライバシー > プライバシー > ファイルとフォルダ を調べて修正することができます。

- 自分で、もしくは使用した何らかのプログラムが問題のファイルのアクセス権を読取り専用に変更した場合。

- ファイルシステムにエラーがあるため OS がそのファイルシステムを読取り専用でマウントしているか、特定のファイルを復旧待ちの保留用に読取り専用にしている場合。

- ファイルの所有者が別のユーザになっている場合。

- そのファイルを別のプログラムが開いている場合。

- ファイルがデバイス上に存在している場合、デバイスのルート上でファイル数が最大 256 の制限に達している可能性があります。この場合、エラーメッセージの示すデバイスまたは SD カードを FAT32 で再フォーマットするか、SD カードまたはデバイスからファイルをいくつか削除する必要があります。

calibre の利用を再開する前に、根底にあるアクセス権のエラーを修正する必要があります。エラーメッセージを注意深く読み、どのファイルを指しているのか確認し、そのファイルまたはそのファイルの置かれているフォルダのアクセス権を修正してください。

リーダ上にコメント書誌を表示させることはできますか？

ほとんどのリーダでサポートされていません。メーカーに苦情を言いましょう。苦情が多ければ、事態は変化するはずです。当面は、コメントを始めとする書誌を、変換時に "本の最初のページに書誌を挿入" オプションを使って電子書籍の先頭にある "表紙ページ" に挿入してください。このオプションは変換設定の 構造検出 セクションにあります。ただしこれを有効にするには、本の 変換 が必須です。もし本がすでに変換の必要のない形式のものなら、その形式から同じ形式に変換してください。

別の方法は、calibre ライブラリ内のすべての本の一覧を書誌とともにカタログしたものを電子書籍の形で作成することです。変換 ボタンを右クリックするとカタログ作成ツールにアクセスできます。聞かれる前にお答えしておきますが、カタログにリーダ上の本へ "直接リンク" させることはできません。

どうすれば calibre に HTTPプロキシを使わせることができますか？

デフォルトでは、OS に設定されているプロキシサーバを使用します。しかしそれが間違っていることもあります。たとえば Windows で Microsoft Edge を使っていない場合には、プロキシ設定はきちんと最新情報に更新されていない可能性があります。http_proxy` および https_proxy` 環境変数を設定すれば、calibre に特定のプロキシサーバを使用させることができます。この変数は http://ユーザ名:パスワード@サーバ名 の形式で指定します。それぞれに指定する正しい値は、ネットワーク管理者に尋ねてください。ただし calibre がサポートしているのは HTTP プロキシだけで、SOCKS プロキシはサポートしていません。現在 calibre が使用しているプロキシは 環境設定->その他 で確認できます。

calibre に機能追加したいのですが、どうすればよいですか？

2 つの方法があります:

1. calibre をハックしてパッチを作成し、レビューして含めることができりうよう私に送ってください。開発 [https://calibre-ebook.com/ja/get-involved] を参照してください。

2. 機能追加を要望する バグ [https://calibre-ebook.com/ja/bugs] を開きます。ただし自分では要求機能がこの上なく重要/不可欠に思えたとしても、calibre 開発者の同意が得られないこともあります。幸いにして calibre はオープンソースですから、その機能を自分で実装したり誰か雇った人にやってもらうことができます。さらに、calibre には包括的なプラグインアーキテクチャを持つため、欲しい機能をプラグインとして開発することも可能です。プラグインを書いて calibre を機能拡張 を参照してください。

どうして calibre には自動更新機能がないのですか？

理由はたくさんあります:

- 毎週更新する必要はありません 。calibre の機能に満足していれば更新通知を無効にして、そのまま楽しんでください。1 年に 1 度くらい更新したい内容があるか確認すれば十分でしょう。更新通知を無効にするチェックボックスは、更新通知の中にあります。

- calibre のダウンロードには現在 :website_base:`1 か月あたり 150 TB の帯域 <dynamic/downloads>`が使用されています。自動更新を実装すると使用帯域が大幅に増加し、誰かが支払わなくてはならない費用が毎月何千ドルもかかることになるでしょう。

- 現在のように Web サイトにアクセスしてダウンロードする代わりに、アップデートをダウンロードして起動するダイアログを実装したとしても、最も熱心に更新する calibre の利用者でも 1 週間あたり 5 クリック の節約にしかなりません。calibre の開発にはそれよりもずっと優先順位の高いものがあるのです。

- もし本当に心の底から calibre を毎週ダウンロードするのがいやだけれども常に最新の状態で使いたい、という場合にはソースから起動することをお薦めします。それなら更新が簡単です。手順は こちら を参照してください。

- calibre ユーザによって作られたサードパーティ製の自動更新プログラムは calibre フォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=238] にあります。

calibre のライセンスはどうなっていますか？

calibre は GNU General Public License v3 (オープンソースライセンス) のもとでライセンスされています。これはソースコードを公開している限り自由に calibre を再配布できることを意味します。したがってもし calibre を製品とともに CD に入れたい場合、calibre のソースコードも CD に入れる必要があります。ソースコードは こちらからダウンロード可能 [https://download.calibre-ebook.com] です。calibre の変換したものは好きなように利用できます。 GNU GPL v3 [https://www.gnu.org/licenses/gpl.html] を参照してください。

どうすれば calibre を USB メモリから実行できますか？

calibre のポータブル版は こちら [https://calibre-ebook.com/ja/download_portable] から入手可能です。

どうすれば自前の Linux サーバ上でニュースダウンロードのような calibre のパーツとコンテンツサーバを実行できますか？

まず calibre を Linux サーバ上にインストールする必要があります。Linux の最新のディストリビューションを使用しているなら、calibre を問題なくインストールできるでしょう。

注釈
calibre needs GLIBC >= 2.31 and libstdc++ >= 6.0.28. If you have an older server, you will either need to compile these from source, or use calibre
3.48 which requires GLIBC >= 2.17 or 2.85.1 which requires GLIBC >= 2.13 or calibre 1.48 which requires only GLIBC >= 2.10. In addition, although the calibre command line utilities do not need a running X server, some of them do require the X server libraries to be installed on your system. This is because of Qt, which is used for various image processing tasks, and links against these libraries. If you get an ImportError about some Qt modules, you are likely missing some X libraries. Typical candidates are: 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

If you want to generate MOBI, use outputfile.mobi instead and use --output-profile kindle.

ダウンロードしたニュースは次のコマンドでメール送信できます:

/opt/calibre/calibre-smtp

実際のコマンドラインの解説は、読者の練習問題として残しておきます。

最後に、ダウンロードしたニュースを calibre ライブラリに次のようにして追加できます:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

これらのコマンドやその他のコマンドについて詳しくは、calibre ユーザマニュアルの コマンドライン・インタフェース をお読みください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル

チュートリアル

ここには、XPath やテンプレートなどのより高度な機能を使用するためのチュートリアルがあります。

- お気に入りのニュースサイトを追加

- 本のサブグループの管理、たとえば「ジャンル」

- XPath のチュートリアル

- calibre テンプレート言語

- calibre で正規表現を利用するにあたって重要なこと

- プラグインを書いて calibre を機能拡張
- 電子書籍での数式の組版
- AZW3 • EPUB • MOBI のカタログを作成
- 仮想ライブラリ

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »

- 本のサブグループの管理、たとえば「ジャンル」

本のサブグループの管理、たとえば「ジャンル」

一部のユーザは、ライブラリの本をサブフォルダに似ているサブグループに整理したいと考えています。一番よくある理由はジャンルに階層を設けたいというものですが、他にもいろいろあります。あるユーザは教科書を科目とコース番号で整理する方法を求めました。別のユーザはテーマと受取人で贈り物を追跡したいと考えました。このチュートリアルではジャンルを例として説明します。

先に進む前に、これはハードディスク上のフォルダについての話ではないことに注意してください。サブグループはファイルのフォルダではありません。本はどこにもコピーされることはありません。calibre ライブラリのファイル構成には影響がありません。代わりに、calibre ライブラリの中で本のサブグループを整理して表示する方法を紹介します。

- セットアップ

- 検索

- 制限

- 有用なテンプレート関数

ジャンルのようなサブグループに対してよくある要求事項には、次のものがあります:

- サブグループ (ここでは例としてジャンル) にはカテゴリではなく本を含める必要があります。これがサブグループがカテゴリと異なる点です。

- 本は複数のサブグループ (ジャンル) に分類できます。これがサブグループが物理的なファイルフォルダと異なる点です。

- サブグループ (ジャンル) は階層を形成しなくてはなりません。サブグループにはサブグループを含めることができます。

タグを使えば最初の二つはかなえられます。ジャンルごとにタグをつければタグブラウザ (または検索) でそのジャンルの本を見つけられます。これが 1 点目。同じタグを何冊でも本につけることは可能です。これが 2 点目。問題は、タグでは 3 番目の要求事項を満たせないということです。階層を提供していないのです。

calibre の階層機能が 3 番目を提供します。ジャンルをツリー構造で見ることができて、そのジャンルまたはサブジャンルにある本を簡単に検索できます。たとえばジャンルの構造が次のようになっているとしましょう:

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

階層機能を使えば、これらのジャンルを画像が示すようにタグブラウザでツリー状に表示できます。この例では一番外側の階層 (ジャンル) は、ジャンルを含むカスタム列です。サブジャンルを含むジャンルは、隣に小さな三角が表示されます。三角をクリックすると項目が開かれてサブジャンルを表示し、ご覧のように "History" と "Science Fiction" が表示されます。

ジャンル名の上でクリックするとそのジャンルおよびその子ジャンルに含まれる本をすべて検索します。たとえば Sicence Fiction の上でクリックすると、その 3 つの子ジャンルである Alternate History, Military, および Space Opera が表示されます。Alternate History の上でクリックすると、そのジャンルに含まれる本だけを検索し、Military と Space Opera は無視します。もちろん本には複数のジャンルを割り当てることが可能です。もしもある本が Space Opera かつ Military genres であるなら、どちらのジャンルをクリックしてもその本は表示されます。検索については下でさらに詳しく説明します。

この画像からわかるもうひとつのことは、Military ジャンルは 2 つあるということです。ひとつは History の下に、もうひとつは Science Fiction にあります。ジャンルは階層構造になっていますから、これらは 2 つの別々のジャンルです。ある本が片方の中にあったり、もう一方の中にあったり、両方にあったり (と言ってもこの場合では、ありえそうもありませんが) します。たとえば Winston Churchill の "The Second World War" は "History.Military" に分類されるでしょう。David Weber の Honor Harrington シリーズは "Science Fiction.Military" に分類されるでしょうし、さらに言えば "Science Fiction.Space Opera" にも分類されるでしょう。

ジャンルが存在する場合、つまり少なくとも 1 冊の本がそのジャンルに含まれている場合には、ライブラリビューから本をドラッグ&ドロップすることで簡単に他の本にそのジャンルを適用できます。詳しくは下に説明します。

セットアップ

ここまでくれば、「設定はどうすればいいの？」と疑問に思うことでしょう。それには 3 つのステップがあります: 1) カスタム列を作成、2) calibre にその新しい列を階層として扱うよう指示、そして 3) ジャンルを追加。

設定 -> 表示列の追加 を使用し、通常の方法でカスタム列を作成します。たとえば "#genre" を検索名、"Genre" を列見出しとします。列タイプは "タグのようにカンマ区切りでタグブラウザに表示されるテキスト" です。

次に、calibre を再起動した後、calibre にその列を階層として扱うよう指示します。環境設定  →  外観  →  タグブラウザ へ行き、検索名 "#genre" を "階層化するカテゴリ" に入力します。適用 を押せば設定完了です。

この時点で列にジャンルはありません。まだ最後のステップが残っています。本にジャンルを適用する方法です。ジャンルは、そこに最低 1 冊の本がないと calibre 上には存在しません。最初にジャンルを適用する方法を学ぶために、本の書誌の中でジャンルがどのように見えるかといった詳細について説明する必要があります。

「モノ」の階層は、ピリオドで区切られたフレーズで構成された項目を作成することで構築されます。ジャンルの例で続けると、このような項目は "History.Military", "Mysteries.Vampire", "Science Fiction.Space Opera" などになります。したがって新しいジャンルを作成するには、そのジャンルに入れたい本を選び、書誌を編集し、作成した列に新しいジャンルを入力します。この例で言えば、新しいジャンル "Comics" とその下にサブジャンル "Superheroes" を本に割り当てたいとき、その (コミック) 本の "書誌を編集" し、カスタム書誌タブを選択して、以下に示されるように (その他のカスタム列は無視してください) "Comics.Superheroes" と入力します。

上記の操作を行ったあと、タグブラウザにはこのように表示されます:

これ以降、この新しいジャンルを本 (おそらくコミック本) に適用するにはその本をジャンルにドラッグ&ドロップするか、"書誌を編集" を使って上記とまったく同じ方法で本に追加することができます。

注釈

階層表示はタグブラウザが項目を名前順に表示するよう設定されている場合にのみ機能します。これがデフォルトで、タグブラウザ下部にある 設定 ボタンで確認できます。

検索

ジャンルを検索する最も簡単な方法はタグブラウザを使用して、見たいジャンルの上でクリックすることです。子を持つジャンルの上でクリックすると、そのジャンルおよびすべての子ジャンルの本を表示します。しかし、ここで疑問に思うかもしれません。ジャンルに子があるからといって、それ自体がジャンルではないという意味ではありません。たとえばある本のジャンルが "History" であるが "History.Military" といった場合です。どうすれば "History" の本だけを検索できるのでしょうか？

タグブラウザの検索メカニズムは、あるアイテムに子があるかどうかを認識しています。子がある場合、アイテム上でクリックすると通常は 3 つのサイクルが 5 つのサイクルで変わります。1 番目は普通の緑のプラスで、これはその本のジャンルのみ (つまり History) の本を表示します。2 番目は 2 個のプラス (上に表示) で、そのジャンルとすべてのサブジャンル (つまり History および History.Military) の本を表示します。3 番目は普通の赤のマイナスで、そのジャンルのみを除外した本を表示します。4 番目は 2 個のマイナスで、そのジャンルとサブジャンルを除外した本を表示します。5 番目で最初に戻り、マークが消え、検索なしを意味します。

制限

あるジャンルを検索してそれから保存済み検索を作成する場合、そのジャンルの本の仮想ライブラリを作成するのに "制限" ボックスを利用できます。これはそのジャンル内で他の検索を行い場合や、そのジャンルの本の書誌を管理/更新したい場合に役立ちます。この例で言うと、'History.Japanese' と名前をつけた保存済み検索を作成するにはタグブラウザで Japanese ジャンルの上でクリックして検索を検索フィールドに表示させ、保存済み検索のフィールドに 'History.Japanese' と入力して、"検索を保存" ボタン (右側にある、白いプラスのついた緑色のボックス) を押します。

保存済み検索を作成したあと、それを制限として利用できます。

有用なテンプレート関数

ジャンルの情報を、ディスクに保存したり、デバイスに送信したりといったテンプレートで使用したいこともあるかもしれません。ここで「どうすれば一番外側のジャンル名を取得できる？」と疑問に思うことでしょう。これを簡単にするために、calibre のテンプレート関数 subitems が提供されています。

たとえば、"History/The Gathering Storm - Churchill, Winston" のように一番外側のジャンルの階層をジャンルフォルダを作成するのために save-to-disk テンプレートに追加したいとします。これを行うには、階層の最初のレベルを抽出し、フォルダを作成する必要があることを示すためにスラッシュをつけて先頭に追加します。下のテンプレートがこれを行います:

{#genre:subitems(0,1)||/}{title} - {authors}

テンプレートと subitems() 関数について詳しくは テンプレート言語 を参照してください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- XPath のチュートリアル

XPath のチュートリアル

このチュートリアルでは、 calibre の HTML [https://en.wikipedia.org/wiki/HTML] ドキュメントの中から任意の部分を選択するために利用できる `XPath <https://en.wikipedia.org/wiki/XPath>`_というクエリ言語に関して軽く紹介します。XPath は広く使用されているる標準で、Google などで検索すると山ほどの情報が出てきます。しかしこのチュートリアルでは、構造化されていない HTML ドキュメントの中から章見出しを探す方法のような、電子書籍に関した特化した XPath の利用法に焦点を当てています。

目次

- タグ名で選択

- 属性で選択

- タグの内容で選択

- サンプル電子書籍

- 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>XPath の使用を実演してみせるための非常に短い電子書籍です。</p> にのみマッチして、他の <p> タグ内のものにはマッチしません。上記の例では XHTML タグにマッチさせるために h: プリフィックスが必要です。なぜなら calibre は内部的にはすべてのコンテンツを XHTML で表現しているためです。XHTML においてタグには 名前空間が 存在し、h: が HTML タグの名前空間プレフィックスです。

では <h1> タグと <h2> タグの両方を選択したいとしましょう。これをするには 述語 と呼ばれる XPath の構造体が必要になります。述語 とはタグの選択に使用される単なるテストです。テストは自ずと強力であり、チュートリアルを進めるにつれて、より強力な使用例を目にするでしょう。述語はテストの式を角カッコで囲むことで作成します:

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

この XPath 式にはいくつかの新しい機能があります。ひとつはワイルドカード * の使用です。これは すべてのタグにマッチ することを意味します。では、テスト式 name()='h1' or name()='h2' を見てみましょう。name() は ビルトイン関数 の例です。これは単にタグの名前を評価します。したがってこれを使用することで、名前が h1 または h2 であるタグを選択できます。name() は名前空間を無視するため、h: プリフィックスは必要ありません。XPath にはいくつもの便利なビルトイン関数があります。このチュートリアルの中で、さらにいくつか紹介します。

属性で選択

属性に基づいてタグを選択するには、述語を使用する必要があります:

//*[@style] (Select all tags that have a style attribute)
//*[@class="chapter"] (Select all tags that have class="chapter")
//h:h1[@class="bookTitle"] (Select all h1 tags that have class="bookTitle")

ここでは @` 演算子がタグの属性を参照します。`XPath ビルトイン関数`_を使用すれば、属性値に対してさらに高度な比較を行うことができます。

タグの内容で選択

XPath を使えば、タグに含まれているテキストに基づいてタグを選択することもできます。これを行うのに一番よいのは、ビルトイン関数 re:test() を通じて正規表現の力を利用することです:

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

ここでは @ 演算子が属性を参照するのと同様に、. 演算子がタグの内容を参照します。

サンプル電子書籍

<html>
<head>
<title>A very short e-book</title>
<meta name="charset" value="utf-8" />
</head>
<body>
<h1 class="bookTitle">A very short e-book</h1>
<p style="text-align:right">Written by Kovid Goyal</p>
<div class="introduction">
<p>A very short e-book to demonstrate the use of XPath.</p>
</div>

<h2 class="chapter">Chapter One</h2>
<p>This is a truly fascinating chapter.</p>

<h2 class="chapter">Chapter Two</h2>
<p>A worthy continuation of a fine tradition.</p>
</body>
</html>

XPath ビルトイン関数

name()

現在のタグの名前

contains()

contains(s1, s2) は s1 に s2 が含まれているときに true を返します。

re:test()

re:test(src, pattern, flags) は 文字列 src が正規表現 pattern にマッチしたとき true を返します。特に便利はフラグは i で、大文字と小文字を区別せずに比較するようになります。正規表現の構文に関しては、 regexp syntax [https://docs.python.org/library/re.html] がよい入門書となります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- calibre テンプレート言語

calibre テンプレート言語
calibre テンプレート言語は、ファイルパスの指定、値の整形、ユーザ指定の列の値の計算などのタスクのために calibre 全体で使用される calibre 固有の言語です。例を挙げると:

- calibre ライブラリからディスクまたは電子書籍リーダにファイルを保存するときに、フォルダ構造とファイル名を指定します。

- calibre の本のリストにアイコンと色を追加するためのルールを定義します。

- 他の列からのデータを含む 仮想列 を定義します。

- 高度なライブラリ検索。

- 高度なメタデータの検索と置換。

この言語は、使用する本の書誌、その書誌の計算、およびそのフォーマット方法を指定する テンプレート の概念に基づいて構築されています。

基本テンプレート

基本テンプレートは、ひとつ以上の テンプレート表現 で構成されます。テンプレート表現 は、中括弧 ({}) 内のテキストと名前で構成され、処理中の本の対応する書誌に置き換えられます。たとえば、本をデバイスに保存するために使用される calibre のデフォルトのテンプレートには、次の 4 つのテンプレート式があります。

{author_sort}/{title}/{title} - {authors}

"Isaac Asimov" の本 "The Foundation" の場合、次のようになります:

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

特定の本のフィールドの書誌が定義されていない場合、テンプレートのフィールドは空の文字列 ('') に置き換えられます。たとえば、次のテンプレートについて考えてみましょう:

{author_sort}/{series}/{title} {series_index}

Asimov の本 "Second Foundation" がシリーズ "Foundation" に含まれている場合、テンプレートは次のように生成します:

Asimov, Isaac/Foundation/Second Foundation 3

本のシリーズが入力されていない場合、テンプレートは次のように生成します:

Asimov, Isaac/Second Foundation

テンプレートプロセッサは、複数のスラッシュと先頭または末尾のスペースを自動的に削除します。

高度な整形

書誌の置換に加えて、テンプレートには条件付きで追加のテキストを含め、置換されたデータの整形方法を制御できます。

条件つきでテキストを含める

フィールドが空でない場合にのみ、出力にテキストを表示したいというような場合があります。よくあるケースは series と series_index` で、何も必要ないか、ハイフンで区切られた 2 つの値が必要です。 calibre は、特殊な ``テンプレート表現 の構文を使用してこのケースを処理します。

たとえば、上記の Foundation の例を使用して、テンプレートで Foundation - 3 - Second Foundation を生成するとします。このテンプレートはその出力を生成します:

{series} - {series_index} - {title}

ただし本にシリーズがない場合、テンプレートは - - タイトル を生成することになりますが、これはおそらくあなたが望むものではないでしょう。一般的に人々は、意味のないハイフンのないただのタイトルとなることを望むものです。これは、次のテンプレート構文を使用すれば達成できます:

{field:|prefix_text|suffix_text}

この テンプレート表現 は field の値が XXXX の場合、結果は prefix_textXXXXXsuffix_text になることを示しています。field が空 (値がない) 場合、プレフィックスとサフィックスが無視されるため、結果は空の文字列 (なし) となります。プレフィックスとサフィックスには空白を含めることができます。

プレフィックスとサフィックスには、サブテンプレート (`{ ... }`) や関数 (下を参照) を使用しないでください。

この構文を使用すると、テンプレートで上記のシリーズなしの問題を解決できます。

{series}{series_index:| - | - }{title}

ハイフンは、本にシリーズインデックスがある場合にのみ含まれます。シリーズインデックスはシリーズがある場合にのみ含まれます。 Foundationの例を再び続けると、テンプレートは Foundation - 1 - Second Foundation を生成します。

メモ:

- プレフィックスやサフィックスを使用している場合は、ルックアップ名 の後にコロンを含める必要があります。

- | の文字は、使用しないか両方使用するかのどちらかです。{field:| - } のようなひとつだけの使用は許されません。

- プレフィックスやサフィックスにテキストを指定せず、{series:|| - } にしてもかまいません。テンプレート {title:||}` は {title} と同じです。

書式設定
series_index を先行ゼロ付きの 3 桁に整形したいとします。次のようにすればそうできます:

{series_index:0>3s} - 先行ゼロ付きの 3 桁

後置ゼロには、次を使います:

{series_index:0<3s} - 後置ゼロ付きの 3 桁

1.1 などの小数値のシリーズインデックスを使用する場合は、小数点の位置を揃えたい場合があります。たとえば、インデックス 1 と 2.5 を 01.00 と 02.50 として表示して、辞書順に並べ替えを行うデバイスで正しく並べ替えられるようにすることができます。これを行うには、次を使用します:

{series_index:0>5.2f} - 先行ゼロ付きの 2 桁、小数点、および小数点以下 2 桁で構成される 5 文字。

データの最初の 2 文字だけが必要な場合は、次を使用します:

{author_sort:.2} - 著者読みの最初の 2 文字のみ

calibre テンプレート言語のフォーマットの多くは Python のものです。これらの高度なフォーマット操作の構文の詳細については、 Python documentation [https://docs.python.org/3/library/string.html#formatstrings] を参照してください。

テンプレートを使用してカスタム列を定義する

テンプレートを使用すると、calibre の書誌にない情報を表示したり、calibre の通常の形式とは異なる方法で書誌を表示したりできます。たとえば、calibre が表示しないフィールドである ISBN を表示したいことがあるかもしれません。これは、他の列から構築された列 タイプ (以降、複合列と呼びます) を使用してカスタム列を作成し、表示されるテキストを生成するためのテンプレートを提供することで実現できます。この列には、テンプレートの評価結果が表示されます。たとえば、ISBN を表示するには、列を作成し、テンプレートボックスに {identifiers:select(isbn)} と入力します。 2 つのシリーズカスタム列の値をカンマで区切って含む列を表示するには、{#series1:||,}{#series2} を使用します。

複合列は、書式設定を含む任意のテンプレートオプションを使用できます。

注: 複合列に表示されているデータを編集することはできません。代わりに、ソース列を編集します。複合列を編集する場合、たとえばダブルクリックした場合、calibreは、基になるデータではなく、編集用のテンプレートを開きます。

テンプレートとプラグボード

プラグボードは、デバイスへの送信およびディスクへの保存操作中に本に書き込まれる書誌を変更するために使用されます。プラグボードを使用すると、本の書誌に書き込むデータを提供するテンプレートを指定できます。プラグボードを使用すると authors, author_sort, language, publisher, tags, title, 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 は次の検索順でプラグボードを検索します:

- 形式とデバイスが完全に一致するプラグボード。例: EPUB かつ ANDROID

- 形式が完全に一致し、特別な 任意のデバイス を選択できるプラグボード。例: EPUB かつ 任意のデバイス

- 特別な 任意のフォーマット を選択し、デバイス上で完全に一致するプラグボード。例: 任意のフォーマット かつ ANDROID

- 任意のフォーマット と 任意のデバイス を備えたプラグボード

タグフィールドと著者フィールドはどちらも複数のアイテムを保持できるため、特殊な扱いがあります。本にはいくつものタグと何人もの著者を含めることができます。これら 2 つのフィールドのいずれかを変更するように指定すると、テンプレートの結果に複数の項目がないか確認されます。タグの場合、calibre はカンマを見つけるたびに結果を分割します。たとえばテンプレートが値 Thriller, Horror を生成すると、結果は2つのタグ、Thriller と ``Horror``になります。タグの途中にカンマを入れる方法はありません。

著者にも同じことが起こりますが、分割に別の文字、カンマの代わりに & (アンパサンド) を使用します。たとえばテンプレートが値 Blogs, Joe&Posts, Susan を生成すると、本の著者は``Blogs, Joe`` と Posts, Susan の 2 人になります。テンプレートが Blogs, Joe;Posts, Susan という値を生成すると、その本の著者はかなり変わった名前の 1 人となります。

プラグボードは、本がディスクに保存されるとき、またはデバイスに書き込まれるときに、本に書き込まれる書誌に影響を与えます。プラグボードは、ファイル名を作成するための ディスクに保存 や デバイスに送信 によって使われる書誌には影響しません。代わりに、ファイル名は、適切な設定ウィンドウに入力されたテンプレートを使用して作成されます。

テンプレートでの関数の使用 - 単関数モード

あるフィールドが通常はタイトルケースで、その値を大文字で表示したいと仮定します。テンプレート関数 を使用すれば、それが可能です。たとえば、タイトルを大文字で表示するには uppercase 関数を使用して、{title:uppercase()}` とします。タイトルケースで表示するには {title:titlecase()} を使用します。

関数は、テンプレートのフォーマット部分に入ります。それは : の後、かつ最初の | またはプレフィックス/サフィックスが使われていれば閉じ括弧 } の前です。フォーマットと関数参照の両方がある場合、関数は 2 個目の : の後に来ます。関数は、テンプレートで指定された列の値を適切に変更して返します。

関数を使用するための構文は、次のいずれかです:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

関数名の後には、必ず開き括弧と閉じ括弧を付ける必要があります。一部の関数は追加の値 (引数) を必要とし、これらは括弧内に入ります。引数はカンマで区切ります。リテラルコンマ (引数の区切り文字ではなくテキストとしてのカンマ) の前には、バックスラッシュ (\) を付ける必要があります。最後の (または唯一の) 引数には、テキストの閉じ括弧を含めることはできません。

関数は、フォーマット指定とプレフィックス/サフィックスの前に評価されます。フォーマットと関数の両方を使用する例については、さらに下を参照してください。

重要: プログラミングの経験がある場合は、単関数モード の構文が思ったものと違うことに注意してください。文字列は引用符で囲まれておらず、スペースは重要です。すべての引数は定数と見なされます。式はありません。

**サブテンプレート ({ ... }) を関数の引数として使用しないでください。**代わりに テンプレートプログラムモード および 一般プログラムモード を使用してください。

一部の関数には正規表現が必要です。テンプレート言語では、正規表現のマッチでは大文字と小文字が区別されません。

以下の関数説明書では、[something]* という表記は something が 0 回以上繰り返される可能性があることを意味しています。[something]+ という表記は something が 1 回以上繰り返される (1 回は必ず存在する) ことを意味しています。

単関数モードでの利用が想定されている関数は:

capitalize() -- 最初の文字が大文字で残りが小文字の値を返します。

contains(pattern, text if match, text if not match) -- 値が正規表現 pattern とマッチするかどうかを調べます。pattern が値にマッチすれば text if match を返し、そうでなければ text if no match を返します。

count(separator) -- 値を separator で区切られた項目のリストとして解釈し、リスト内の項目数を返します。ほとんどのリストは区切り文字としてカンマを使用しますが、authors はアンパーサンド (&) を使用します。例: {tags:count(,)}, {authors:count(&)}. Aliases: 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() -- 値が数値であると想定し、その数値を KB、MB、GB などで表す文字列を返します。

ifempty(text if empty) -- 値が空でない場合はフィールドの値を返し、そうでない場合は text if empty を返します

in_list(separator, [ pattern, found_val, ]* not_found_val) -- 値を separator で区切られた項目のリストと解釈し、リスト内の各項目に対して pattern をチェックします。pattern が項目にマッチする場合は found_val を返し、そうでなければ not_found_val を返します。pattern と found_value のペアは好きなだけ繰り返すことができるので、項目の値に応じて異なる値を返すことができます。pattern は順番にチェックされ、最初にマッチしたものが返されます。

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] を返します。例: {languages:language_strings()} もし localize がゼロなら、英語の文字列を返します。localize がゼロでなければ、現在のロケールの言語の文字列を返します。Lang_codes はカンマ区切りのリストです。

list_item(index, separator) -- 値を separator で区切られた項目のリストと解釈し、'index' 番目の項目を返します。最初の項目はゼロ番目です。最後の項目は -1 番目で、list_item(-1,separator)` のように指定します。項目がリストの中になければ、空の文字列を返します。

lookup([ pattern, key, ]* else_key) -- pattern は値に対して順番にチェックされます。pattern がマッチしたら、key で指定されたフィールドの値を返します。pattern にマッチするものがなければ、 else_key で指定されるフィールドの値を返します。switch (下記) を参照のこと。

lowercase() -- フィールドの値を小文字にして返します。

rating_to_stars(use_half_stars) -- 星 (★) の文字列で評価を返します。値は 0 から 5 までの数値でなくてはなりません。カスタム評価列で使用可能な小数値に星半分を使いたい場合は、use_half_stars に 1 を設定します。

re(pattern, replacement) -- 正規表現を適用した後に値を返します。値の中にある pattern のインスタンスはすべて replacement に置換されます。テンプレート言語では、大文字と小文字を区別しない Python 正規表現 [https://docs.python.org/3/library/re.html] を使用します。

select(key) -- 値を項目のカンマ区切りリストとして解釈します。項目はそれぞれ id:value (calibre の identifier ID形式) の形式です。この関数は、id が key に等しい最初のペアを見つけて、対応する値を返します。一致するIDがなければ、関数は空の文字列を返します。

shorten(left chars, middle text, right chars) -- 値の短縮バージョンを返します。短縮バージョンは値の先頭から left chars で指定した文字数分の文字列、その次に middle text、その後ろに値の末尾から right chars で指定した文字数分の文字列で構成されます。Left chars および right chars 負でない整数でなければなりません。例: 最大 15 文字の長さのタイトルを表示するとします。これを行うテンプレートの一例は、 {title:shorten(9,-,5)} です。Ancient English Laws in the Times of Ivanhoe というタイトルの本の場合は Ancient E-anhoe: タイトルの最初の 9 文字, -, そして末尾の 5 文字です。 値の長さが left chars + right chars + middle text の長さよりも短ければ、変更せずに返されます。たとえば The Dome は変更されません。

str_in_list(separator, [ string, found_val, ]+ not_found_val) -- 値を separator で区切られた項目のリストとして解釈し、string をリスト内の各値と比較します。string は正規表現ではありません。string が任意の項目と等しい場合 (大文字と小文字を区別しない)、対応する found_val を返します。string に separators が含まれていればリストとしても扱われ、各サブ値がチェックされます。string と found_value のペアは好きなだけ繰り返すことができ、文字列の値の応じて異なる値を返すことができます。マッチする文字列がなければ not_found_value が返されます。文字列は順番にチェックされ、最初にマッチしたものが返されます。

subitems(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" が含まれていると仮定すると:

- {#genre:subitems(0,1)} は "A, D" を返します

- {#genre:subitems(0,2)} は "A.B, D.E" を返します

sublist(start_index, end_index, separator) -- 値を separator で区切られた項目のリストとして解釈し、項目の start_index から end_index で作られた新しいリストを返します。項目の最初の番号はゼロです。インデックスが負の値であれば、リストの後ろから数えます。特殊なケースとして、ゼロの end_index はリストの長さと解釈されます。

タグ列 (カンマ区切り) に "A, B ,C" が含まれていると仮定した例:

- {tags:sublist(0,1,\,)} は "A" を返します

- {tags:sublist(-1,0,\,)} は "C" を返します

- {tags:sublist(0,-1,\,)} は "A, B" を返します

swap_around_articles(separator) -- 記事を最後に移動した値を返します。値はリストにすることもでき、その場合にはリスト内の各項目が処理されます。値がリストのときには、separator を指定する必要があります。separator の指定がなければ値はリストではなく単一の値として扱われます。記事 は title_sort を生成するために calibre によって使用される記事です。

swap_around_comma() -- B, A` の形式の値を指定すると A B を返します。これは「姓, 名」の形式の名前を「名 姓」に変換するのに最適です。値の中にカンマがなければ、関数は値を変更せずに返します。

switch([pattern, value,]+ else_value) -- pattern, value のペアごとに値が正規表現 pattern にマッチするか確認し、マッチすれば対応する value を返します。pattern にマッチするものがなければ else_value を返します。pattern, value のペアは好きなだけ繰り返すことができます。最初にマッチしたものが返されます。

test(text if not empty, text if empty) -- 値が空でなければ text if not empty を返し、空なら text if empty を返します。

titlecase() -- フィールドの値をタイトルケースにして返します。

transliterate() --ソースフィールドの単語の音を近似することによって形成されたラテンアルファベットの文字列を返します。たとえば、ソースフィールドが Фёдор Миха́йлович Достоевский の場合、この関数は Fiodor Mikhailovich Dostoievskii を返します。

uppercase() -- フィールドの値を大文字にして返します。

同じテンプレートで関数と書式を使用する

整数のカスタム列 #myint を 003 のように先行ゼロ付きで表示したいと仮定します。これを実現する方法のひとつは 0>3s という書式を使用することです。ただしデフォルトでは数値 (整数または浮動小数点) がゼロに等しい場合には、その値は空の文字列として表示されるため、値ゼロは 000 ではなく空の文字列となります。値を 000 と表示したいのであれば、書式文字列と ifempty 関数を両方使用して空の値をゼロに戻してやります。そのテンプレートは次のようになるでしょう:

{#myint:0>3s:ifempty(0)}

プレフィックスとサフィックスも使用できます。もし数字を [003] や [000] のように表示したいのであれば、次のテンプレートを使用します:

{#myint:0>3s:ifempty(0)|[|]}

一般プログラムモード

一般プログラムモード (GPM) は テンプレート表現 を テンプレート言語 で記述したプログラムに置き換えます。言語の構文は、次の文法で定義されます:

program ::= 'program:' expression_list expression_list ::= top_expression [ ';' top_expression ]* top_expression ::= or_expression or_expression ::= and_expression [ '||' and_expression ]* and_expression ::= not_expression [ '&&' not_expression ]* not_expression ::= [ '!' not_expression ]* | concatenate_expr concatenate_expr::= compare_expr [ '&' compare_expr ]* compare_expr ::= add_sub_expr [ compare_op add_sub_expr ]
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | 'inlist' |
'==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]* add_sub_op ::= '+' | '-'
times_div_expr ::= unary_op_expr [ times_div_op unary_op_expr ]* times_div_op ::= '*' | '/'
unary_op_expr ::= [ add_sub_op unary_op_expr ]* | expression expression ::= identifier | constant | function | assignment | field_reference | if_expr | for_expr | break_expr | continue_expr |
'(' expression_list ')' | function_def field_reference ::= '$' [ '$' ] [ '#' ] identifier identifier ::= id_start [ id_rest ]* id_start ::= letter | underscore id_rest ::= id_start | digit constant ::= " string " | ' string ' | number function ::= identifier '(' expression_list [ ',' expression_list ]* ')'
function_def ::= 'def' identifier '(' top_expression [ ',' top_expression ]* ')' ':'
expression_list 'fed'
assignment ::= identifier '=' top_expression if_expr ::= 'if' condition 'then' expression_list
[ elif_expr ] [ 'else' expression_list ] 'fi'
condition ::= top_expression elif_expr ::= 'elif' condition 'then' expression_list elif_expr | ''
for_expr ::= for_list | for_range for_list ::= 'for' identifier 'in' list_expr
[ 'separator' separator_expr ] ':' expression_list 'rof'
for_range ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
range_expr ::= 'range' '(' [ start_expr ',' ] stop_expr
[ ',' step_expr [ ',' limit_expr ] ] ')'
list_expr ::= top_expression break_expr ::= 'break'
continue_expr ::= 'continue'
separator_expr ::= top_expression start_expr ::= top_expression stop_expr ::= top_expression step_expr ::= top_expression limit_expr ::= top_expression

メモ:

- top_expression には常に値があります。expression_list の値は、リストの最後の top_expression の値です。たとえば、式リストの値 1;2;'foobar';3 は 3 です。

- 論理的なコンテキストでは、空でない値はすべて True

- 論理的なコンテキストでは、空の値はすべて False

- 文字列と数字は同じ意味で使用できます。たとえば 10 と '10' は同じものです。

- コメントは '#' 文字で始まる行です。行の途中から始まるコメントはサポートされていません。

演算子の優先順位

演算子の優先順位 (評価する順) の最高 (最初に評価) から最低 (最後に評価) までは、次のとおりです:

- 関数呼び出し、定数、括弧で囲まれた式、比較演算式、代入式、フィールド参照。

- 単項プラス (+) およびマイナス (-)。これらの演算子は右から左に評価します。

これらおよび他のすべての算術演算子は、式の小数部がゼロならば整数を返します。たとえば式が 3.0 を返す場合、それは 3 に変更されます。

- 乗算 (*) と除算 (/)。これらの演算子は結合性があり、左から右に評価します。評価の順序を変更する場合は、括弧を使用してください。

- 加算 (+) と減算 (-)。これらの演算子は結合性があり、左から右に評価します。

- 数値と文字列の比較。これらの演算子は、比較が成功した場合は '1' を返し、それ以外の場合は空の文字列 ('')を返します。比較は連想的ではありません。 a < b < c は構文エラーです。

- 文字列の連結 (&)。& 演算子は、左辺と右辺の式を連結して形成された文字列を返します。例: 'aaa' & 'bbb' は 'aaabbb' を返します。演算子は結合性があり、左から右に評価します。

- 単項論理否定 (!)。この演算子は、式が False (空の文字列に評価される) なら ``'1'``を返し、そうでなければ ``''``を返します。

- 論理積 (&&)。この演算子は、左辺と右辺の両方の式が True のときに '1' を返し、どちらかが False なら空の文字列 '' を返します。これは連想的で、左から右に評価し、 短絡評価 [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_reference は $ または $$ に続くルックアップ名で指定された書誌フィールドの値に評価されます。$ の使用は field() 関数の使用と同等です。$$ の使用は raw_field 関数の使用と同等です。例:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

if 文

If 文は最初に condition を評価します。condition が True (空でない値) であれば、then 句の中にある expression_list が評価されます。False なら elif または else 句の中に expression_list があればそれが評価されます。elif と else の部分はオプションです。if, then, elif, else, および fi は予約語です。識別子の名前として使用することはできません。 意味のあるところならどこにでも改行と空白を置くことができます。condition は top_expression であって expression_list ではありません。セミコロンは使用できません。expression_lists は top_expressions のセミコロン区切りのシーケンスです。if 文は評価した expression_list の中で最後の top_expression を返し、評価したものがなければ空の文字列を返します。

例:

* program: if field('series') then 'yes' else 'no' fi
* program:
if field('series') then
a = 'yes';
b = 'no'
else
a = 'no';
b = 'yes'
fi;
strcat(a, '-', b)

入れ子になった if の例:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

上で説明したように if は値を生成します。これは、次のすべてが同等であることを意味します:

* program: if field('series') then 'foo' else 'bar' fi
* program: if field('series') then a = 'foo' else a = 'bar' fi; a
* program: a = if field('series') then 'foo' else 'bar' fi; a

最後の例として、このプログラムは、本にシリーズがある場合は シリーズ 列の値を返し、そうでない場合は タイトル 列の値を返します:

program: field(if field('series') then 'series' else 'title' fi)

for 文
for 文は値のリストを反復処理し、一度にひとつずつ処理します。 list_expression は tags や #genre などの書誌フィールド ルックアップ名、または値のリストのいずれかに評価される必要があります。range() 関数 (下を参照) は数値のリストを生成します。 結果が有効な ルックアップ名 であればフィールドの値がフェッチされ、そのフィールドタイプに指定されたセパレータが使用されます。結果が有効なルックアップ名でない場合は、値のリストであると見なされます。オプションのキーワード separator が指定されていない限り、リストはコンマで区切られていると見なされます。指定されている場合、リスト値は、 separator_expr``の評価結果で区切る必要があります。``range() で生成されたリストには区切り文字は使用できません。リスト内の各値は指定された変数に割り当てられ、expression_list が評価されます。break を使ってループから抜けることもでき、continue を使うと次の反復のためにループの先頭に移動します。

例: このテンプレートは、ジャンル (#genre) の各値の最初の階層名を削除し、新しい名前でリストを作成します:

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

元のジャンルが History.Military, Science Fiction.Alternate History, ReadMe の場合、テンプレートは Military, Alternate History, ReadMe を返します。このテンプレートを calibre の 書誌を一括編集  →  検索と置換 で 検索 に template を設定して階層の最初のレベルを取り除き、結果の値をジャンルに割り当てます。

注: この場合、テンプレートの最後の行である new_tags は必ずしも必要ではありません。これは、for が式リストの最後の top_expression の値を返すためです。割り当ての値はその式の値であるため、for 文の値は new_tags に割り当てられたものです。

関数の定義

テンプレートの中に繰り返されるコードがある場合は、そのコードをローカル関数に配置できます。def キーワードで定義を開始します。その後に関数名、引数リスト、関数内のコードが続きます。関数定義は fed キーワードで終了します。

引数はキーワード引数です。関数が呼び出されると、指定された引数が定義されたパラメータに対して左から右に照合され、引数の値がパラメータに割り当てられます。定義されたパラメータよりも多くの引数を指定するとエラーになります。パラメータには、a = 25 などのデフォルト値を設定できます。そのパラメータに引数が指定されていない場合はデフォルト値が使用され、それ以外の場合はパラメータが空の文字列に設定されます。

return 文はローカル関数の中で使用できます。

関数は、使用する前に定義する必要があります。

例: このテンプレートは、日数から年、月、日でおおよその期間を計算します。関数 to_plural()` は、計算された値を整形します。ただしこの例では``&`` 演算子も使用してます::eo

program:
days = 2112;
years = floor(days/360);
months = floor(mod(days, 360)/30);
days = days - ((years*360) + (months * 30));

def to_plural(v, str):
if v == 0 then return '' fi;
return v & ' ' & (if v == 1 then str else str & 's' fi) & ' '
fed;

to_plural(years, 'year') & to_plural(months, 'month') & to_plural(days,'day')

関係演算子

関係演算子は、比較が真なら ``'1'``を返し、そうでなければ空の文字列 ('') を返します。

関係演算子は 2 種類あり、それは文字列比較と数値比較です。

文字列の比較では、大文字と小文字を区別しない文字列比較を辞書順で行います。サポートされている文字列比較演算子は、==, !=, <, <=, >, >=, in, および inlist です。 in 演算子の場合、左側の式の結果は正規表現パターンとして解釈されます。左側の正規表現の値が右側の式の値とマッチする場合、in 演算子は True です。左側の正規表現が、リスト内の項目がカンマで区切られている右側のリスト内の項目のいずれかにマッチする場合、inlist 演算子は True です。マッチには大文字と小文字は区別されません。

数値比較演算子は、==#, !=#, <#, <=#, >#, >=# です。左と右の式は、2 つの例外を除いて数値に評価される必要があります。文字列値 "None" (未定義のフィールド) と空の文字列の両方が値ゼロに評価されます。

例:

- program: field('series') == 'foo' は本のシリーズが 'foo' であれば '1' を返し、そうでなければ '' を返します。

- program: 'f.o' in field('series') は、本のシリーズが正規表現 f.o (例: foo, Off Onyx, など) とマッチすれば '1' を返し、そうでなければ '' を返します。

- 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') は、本のジャンルのいずれかが正規表現 ^science$ に完全一致すれば (例: Science) '1' を返します。ジャンル History of Science や Science Fiction はマッチしません。マッチするものがなければ '' を返します。

- program: if field('series') != 'foo' then 'bar' else 'mumble' fi は、本の series が foo でなければ 'bar' を返します。それ以外のときには 'mumble' を返します。

- program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi は、本の series が 'foo' または '1632' のとき 'yes' を返し、それ以外のときにいは 'no' を返します。

- program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi は series が 'foo' または '1632' なら 'yes'``を返し、それ以外のときには ``'no' を返します。

- program: if 11 > 2 then 'yes' else 'no' fi は 'no' を返します。なぜなら > 演算子が辞書比較を行うからです。

- program: if 11 ># 2 then 'yes' else 'no' fi は 'yes' を返します。なぜなら ># 演算子は数値比較を行うからです。

利用可能な追加の関数

単関数モード で説明したものに加えて、次の関数が利用できます。

GPM では、単関数モード で説明されている機能はすべて、操作する値を指定する最初のパラメータを追加で必要とします。すべてのパラメータは expression_lists です (上記の文法を参照)。

add(x [, y]*) -- 引数の合計を返します。引数が数値でない場合は、例外をスローします。ほとんどの場合、この関数の代わりに + 演算子を使用できます。

and(value [, value]*) -- すべての値が空でない場合は文字列 "1" を返し、そうでない場合は空の文字列を返します。必要な数だけ値を持つことができます。ほとんどの場合、この関数の代わりに && 演算子を使用できます。and と && を置き換えない理由のひとつは、副作用により短絡評価が結果を変えてしまう可能性があるためです。たとえば、and(a='',b=5) は常に両方の割り当てを行いますが、&& 演算子は 2 番目の割り当てを行いません。

assign(id, val) -- val を id に割り当ててから、val を返します。 id は、式ではなく識別子でなくてはなりません。ほとんどの場合、この関数の代わりに = 演算子を使用できます。

approximate_formats() -- 本に関連付けられている形式のカンマ区切りリストを返します。おそらく正しいとはいえ、リストが正しいという保証はありません。この関数およびその他のゼロパラメータ関数は、テンプレート {:'approximate_formats()'} を使用してテンプレートプログラムモード (以下を参照) で呼び出すことができます。結果の形式名は、EPUB の場合と同様に、常に大文字となります。approximate_formats() 関数は、以下で説明する ``formats_... `` 関数よりも大幅に高速です。

author_links(val_separator, pair_separator) -- 著者のリストとそれらの著者のリンク値を含む文字列を次の形式で返します:

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) -- returns the count of books found by searching for query. If use_vl is 0 (zero) then virtual libraries are ignored. This function and its companion book_values() are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak allow_template_database_functions_in_composites is set to True. It can be used only in the GUI.

For example this template search uses this function and its companion to find all series with only one book:

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

The first time the template runs (the first book checked) it stores the results of the database lookups in a global template variable named vals. These results are used to check subsequent books without redoing the lookups.

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) -- returns a list of the unique values contained in the column column (a lookup name), separated by sep, in the books found by searching for query. If use_vl is 0 (zero) then virtual libraries are ignored. This function and its companion book_count() are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak allow_template_database_functions_in_composites is set to True. It can be used only in the GUI.

booksize() -- calibre の 'size' フィールドの値を返します。形式がない場合は '' を返します。

check_yes_no(field_name, is_undefined, is_false, is_true) -- ルックアップ名 field_name で指定された yes/no フィールドの値がパラメータで指定された値のいずれかであるかどうかをチェックし、一致が見つかった場合は '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).

is_undefined, is_false, または is_true のうち複数を 1 に設定できます。

ceiling(x) -- x 以上の最小の整数を返します。 x が数値でない場合、例外をスローします。

character(character_name) -- character_name で指定された文字を返します。たとえば、character('newline') は改行文字 ('\n')を返します。サポートしている文字名は、newline, return, tab, および backslash です。

cmp(x, y, lt, eq, gt) -- x と y の両方を数値に変換した後、それらを比較します。x <# y なら lt、x ==# y なら eq を返し、それ以外の場合は gt を返します。この関数は通常、数値比較演算子(==#, <#, ># など)のひとつに置き換えることができます。

connected_device_name(storage_location_key) -- デバイスが接続されていればデバイス名を返し、そうでない場合は空の文字列を返します。デバイスの各保管場所には、独自のデバイス名があります。storage_location_key の名前は、'main', 'carda' および ``'cardb'``です。この機能は GUI でのみ機能します。

connected_device_uuid(storage_location_key) -- デバイスが接続されていればデバイスのuuid (一意のID) を返します。それ以外の場合は、空の文字列を返します。デバイスの保存場所ごとに異なる uuid がついています。storage_location_key の名前は、'main', 'carda' および ``'cardb'``です。この機能は GUI でのみ機能します。

current_library_name() -- 現在の calibre ライブラリへのパスの最後の名前を返します。

current_library_path() -- 現在の calibre ライブラリへのフルパスを返します。

current_virtual_library_name() -- 現在の仮想ライブラリがある場合はその名前を返します。ない場合は空の文字列を返します。ライブラリ名の大文字小文字は保持されます。例: program: current_virtual_library_name()。この機能はGUIでのみ機能します。

date_arithmetic(date, calc_spec, fmt) -- calc_spec を使用して date から新しい日付を計算します。オプションの fmt に従って整形された新しい日付を返します。指定されていない場合、結果は ISO 書式になります。 calc_specは、vW (valueWhat) のペアを連結して形成された文字列です。ここで、v は負の可能性のある数値であり、Wは次のいずれかの文字です:

- s: date に v 秒を加算

- m: date に v 分を加算

- h: date に v 時間を加算

- d: date に v 日を加算

- w: date に v 週間を加算

- y: date に v 年を加算、ただし 1 年は 365 日

例: '1s3d-1m' は date に 1 秒を加算、3 日を加算、1 分を減算します。

days_between(date1, date2) -- rdate1とdate2の間の日数を返します。 date1 が date2``より大きい場合、数値は正であり、それ以外の場合は負です。``date1 または ``date2``のいずれかが日付でない場合、関数は空の文字列を返します。

divide(x, y) -- x / y を返します。x または y のいずれかが数値でない場合、例外をスローします。この関数は通常、 / 演算子で置き換えることができます。

eval(string) -- 文字列をプログラムとして評価し、ローカル変数を渡します。これにより、テンプレートプロセッサを使用して、ローカル変数から複雑な結果を作成できます。テンプレートプログラムモード <template_mode>`では、テンプレートが評価される前に `{ および } の文字が解釈されるため、} の文字には [[ 、 } の文字には ]] を使用する必要があります。これは自動的に変換されます。テンプレートプログラムモード を使用している場合、この関数の引数にプレフィックスとサフィックス (|prefix|suffix 構文) を使用できないことにも注意してください。

extra_file_size(file_name) -- returns the size in bytes of the extra file file_name in the book's data/ folder if it exists, otherwise -1. See also the functions has_extra_files(), extra_file_names() and extra_file_modtime(). This function can be used only in the GUI.

extra_file_modtime(file_name, format_string) -- returns the modification time of the extra file file_name in the book's data/ folder if it exists, otherwise -1. The modtime is formatted according to format_string (see format_date() for details). If format_string is the empty string, returns the modtime as the floating point number of seconds since the epoch. See also the functions has_extra_files(), extra_file_names() and extra_file_size(). The epoch is OS dependent. This function can be used only in the GUI.

extra_file_names(sep [, pattern]) returns a sep-separated list of extra files in the book's data/ folder. If the optional parameter pattern, a regular expression, is supplied then the list is filtered to files that match pattern. The pattern match is case insensitive. See also the functions has_extra_files(), extra_file_modtime() and extra_file_size(). This function can be used only in the GUI.

field(lookup_name) -- ルックアップ名 lookup_name の書誌フィールドの値を返します。

field_exists(field_name) -- ルックアップ名 field_name のフィールド（列）が存在するかどうかを確認し、存在すれば '1' を返し、存在しない場合は空の文字列を返します。

finish_formatting(val, fmt, prefix, suffix) -- {series_index:05.2f| - |- } のようなテンプレートで行われるのと同じ方法で、フォーマット、プレフィックス、およびサフィックスを値に適用します。この関数は、複雑な単関数モードやテンプレートプログラムモードのテンプレートから GPM テンプレートへの変換を容易にするために提供されています。たとえば次のプログラムは、上記のテンプレートと同じ出力を生成します:

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

別の例: テンプレート {series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} には次を使用します:

program:
strcat(
re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
field('title')
)

first_matching_cmp(val, [ cmp, result, ]* else_result) -- val < cmp を順番に比較し、成功した最初の比較に関連づけられた結果を返します。比較が成功しなかった場合は else_result を返します。例:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

これは "large" を返します。同じ例で最初の値を 16 にすると "giant" を返します。

first_non_empty(value [, value]*) -- 空ではない最初の value を返します。すべての値が空の場合、空の文字列が返されます。必要なだけの数の value を渡すことができます。

floor(x) -- x 以下の最大の整数を返します。x が数値でない場合、例外をスローします。

format_date(val, format_string) -- format_string を使用して、文字列を返す値を整形します。これは日付文字列である必要があります。書式コードは次のとおりです:

- d    : 先行ゼロのない数値としての日 (1から31)

- dd   : 先行ゼロ付きの数値としての日 (01 to 31)

- ddd  : ローカライズされた曜日の省略名 (例: "Mon" から "Sun").

- dddd : ローカライズされた曜日の正式名 (例: "Monday" から "Sunday").

- M    : 先行ゼロのない数値としての月 (1 から 12).

- MM   : 先行ゼロ付きの数値としての月 (01 から 12)

- MMM  : ローカライズされた月の省略名 (例: "Jan" から "Dec").

- MMMM : ローカライズされた月の正式名 (例: "January" to "December").

- yy   : 2 桁の数字としての年 (00 から 99)

- yyyy : 4 桁の数字としての年

- h    : 先行ゼロのない時間 (am/pm に応じて 0 から 11 または 0 から 23)

- hh   : 先行ゼロ付きの時間 (am/pm に応じて 00 から 11 または 00 から 23)

- m    : 先行ゼロのない分 (0 から 59)

- mm   : 先行ゼロ付きの分 (00 から 59)

- s    : 先行ゼロのない秒 (0 から 59)

- ss   : 先行ゼロ付きの秒 (00 から 59)

- ap   : 24時間制ではなく12時間制を使用し、 'ap' を午前または午後のローカライズされた文字列に置き換えます。

- AP   : 24時間制ではなく12時間制を使用し、'AP' を AM または PM のローカライズされた文字列に置き換えます。

- iso  : 時間とタイムゾーンのある日付。単独で使用すること。

- to_number   : 日付と時刻を浮動小数点数 (タイムスタンプ) に変換します

- from_number : 浮動小数点数 (タイムスタンプ) を iso 書式の日付に変換します。別の日付書式が必要な場合は、from_number とコロン (:)の後に目的の書式文字列を追加します。例: from_number:MMM dd yyyy

整形する日付にローカライズされた月の名前が含まれていると、予期しない結果が生じる可能性があります。これは、日付書式の tweak を MMMM を含むように変更した場合に発生する可能性があります。この場合、代わりに field() 関数を次のように使用します:

format_date(field('pubdate'), 'yyyy')

次のように raw_field() 関数を使用します:

format_date(raw_field('pubdate'), 'yyyy')

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. Examples:

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() -- コロンで区切られたFMT：SIZEアイテムのコンマ区切りのリストを返し、本の形式のサイズをバイト単位で示します。 select 関数を使用して、特定の形式のサイズを取得できます。 EPUB のように、形式名は常に大文字となります。

fractional_part(x) -- 小数点以下の値を返します。たとえば fractional_part(3.14) は 0.14 を返します。x が数値でない場合、例外をスローします。

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() -- 本に表紙があれば 'Yes' を返し、そうでなければ空の文字列を返します。

has_extra_files([pattern]) -- returns the count of extra files, otherwise '' (the empty string). If the optional parameter pattern (a regular expression) is supplied then the list is filtered to files that match pattern before the files are counted. The pattern match is case insensitive. See also the functions extra_file_names(), extra_file_size() and extra_file_modtime(). This function can be used only in the GUI.

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

is_marked() -- 本が calibre でマークされているかどうかを確認します。マークされていればマークの値を返します。マークの値は 'true' (小文字) または名前付きマークのコンマ区切りリストのいずれかです。本がマークされていない場合は、 '' (空の文字列) を返します。この機能は 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 は必要な回数だけ繰り返すことができ、検索によって異なる値を返すことができます。patterm は順番にチェックされます。最初のマッチが返されます。別名: in_list(), list_contains()

list_count(value, separator) -- value を separator で区切られた項目のリストとして解釈し、リスト内の項目の数を返します。別名: count(), list_count()

list_count_matching(list, pattern, separator) -- list を separator で区切られた項目のリストとして解釈し、正規表現 pattern にマッチするリスト内の項目の数を返します。別名: list_count_matching(), count_matching()

list_difference(list1, list2, separator) -- 大文字と小文字を区別しない比較を使用して、list2 で見つかった項目を list1 から削除して作成されたリストを返します。 list1 と list2 の項目、および返されたリストの項目は、いずれも separator で区切られます。

list_equals(list1, sep1, list2, sep2, yes_val, no_val) -- list1 と list2 に同じ項目が含まれていれば yes_val を返し、そうでなければ no_val を返します。項目は、適切な区切り文字 (sep1 または sep2) を使用して各リストを分割することによって決定されます。リスト内の項目の順序は関係ありません。比較では大文字と小文字は区別されません。

list_intersection(list1, list2, separator) -- 大文字と小文字を区別しない比較を使用して、list2 にない項目を list1 から 削除して作成されたリストを返します。 list1 と list2 の項目、および返されたリストの項目は、いずれも separator で区切られます。

list_join(with_separator, list1, separator1 [, list2, separator2]*) -- ソースリスト (list1 など) の項目と結果リストの項目を with_separator を使用して結合して作成されたリストを返します。各ソース list[123...] の項目は、対応する``separator[123...]`` で区切られています。リストにはゼロ値を含めることができます。これは、値をひとつしか持たない、事実上 1 項目のリストである publisher のようなフィールドにすることができます。重複は、大文字と小文字を区別しない比較を使用して削除します。項目は、ソースリストに表示されている順序で返されます。リストの項目が大文字と小文字の違いしかない場合は、最後のものが使用されます。区切り文字はすべて、複数の文字にすることができます。

例:

program:
list_join('#@#', $authors, '&', $tags, ',')

次のように list_join への以前の呼び出しの結果に list_join を使用できます:

program:
a = list_join('#@#', $authors, '&', $tags, ',');
b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

式を使用してリストを生成できます。たとえば authors と `` #genre`` の項目が必要だと仮定して、しかしジャンルが "Genre: " の後にそのジャンルの頭文字をつけた単語に変更されるとします。つまりジャンル "Fiction" は "Genre: F" になります。次のようにするとそれを行います:

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

list_re(src_list, separator, include_re, opt_replace) -- 最初に src_list を separator を使用して項目に分割することにより、リストを作成します。リスト内の各項目について、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) -- list 内の重複項目を削除して作成されたリストを返します。項目に大文字と小文字しか違いがない場合、最後のものが返されます。list 内の項目、および返されたリスト内の項目は、いずれも separator で区切られます。

list_sort(list, direction, separator) -- 大文字と小文字を区別せず辞書順に並べ替えられた list を返します。方向がゼロの場合、リストは昇順に並べ替えられ、そうでない場合は降順並べ替えられますます。list の項目と、返されたリストの項目は、いずれも separator で区切られます。

list_split(list_val, sep, id_prefix) -- sep を区切り文字として使用して list_val を個別の値に分割し、id_prefix_N という名前のローカル変数に値を割り当てます。N はリスト内の値の位置です。最初の項目の位置は 0 (ゼロ) です。この関数は、リストの最後の要素を返します。

例:

list_split('one:two:foo', ':', 'var')

これは次と同等です:

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

list_union(list1, list2, separator) -- list1 と list2 の項目をマージし、重複する項目を大文字と小文字を区別しない比較を使用して削除して作成されたリストを返します。大文字と小文字が異なる場合は、list1 のものを使用します。list1 と list2 の項目と、返されたリストの項目は、いずれも separator で区切られます。別名: merge_lists(), list_union()

mod(x, y) -- x / y の余りの floor を返します。x と y のいずれかが数値でない場合、例外をスローします。

multiply(x [, y]*) -- 引数の積を返します。引数が数値でない場合は、例外をスローします。この関数は通常、* 演算子で置き換えることができます。

not(value) -- 値が空であれば文字列 "1" を返し、そうでなければ空の文字列を返します。この関数は通常、単項否定演算子 (!) に置き換えることができます。

ondevice() -- ondevice が設定されていれば文字列 'Yes' を返し、そうでなければ空の文字列を返します。

or(value [, value]*) -- 値が空でなければ文字列 '1' を返し、そうでなければ空の文字列を返します。必要なだけの数の value を渡せます。この関数は通常、|| 演算子に置き換えることができます。置き換えができないのは、短絡評価すると副作用により結果が変わる場合です。

print(a [, b]*) -- 引数を標準出力に出力します。コマンドライン (calibre-debug -g) から calibre を開始しない限り、出力はブラックホールに入ります。 print 関数は、常に最初の引数を返します。

range(start, stop, step, limit) -- パラメータ start、stop、および step で指定された範囲をループすることによって生成された数値のリストを、長さの上限値とともに返します。生成される最初の値は 'start' です。それに続く値は next_v = current_v + step です。step が正の数であれば next_v < stop の間、そうでなければ next_v > stop の間ループします。step が正のとき、start が start >= stop のテストに失敗すると、空のリストが生成されます。limit はリストの最大長を設定し、デフォルトは1000です。パラメータ start, step, および limit はオプションです。引数 1 個を指定して range() を呼び出すと、stop を指定します。引数 2 個は、start と stop を指定します。引数 3 個は、start, stop, および step``を指定します。引数 4 個は、``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]) -- 書式を適用せずに lookup_name で指定された書誌フィールドを返します。フィールドの値が未定義 (None) の場合、オプションの 2 番目の引数 optional_default を評価して返します。

raw_list(lookup_name, separator) -- 書式や並べ替えを適用せずに、lookup_name で指定した書誌リストを返します。項目は``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 }.

次の例では、複数の単語を含むシリーズを検索し、最初の単語を大文字にします:

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

round(x) -- x に最も近い整数を返します。x が数値でない場合、例外をスローします。

series_sort() -- シリーズ読みの値を返します。

strcat(a [, b]*) -- 任意の数の引数を渡せます。すべての引数を連結して形成された文字列を返します。

strcat_max(max, string1 [, prefix2, string2]*) -- 引数を連結して形成された文字列を返します。戻り値は string1 に初期化されます。結果の文字列の長さが max 未満である限り、prefix, string のペアから作成された文字列が値の最後に追加されます。プレフィックスは空にすることができます。 string1 が max より長くても、string1 を返します。必要なだけの数の prefix, string のペアを渡すことができます。

strcmp(x, y, lt, eq, gt) -- 大文字と小文字を区別しないxとyの字句比較を行います。x < y ならば lt を返し、x == y であれば eq を返し、それ以外の場合に gt を返します。この関数はたいていの場合、辞書比較演算子 (==, >, < など) のひとつに置き換えることができます。

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) -- 文字列 value の長さを返します。

substr(str, start, end) -- str の start 番目から end 番目まで文字列を返します。 str の先頭の文字は 0 番目の文字となります。 end が負の場合には、右から数えて end 番目までの文字となります。 end がゼロなら、最後の文字を示します。たとえば substr('12345', 1, 0) は '2345' を返し、substr('12345', 1, -1) は '234' を返します。

subtract(x, y) -- x - y を返します。 x と y のいずれかが数値でない場合、例外をスローします。この関数は通常、 - 演算子で置き換えることができます。

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.

today() -- 今日 (現在) の日付と時刻の文字列を返します。この値は format_date または days_between で使用するために設計されていますが、他の文字列と同じように取り扱うことができます。日付は ISO [https://en.wikipedia.org/wiki/ISO_8601] による日付/時刻の書式です。

template(x) -- x をテンプレートとして評価します。評価は独自のコンテキストで実行されます。つまり、変数は呼び出し元とテンプレート評価の間で共有されません。

to_hex(val) -- returns the string val encoded in hex. This is useful when constructing calibre URLs.

urls_from_identifiers(identifiers, sort_results) -- カンマで区切られた identifiers のリストを指定すると、その ID から生成されたHTML URLをカンマで区切ったリストで返します。identifier はコロンで区切られた値のペア(id_name:id_value) です。sort_results が 0 (文字または数値) のとき、リストは並べ替えられません。それ以外の場合は、ID 名のアルファベット順に並べ替えられます。 URLは、本の詳細 に表示されるビルトイン ID 列と同じ方法で生成されます。

より複雑なテンプレート表現のプログラム - テンプレートプログラムモード

テンプレートプログラムモード (TPM) は、 一般プログラムモード and 単関数モード を組み合わせたものです。 TPM は、他の書誌フィールドを参照し、ネストされた関数を使用し、変数を変更し、算術演算を実行するテンプレート式を記述できるという点で、単関数モードとは異なります。このテンプレートが { と } 文字の間に置かれ、program: という部分で始まらないという点で、一般プログラムモード とは異なります。テンプレートのプログラム部分は、一般プログラムモードの式リストです。

例: テンプレートで、ある本がシリーズものならシリーズを表示し、そうでなければカスタムフィールド #genre の値を表示したいと仮定します。単関数モード ではテンプレートの式の中からは別の書誌フィールドを参照できないため、この処理は実現できません。 TPM でなら可能です。次の式のようにします:

{#series:'ifempty($, field('#genre'))'}

例からはいくつかのことがわかります:

- TPM is used if the expression begins with :' and ends with '}. Anything else is assumed to be in Single Function Mode.

- 変数 $ は、テンプレートで指定されたフィールドを表します。式は操作対象で、この場合は #series です。

- 関数にはすべての引数を指定する必要があります。デフォルト値はありません。たとえば標準のビルトイン関数には、ソースフィールドを示す追加の初期パラメータを指定する必要があります。

- 空白は無視され、式の中のどこでも使用できます。

- 定数文字列は対になる引用符、' または " のいずれかで囲まれます。

単関数モード および 一般プログラムモード にリストされている関数はすべて TPM で使用できます。

TPM では、文字列リテラルで { および } 文字を使用するとテンプレートプロセッサが混乱するため、エラーや予期しない結果が発生する可能性があります。それらを文字ではなく、テンプレート式の境界として処理しようとするのです。必ずしもすべてのケースでうまく行くわけではありませんが、{ を [[ に、そして } を ]] に置き換えることができます。一般に、プログラムに { および } 文字が含まれている場合は、一般プログラムモード を使用すべきです。

一般プログラムモード と同様に、単関数モード で書かれた関数の場合、書かれているパラメータに加えて、関数が最初のパラメータとして機能する値を指定する必要があります。 TPM では、 $ を使用して、テンプレート式の ルックアップ名 で指定された値にアクセスできます。

Python Template Mode

Python Template Mode (PTM) lets you write templates using native python and the calibre API [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. The database API will be of most use; further discussion is beyond the scope of this manual. PTM templates are faster and can do more complicated operations but you must know how to write code in python using the calibre API.

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.

The context.funcs attribute allows calling Built-in and User template functions, and Stored GPM/Python templates, so that you can execute them directly in your code. The functions are retrieved using their names. If the name conflicts with a Python keyword, add an underscore to the end of the name. Examples:

context.funcs.list_re_group()
context.funcs.assert_()

Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a Column built from other columns, behaves like tags. It shows in Book details and has the on separate lines checked (in Preferences → Look & feel → Book details). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses 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:

保存済みテンプレート

Both General Program Mode and Python Template Mode support saving templates and calling those templates from another template, much like calling stored functions. You save templates using Preferences → Advanced → Template functions. More information is provided in that dialog. You call a template the same way you call a function, passing positional arguments if desired. An argument can be any expression. Examples of calling a template, assuming the stored template is named foo:

- foo() -- 引数を渡さずにテンプレートを呼び出します。

- foo(a, b) は、2 つの変数 a と b の値を渡すテンプレートを呼び出します。

- foo(if field('series') then field('series_index') else 0 fi) -- 本に series があれば series_index を渡し、そうでなければ値 0 を渡します。

In GPM you retrieve the arguments passed in the call to the stored template using the arguments function. 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. For example, the following arguments function declares 2 variables, key, alternate:

arguments(key, alternate='series')

例として、保存されたテンプレートの名前が今回も foo であると仮定します:

- foo('#myseries') -- 引数 key には値 'myseries' が割り当てられ、引数 alternate にはデフォルト値 'series' が割り当てられます。

- foo('series', '#genre') 変数 key には値 'series' が割り当てられ、変数 alternate には値 '#genre' が割り当てられます。

- foo() -- 変数 key には空の文字列が割り当てられ、変数 alternate には値 '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.

保存済みテンプレートをテストする簡単な方法は、 テンプレートテスタ ダイアログを使用することです。アクセスしやすくするには、環境設定 → 高度な設定 → キーボードショートカット → テンプレートテスタ でキーボードショートカットを指定します。 保存済みテンプレート ダイアログにショートカットを指定すると、テスタをすばやく切り替えたり、保存されたテンプレートのソースコードを編集したりするのに役立ちます。

テンプレートに追加情報を与える

開発者は、アプリケーション固有の本の書誌やプロセッサに要求されていることに関する情報など、追加情報をテンプレートプロセッサに渡すことを選択できます。テンプレートはこの情報にアクセスして、評価中に使用できます。

開発者: 追加情報の渡し方

The additional information is a Python dictionary containing pairs variable_name: variable_value where the values must be strings. The template can access the dictionary, creating template local variables named variable_name containing the value variable_value. The user cannot change the name so it is best to use names that won't collide with other template local variables, for example by prefixing the name with an underscore.

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={})

テンプレート作者: 追加情報のアクセス方法

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.

モード間の違いに関する注意事項

3つのプログラムモード、 単関数モード (SFM), テンプレートプログラムモード (TPM), and 一般プログラムモード (GPM)は、それぞれ動作が異なります。 SFMは「シンプル」を目指しているため、多くのプログラミング言語情報を隠します。

相違点:

- SFM では、列の値は常に「非表示」の最初の引数としてテンプレートに含まれる関数に渡されます。

- SFMは、変数と文字列の違いをサポートしていません。すべての値は文字列です。

- 次のSFMテンプレートは、シリーズ名か文字列 "no series" のいずれかを返します:

{series:ifempty(no series)}

TPM で同等のテンプレートは

{series:'ifempty($, 'no series')'}

GPM で同等のテンプレートは:

program: ifempty(field('series'), 'no series')

ifempty の最初の引数は、フィールド series の値です。 2 番目の引数は文字列 no series です。 SFMでは、最初の引数であるフィールドの値が自動的に渡されます (非表示の引数)。

- booksize() や current_library_name() などのテンプレート関数には引数を受け取らないものがいくつかあります。 「目に見えない引数」を使用するせいで、SFMではこれらの関数は使用できません。

- 関数が別の関数を呼び出して引数を計算する入れ子関数は、SFMでは使用できません。たとえば、大文字のシリーズ値の最初の 5 文字を返すことを目的としたこのテンプレートは、SFMでは機能しません:

{series:uppercase(substr(0,5))}

- TPM と`GPM` は、入れ子の関数をサポートします。 TPM の上記のテンプレートは次のようになります。

{series:'uppercase(substr($, 0,5))'}

GPM では次のようになります:

program: uppercase(substr(field('series'), 0,5))

- 上記のテンプレートプログラムモードのセクションで説明したように、TPM文字列リテラルで{および}文字を使用すると、テンプレートプロセッサが混乱するため、エラーや予期しない結果が発生する可能性があります。それらを文字ではなくテンプレート境界として処理しようとします。すべてではありませんが { を [[ に、そして } を ]] に置き換えることができる場合もあります。一般的に、プログラムに { や } の文字が含まれている場合には 一般プログラムモード を使用すべきです。

ユーザ定義の Python テンプレート関数

テンプレートプロセッサに独自の Python 関数を追加できます。このような関数は、3つのテンプレートプログラミングモードのいずれでも使用できます。関数は 環境変数  →  高度な設定  →  テンプレート関数 に行って追加します。手順はそのダイアログに表示されます。

テンプレートの保存/送信に関する特記事項

あるテンプレートが ディスクに保存 や デバイスに送信 テンプレートの中で使用される場合、特別な処理が適用されます。フィールドの値がクリーンアップされ、ファイルシステムに固有の文字がスラッシュを含むアンダースコアに置き換えられます。これは、フィールドテキストはフォルダの作成には使用できないことを意味します。ただし、プレフィックス文字列またはサフィックス文字列ではスラッシュが変更されないため、これらの文字列にスラッシュを含めると、フォルダが作成されます。このため、深さが可変のフォルダ構造を作成できます。

たとえば、フォルダの構造を series/series_index - title としたいが、シリーズが存在しない場合はタイトルを最上位のフォルダに配置したいと仮定します。これを行うためのテンプレートは次のとおりです:

{series:||/}{series_index:|| - }{title}

スラッシュとハイフンは、シリーズが空でない場合にのみ表示されます。

ルックアップ関数を使用すると、さらに高度な処理を実行できます。たとえば、本にシリーズがあるときにはフォルダ構造を series/series index - title.fmt にしたいと仮定します。そして本にシリーズがない場合は、フォルダ構造を genre/author_sort/title.fmt にしたいものとします。その本にジャンルがない場合は 'Unknown' を使用します。series の値に応じて、2つの完全に異なるパスが必要です。

これを達成するには:

1. {series}/{series_index} - {title} を含む複合フィールドを作成します (ルックアップ名 #aa を指定します)。シリーズが空でなければ、このテンプレートは series/series_index - title を生成します。

2. {#genre:ifempty(Unknown)}/{author_sort}/{title} を含む複合フィールドを作成します (ルックアップ名 #bb を指定します)。このテンプレートは genre/author_sort/title を生成し、空のジャンルは 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.

ヒント

- テンプレートテスタを使用してテンプレートをテストします。テスタをライブラリ内の本のコンテキストメニューに追加するか、キーボードショートカットを指定します。

- テンプレートは、他のテンプレートを使用できます。それをするには、使用したいテンプレートの作成した複合列を参照します。または、保存済みテンプレートを使用することもできます。

- 書誌の置換に加えて、テンプレートには条件付きで追加のテキストを含め、置換されたデータの整形方法を制御できます。

- ゼロ値であっても数値を表示する上記の手法は、標準フィールド series_index で機能します。

関数リファレンス

Reference for all built-in template language functions

Arithmetic

- add(x [, y]*)
- ceiling(x)
- divide(x, y)
- floor(x)
- fractional_part(x)
- mod(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)

- urls_from_identifiers(identifiers, sort_results)

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

- check_yes_no(field_name, is_undefined, is_false, is_true)

- 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_template]+)

- list_remove_duplicates(list, separator)
- list_sort(list, direction, separator)
- list_split(list_val, sep, id_prefix)
- 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

- cmp(x, y, lt, eq, gt)

- 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_count(query, use_vl)

- book_values(column, query, sep, use_vl)

- extra_file_modtime(file_name, format_string)

- 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

© Copyright 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)

- mod(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)

- urls_from_identifiers(identifiers, sort_results)

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

- 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

- check_yes_no(field_name, is_undefined, is_false, is_true)

- 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_template]+)

- list_remove_duplicates(list, separator)

- list_sort(list, direction, separator)

- list_split(list_val, sep, id_prefix)

- 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

- cmp(x, y, lt, eq, gt)

- 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_count(query, use_vl)

- book_values(column, query, sep, use_vl)

- extra_file_modtime(file_name, format_string)

- 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 が数値でない場合は例外を起こします。

mod(x)

class calibre.utils.formatter_functions.BuiltinMod[ソース]

mod(x) -- 余剰 (x / y の余り) を返します。x または y が数字でない場合に、エラーを発生します。

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" を返し、そうでなければ空の文字列を返します。tes t関数やfirst_non_empty 関数と組み合わせて利用できます。引数には、文字列をいくつでも指定できます。たいていの場合には、&& 演算子でこの関数を置き換えることが可能です。

not(value)

class calibre.utils.formatter_functions.BuiltinNot[ソース]

not(value) -- value が空なら "1" を返し、そうでなければ空の文字列を返します。test 関数や first_non_empty 関数と一緒に利用できます。多くの場合 ! 演算子で置き換えることが可能です。

or(value [, value]*)

class calibre.utils.formatter_functions.BuiltinOr[ソース]

or(value [, value]*) -- value が空でなければ "1" を返し、そうでなければ空の文字列を返します。この関数は test 関数や first_non_empty 関数と一緒に利用できます。value はいくつでも指定することができます。多くの場合、 || 演算子でこの関数を置き換えることが可能です。

Date functions

date_arithmetic(date, calc_spec, fmt)

class calibre.utils.formatter_functions.BuiltinDateArithmetic[ソース]

date_arithmetic(date, calc_spec, fmt) -- 'date' から 'calc_spec' を使って新しい日付を計算します。オプションで 'fmt' が指定されていればその形式に従った新しい日付を返します。オプションの指定がなければISO形式で返します。calc_spec は vW (valueWhat) の形に連結した文字列で、v は負の数を含む整数で、W は次の文字のうちのいずれかです: s: 'v' 秒を 'date' に加算、m: 'v' 分を 'date' に加算、h: 'v' 時間を 'date' に加算、d: 'v' 日を 'date' に加算、w: 'v' 週を 'date' に加算、y: 'v' 年を加算、ただし1年を365日とします。使用例: '1s3d-1m' は 'date' に1秒加算、3日加算、そして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) -- format、prefix、suffix を {series_index:05.2f| - |- } と同じ方法で適用します。例えば、以下のプログラムは上記のテンプレートと同じ結果を返します。program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

format_date(val, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDate[ソース]

format_date(val, format_string) -- 日時の値 val を format_string を使ってフォーマットし、文字列を返します。フォーマットコードは次のとおり:
d : 月の日付 (1 - 31)
dd : 2桁の月の日付 (01 - 31)
ddd : 省略した曜日名 (例 "Mon" - "Sun")
dddd : 省略なしの曜日名 (例 "Monday" - "Sunday")
M : 月 (1 - 12)
MM : 2桁の月 (01 - 12)
MMM : 省略された月の名称 (例 "Jan" - "Dec").
MMMM : 省略なしの月の名称 (例 "January" - "December").
yy : 2桁の年 (00 - 99)
yyyy : 4桁の年 h : 時間 (am/pmの指定により 0 - 11 または 0 - 23)
hh : 2桁の時間 (am/pmの指定により 00 - 11 または 00 - 23)
m : 分 (0 - 59)
mm : 2桁の分 (00 - 59)
s : 秒 (0 - 59)
ss : 2桁の秒 (00 - 59)
ap : 24時間表記でなく12時間表記を使用。"ap" では am または pm にローカライズされた文字列が使用されます。
AP : 24時間表記でなく12時間表記を使用。"ap" では AM または PM にローカライズされた文字列が使用されます。
iso : 時刻とタイムゾーン付きの日付。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

format_date_field(field_name, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDateField[ソース]

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

format_number(v, template)

class calibre.utils.formatter_functions.BuiltinFormatNumber[ソース]

format_number(v, template) -- 数値 v を "{0:5.2f}" や "{0:,d}" や "${0:5,.2f}" などPythonのフォーマットテンプレートを使ってフォーマットします。テンプレートの一部であるfield_nameは0 (ゼロ) (上記例では "{0:" ) でなくてはなりません。もっと詳しい例についてはテンプレート言語とPythonのドキュメントを参照してください。テンプレートにフォーマットがひとつしか指定されていない場合には、先頭の "{0:" と最後の "}" は省略可能です。フォーマットに失敗したときには空のstringを返します。

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) -- 評価を星の文字で返します。value は0から5までの数字です。2.5 のような整数以外の評価をサポートするカスタム評価列を利用したい場合には use_half_stars に 1 を指定します。

urls_from_identifiers(identifiers, sort_results)

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[ソース]

urls_from_identifiers(identifiers, sort_results) -- 識別子のカンマ区切りリストを指定すると、識別子から生成されたHTML URLのカンマ区切りリストを返します。識別子はコロンで区切られた値のペア (name:id_value) です。sort_results が 0 (文字または数字) の場合、リストはソートされません。それ以外の場合は、識別子名のアルファベット順にソートされます。URL は、本の詳細に表示される場合、組み込みの識別子列と同じ方法で生成されます。

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のように常に大文字になることに注意してください。この関数はGUI上でのみ機能します。この値を「ディスクに保存」や「ディスクに送る」のテンプレートで使用したい場合には、「他の列から作られる列」でこの関数を使用したカスタム列を作成し、その列の値を「ディスクに保存」や「ディスクに送る」のテンプレートで参照するようにしてください。

author_links(val_separator, pair_separator)

class calibre.utils.formatter_functions.BuiltinAuthorLinks[ソース]

author_links(val_separator, pair_separator) -- 著者と著者のリンクを、author1 val_separator author1link pair_separator author2 val_separator author2link の書式で返します。著者と著者のリンクは val_separator 文字列で区切られ、それ際にスペースなどは付加されません。著者と著者のリンクのペアは、pair_separator によって区切られ、その際にスペースなどは付加されません。 separator 文字列が、著者や著者リンクに含まれないようにするのは、利用者側の責任で行ってください。著者は著者リンクがなくても戻り値に含まれます。

author_sorts(val_separator)

class calibre.utils.formatter_functions.BuiltinAuthorSorts[ソース]

author_sorts(val_separator) -- 本の著者の著者読みの値を、リストにして返します。返される著者読みは、書誌の著者から取得したものです(本の著者読みの値とは異なる場合があります)。返されるリストは、author sort 1 val_separator author sort 2 のような形式になります。リストの著者読みの並び順は、本の著者の並び順と同じになります。val_separator にスペースで囲いたい場合には、separator 文字列に含めてください。

booksize()

class calibre.utils.formatter_functions.BuiltinBooksize[ソース]

booksize() -- 本のサイズを返します。この関数はGUI上でのみ機能します。この値を「ディスクに保存」や「ディスクに送る」のテンプレートで使用したい場合には、「他の列から作られる列」でこの関数を使用したカスタム列を作成し、その列の値を「ディスクに保存」や「ディスクに送る」のテンプレートで参照するようにしてください。

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() -- 現在のライブラリのパスの最後のフォルダ名を返します。この関数はテンプレートプログラムモードで以下のように呼び出すことができます 。"{:'current_library_name()'}"

current_library_path()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[ソース]

current_library_path() -- 現在のライブラリのパスを返します。この関数はテンプレートプログラムモードで以下のように呼び出すことができます。"{:'current_library_path()'}".

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() -- もし本が表紙を持っていればYesを返し、それ以外では空文字列を返す。

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() -- ondevice が設定されている場合に Yes を返し、その他の場合は空文字列を返します。この関数はGUI上でのみ機能します。この値を「ディスクに保存」や「ディスクに送る」のテンプレートで使用したい場合には、「他の列から作られる列」でこの関数を使用したカスタム列を作成し、その列の値を「ディスクに保存」や「ディスクに送る」のテンプレートで参照するようにしてください。

raw_field(lookup_name [, optional_default])

class calibre.utils.formatter_functions.BuiltinRawField[ソース]

raw_field(lookup_name [, optional_default]) -- lookup_name で名前を指定された書籍情報のフィールドを整形する前の状態で返します。フィールドが定義されていない場合 ('None') には、オプションで指定する2番目の引数をデフォルトとして返します。

raw_list(lookup_name, separator)

class calibre.utils.formatter_functions.BuiltinRawList[ソース]

raw_list(lookup_name, separator) -- lookup_name と名付けた書誌リストを返す。書式設定や並べ替えを行わず、項目は separator で区切られる。

series_sort()

class calibre.utils.formatter_functions.BuiltinSeriesSort[ソース]

series_sort() -- シリーズのソート値を返します

user_categories()

class calibre.utils.formatter_functions.BuiltinUserCategories[ソース]

user_categories() -- この本が含まれるユーザカテゴリをカンマ区切りリストで返します。この関数はGUI上でのみ機能します。この値を「ディスクに保存」や「ディスクに送る」のテンプレートで使用したい場合には、「他の列から作られる列」でこの関数を使用したカスタム列を作成し、その列の値を「ディスクに保存」や「ディスクに送る」のテンプレートで参照するようにしてください。

virtual_libraries()

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[ソース]

virtual_libraries() -- この本を含む仮想ライブラリのリストをカンマ区切りで返します。この関数はGUI上でのみ利用可能です。この値を save-to-disk テンプレートや send-to-device テンプレートで利用したい場合には、カスタムで "別の列から生成された列" を作成し、その列のテンプレート内で関数を使用し、列の値を save / send テンプレートで利用する必要があります。

If-then-else

check_yes_no(field_name, is_undefined, is_false, is_true)

class calibre.utils.formatter_functions.BuiltinCheckYesNo[ソース]

check_yes_no(field_name, is_undefined, is_false, is_true) -- 引数で指定された検索キー field_name に一致する yes/no フィールドをチェックし、一致するものが見つかれば "yes" を返し、そうでなければ空の文字列を返します。引数 is_undefined, is_false, または is_true を 1 (数字) を設定すると、その条件をチェックします。チェックしない場合には 0 を設定します。使用例: check_yes_no("#bool", 1, 0, 1) は yes/no フィールド "#bool" が未定義 (True でも Falseでもない) または True のときに"yes" を返します。is_undefined, is_false, または is_true can のうちひとつ以上に 1 を設定できます。この関数は通常 test() 関数、または is_empty() 関数から利用されます。

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) -- フィールドに正規表現'pattern'が含まれているかをチェックします。マッチした場合`text if match`を、それ以外の場合には `text if no match`を返します。

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を返します。それ以外の場合は `text if empty`を返します。

test(val, text if not empty, text if empty)

class calibre.utils.formatter_functions.BuiltinTest[ソース]

test(val, text if not empty, text if empty) -- もし 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]*) -- 空ではない最初の値を返します。すべての値が空だっった場合には、空の文字列を返します。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) -- 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.

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]) -- 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.

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) -- val を separator で区切られた項目のリストとして解釈し、index 番目の物を返す。最初の項目は０番目です。最後の項目は list_item(-1,separator) とすれば返ります。separator は 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 を separator で区切られた項目のリストとして扱い、文字列がリスト値のいずれかと一致する場合は found_val を返します。文字列がリスト値と一致しない場合niは、not_found_val を返します。比較は完全一致 (「含む」ではない) であり、大文字と小文字は区別されません。文字列と found_value のペアは、必要な回数だけ繰り返すことができます。パターンは順番にチェックされます。最初に一致した found_val が返されます。

List manipulation

count(val, separator)

class calibre.utils.formatter_functions.BuiltinCount[ソース]

count(val, separator) -- value を separator で区切られた項目のリストとして解釈し、リストの中にある項目数を返します。たいていのリストは 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、そして返されるリストの項目は separator で区切られます。

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、そして返されるリストの項目は separator で区切られます。

list_join(with_separator, list1, separator1 [, list2, separator2]*)

class calibre.utils.formatter_functions.BuiltinListJoin[ソース]

list_join(with_separator, list1, separator1 [, list2, separator2]*) -- return a list made by joining the items in the source lists (list1, etc) using with_separator between the items in the result list. Items in each source list[123...] are separated by the associated separator[123...]. A list can contain zero values. It can be a field like publisher that is single-valued, effectively a one-item list. Duplicates are removed using a case-insensitive comparison. Items are returned in the order they appear in the source lists. If items on lists differ only in letter case then the last is used. All separators can be more than one character.
Example:

program:

list_join('#@#', $authors, '&', $tags, ',')

次のようにすると、ひとつ前の list_join の実行結果に対して list_join を使用できます:

program:

a = list_join('#@#', $authors, '&', $tags, ','); b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&')

リストの生成には式が使えます。たとえば 'authors' と '#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 を separator 文字で分割しリストにします。それぞれの要素について、include_re の正規表現とのマッチを行い、マッチしたもののみ戻り値のリスト加えます。もし opt_replace が空文字列でなかった場合、戻り値のリストに加える前に、それぞれの要素に置換を適用します。

list_re_group(src_list, separator, include_re, search_re [, group_template]+)

class calibre.utils.formatter_functions.BuiltinListReGroup[ソース]

list_re_group(src_list, separator, include_re, search_re [, group_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) -- ソースリスト内の重複項目を削除したリストを返します。大文字と小文字だけが異なる項目の場合は、最後の項目が返されます。ソースリスト内の項目は、戻されたリスト内の項目と同様に、separator で区切られます。

list_sort(list, direction, separator)

class calibre.utils.formatter_functions.BuiltinListSort[ソース]

list_sort(list, direction, separator) -- 大小文字無視の整列を行ったlistを返します。もし direction が0の場合、リストは昇順で、それ以外の時には降順で整列されます。list と返されるリストの項目は separator で区切られます。

list_split(list_val, sep, id_prefix)

class calibre.utils.formatter_functions.BuiltinListSplit[ソース]

list_split(list_val, sep, id_prefix) -- 'sep' を使用してlist_valを別々の値に分割し、その値を 'id_prefix_N' という名前の変数に割り当てます (N はリスト内の値の位置)。最初の項目の位置は 0 (ゼロ) です。この関数は、リストの最後の要素を返します。例: split('one:two:foo', ':', 'var') は var_0 = 'one'; var_1 = 'two'; var_2 = 'foo' と等価です。

list_union(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListUnion[ソース]

list_union(list1, list2, separator) -- list1 と list2 の項目をマージししてできたリストを返します。リストからは重複する項目は削除されます (大文字/小文字を区別しない比較を使用)。項目の大文字と小文字が異なる場合は、list1 の項目が使用されます。list1 と list2 の項目は、返されるリスト内の項目と同様に、separator で区切られます。別名: list_union()、merge_lists()

range(start, stop, step, limit)

class calibre.utils.formatter_functions.BuiltinRange[ソース]

range(start, stop, step, limit) -- returns a list of numbers generated by looping over the range specified by the parameters start, stop, and step, with a maximum length of limit. The first value produced is 'start'. Subsequent values next_v 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).

subitems(val, start_index, end_index)

class calibre.utils.formatter_functions.BuiltinSubitems[ソース]

subitems(val, start_index, end_index) -- この関数はジャンルのような項目のリストを分解します。値をカンマ区切りのリストとして解析し、それぞれの値をさらにピリオド区切りのリストとして解析します。そして、ピリオド区切りのリストの start_index から end_index を取り出し、それを再度リストに組み立てて返します。ピリオド区切りのリストの最初の要素のインデックスは 0 になります。インデックスが負値の場合は、リストの後ろから数えます。特別のケースとして、end_index が 0 の場合、リストの長さの値と同じと見なされます。以下の例では、基本テンプレートモードを用います。#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) -- valをseparatorで区切られた項目のリストとして解釈し、start_index から end_index の項目のリストを返す。最初の項目は0です。もしインデックスが負の場合には、リストの最後から数えます。特別なケースとしてend_indexが0の時にはリストの長さをあらわします。例えば、ベーシックテンプレートモードでタグ列（それはカンマ区切りのリスト)を扱う場合、 "A, B, C": {tags:sublist(0,1,\,)} は "A" を返します。 {tags:sublist(-1,0,\,)} は "C"を返します。 {tags:sublist(0,-1,\,)} は "A, B"を返します。

Other

arguments(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinArguments[ソース]

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.

assign(id, val)

class calibre.utils.formatter_functions.BuiltinAssign[ソース]

assign(id, val) -- id に val を割り当て、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 をコマンドラインから実行しない限り (calibre-debug -g) この出力はどこにも表示されません。

Recursion

eval(template)

class calibre.utils.formatter_functions.BuiltinEval[ソース]

eval(template) -- 本の書誌の代わりにローカル変数(あらかじめ 'assign' された変数)を渡して、template を評価します。これにより、テンプレート処理でローカル変数を使ったより複雑な結果を生成することができます。{ と } は特殊文字であるため、{ の代わりに [[ を、 } の代わりに ]] を使わなければなりません。これらの文字は自動的に変換されます。例, template('[[title_sort]]') は、テンプレート {title_sort} を評価してその値を返します。テンプレートプログラムモードを使用している場合は、引数に prefixes と suffixes (|prefix|suffix の書式) が使用できないことに注意してください。

template(x)

class calibre.utils.formatter_functions.BuiltinTemplate[ソース]

template(x) -- x をテンプレートとして評価します。それぞれの評価はそれぞれ独自のコンテキストで行われます。つまり変数は呼出元とそれぞれの評価で共有されません。{ と } は特殊文字であるため、{ の代わりに [[ を、 } の代わりに ]] を使わなければなりません。これらの文字は自動的に変換されます。例, template('[[title_sort]]') は、テンプレート {title_sort} を評価してその値を返します。テンプレートプログラムモードを使用している場合は、引数に prefixes と suffixes (|prefix|suffix の書式) が使用できないことに注意してください。

Relational

cmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinCmp[ソース]

cmp(x, y, lt, eq, gt) -- x と y をそれぞれ数字に変換してから比較します。x < y ならば lt を返し、x == y ならば eq を返し、それ以外の場合に gt を返します。多くの場合、数字の比較演算子 (>#, <#, ==# など) でこの関数を置き換えることが可能です。

first_matching_cmp(val, [cmp1, result1,]+, else_result)

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[ソース]

first_matching_cmp(val, [cmp1, result1,]+, else_result) -- "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) -- does a case-sensitive comparison of x and y as strings. Returns lt if x < y. Returns eq if x == y. Otherwise returns 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.

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 で名付けられた文字を返します。たとえば character('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]*) -- pattern で指定した正規表現を val に適用し、それぞれのマッチを指定されたテンプレートから返された値で置換します。マッチした元の値は $ で利用可能です。テンプレートプラグラムモードでは、 { の代わりに [[ を、} の代わりに ]] を使用します。テンプレートプログラムモードの次の例では、ひとつ以上の単語を含み、最初の文字が大文字であるシリーズを検索します: {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 の先頭から 'left chars' 個の文字、次に 'middle text'、さらに末尾から 'right hars' 個の文字で構成された値です。'left chars' と 'right chars' には整数を指定します。例えば本のタイトルが "Ancient English Laws in the Times of Ivanhoe" だとして、それを15文字以内に収めたいとしましょう。{title:shorten(9,-,5)} を使用すると、結果は "Ancient E-nhoe" となります。もしフィールド長が left chars + right chars + middle text より短い場合、そのままの値が使用されます。例えば "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' より小さい限り、値の末尾に追加されます。文字列 1 が max より長い場合も、文字列 1 が返されます。'接頭辞, 文字列' のペアはいくつでも渡すことができます。

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文字列中の、start番目からend番目までの文字列を返します。文字列の最初の文字は0番目の文字です。もし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 を返します。val にはリストも指定可能で、リストで指定した場合にはその中のものをひとつずつ処理します。値がリストの場合、separator の値として区切り記号を指定する必要があります。separator が指定されていない場合、val はリストではなくひとつの値として扱われます。

swap_around_comma(val)

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[ソース]

swap_around_comma(val) -- "B, A" の様な値を与えた場合、"A B"を返します。これは名前の形式を LN, FN から FN LNに変換する場合には便利です。もしカンマを含まない場合、valをそのまま返します。

to_hex(val)

class calibre.utils.formatter_functions.BuiltinToHex[ソース]

to_hex(val) -- 文字列を16進にエンコードして返します。calibre URLを生成する際に有用です

transliterate(a)

class calibre.utils.formatter_functions.BuiltinTransliterate[ソース]

transliterate(a) -- 元の文字列を、概ねその発音に基づいてラテン文字に変換し返します。例えば、変換元が "Фёдор Миха́йлович Достоевский" の場合は、"Fiodor Mikhailovich Dostoievskii" を返します。

Template database functions

book_count(query, use_vl)

class calibre.utils.formatter_functions.BuiltinBookCount[ソース]

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.

book_values(column, query, sep, use_vl)

class calibre.utils.formatter_functions.BuiltinBookValues[ソース]

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.

extra_file_modtime(file_name, format_string)

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[ソース]

extra_file_modtime(file_name, format_string) -- returns the modification time of the extra file 'file_name' in the book's 'data/' folder if it exists, otherwise -1.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.

extra_file_names(sep [, pattern])

class calibre.utils.formatter_functions.BuiltinExtraFileNames[ソース]

extra_file_names(sep [, pattern]) -- returns a sep-separated list of extra files in the book's 'data/' folder. If the optional parameter 'pattern', a regular expression, is supplied then the list is filtered to files that match pattern. The pattern match is case insensitive. This function can be used only in the GUI.

extra_file_size(file_name)

class calibre.utils.formatter_functions.BuiltinExtraFileSize[ソース]

extra_file_size(file_name) -- returns the size in bytes of the extra file 'file_name' in the book's 'data/' folder if it exists, otherwise -1.This function can be used only in the GUI.

get_link(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinGetLink[ソース]

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

get_note(field_name, field_value, plain_text)

class calibre.utils.formatter_functions.BuiltinGetNote[ソース]

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

has_extra_files([pattern])

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[ソース]

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.

has_note(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinHasNote[ソース]

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.

other

set_globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinSetGlobals[ソース]

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.

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

© Copyright 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!" という文字列 だけ です。つまり、"Hello, wOrld!" や "hello, world!" など他のバリエーションにはマッチしません。

そんなに悪くありません。次は？

次は、本当に使えるもののさわりの部分です。正規表現が複数の文字列にマッチする可能性があると説明したのを覚えていますか？ これが少し複雑になっていくところです。たとえば、もっと実用的な例として、変換したい電子書籍のフッタ部分に "Page 5 of 423" のように厄介なページカウントがあったとします。明らかにページ数は 1 から 423 まで順に増えていきますから、423 通りの異なる文字列にマッチさせる必要があります。そうでしょう？ しかし、実は必要ないのです。正規表現を使えば、マッチする文字のセットを定義することが可能です。セットを定義するには、マッチさせたいすべての文字を角カッコの中に羅列します。したがってたとえば [abc] というセットは "a", "b" または "c" という文字にマッチします。セットは、常にセット内の文字のひとつとだけマッチします。セットは文字の範囲を "理解" します。つまり、すべての小文字とマッチさせたい場合には [a-z] を使用し、小文字と大文字にマッチさせたい場合には [a-zA-Z] を使用するといった具合です。理解できましたか？ したがって Page [0-9] of 423 という表現を使用すれば、言うまでもなく最初の 9 ページにマッチさせることができます。こうして必要な表現を 3 個に絞ることができます。2 個目の表現は Page [0-9][0-9] of 423 で、すべての 2 桁のページにマッチします。3 個目の表現をどうすればよいかは、もうおわかりですね。はい、どうぞ。書いてみてください。

すごい！ 意味がわかってきました！

そう言ってくれると思っていました。でも気を引き締めてください。まだまだよくなって行きます。セットを使用すると、いくつかの文字を一度にマッチさせることができることがわかりました。しかし文字やセットを繰り返すことも可能なのです。そうすることで、上の例でページ番号を扱う正規表現をひとつに減らすことができます。そうです、たったの 1 個です。わくわくしますか？ きっとするでしょう。それは次のように機能します: 特殊文字と呼ばれるもの一部、"+", "?" および "*" は それに続くひとつの要素を繰り返します 。(要素とは、ひとつも文字、ひとつの文字セット、エスケープシーケンス、またはグループのことです。最後の 2 つについては後で説明します。要するに、正規表現の中にある任意のひとつのエンティティです) このような文字は、ワイルドカードまたは量指定子と呼ばれます。具体的に言うと、"?" は後ろに続く要素が 0 または 1 個のものにマッチし、"*" は後ろに続く要素が 0 個以上 のものにマッチし、"+" は後ろに続く要素が 1 個以上 のものにマッチします。いくつか例を示しましょう。正規表現 a? は "" (つまり空の文字列で、このケースでは特に便利ではありません) または "a" にマッチし、a* は "", "a", "aa", および a が連続して並んでいる限り何個でもマッチし、正規表現 a+ は "a", "aa", および a が連続して並んでいる限り何個でもマッチします (ただし空の文字列にはマッチしません)。セットについても同じことが言えます。正規表現 [0-9]+ は すべての整数 にマッチします。さてここで、読者が何を考えているか筆者にはわかります。そしてそれは間違っていません。もしこれを上記のケースでページ番号のマッチに使用したら、ひとつの正規表現だけですべてのページにマッチするのではないか？ そのとおり、Page [0-9]+ of 423 は本の中のすべてのページにマッチするでしょう。

注釈

量指定子に関する注意点: 量指定子は通常、可能な限り多くのテキストとマッチさせようとします。ですから使用する場合には注意してください。これは "貪欲な動作" と呼ばれます。もちろん理由はおわかりですね。これは、たとえばタグをマッチさせようとする場合などに問題が起きます。たとえば、文字列 "<p class="calibre2">Title here</p>" から開きタグ (山カッコで囲まれた最初のパーツ、タグについては後述します) にマッチさせたいとしましょう。正規表現 <p.*> がそのタグにマッチすると思うかもしれませんが、実際には文字列全体にマッチしてしまいます。(文字 "." もまた特殊文字です。これは改行 以外 のすべての文字にマッチします。したがって、基本的には正規表現 .* はどのような内容でも 1 行すべてにマッチします) 代わりに <p.*?> を試してみましょう。これは量指定子 "*" を貪欲ではなくします。この正規表現は、意図したとりに最初のタグにだけマッチします。実はこれにはもうひとつ別のやり方があります。正規表現 <p[^>]*> も同様に最初の開きタグにマッチします。理由は次の章以降にわかるでしょう。ここでは、しばしば正規表現の書き方はひとつではないということを心に留めておいてください。

特殊文字ってすごいけど、ドットやクエスチョンマークにマッチさせたかったらどうすればいい？

もちろんできます。特殊文字の前にバックスラッシュをつければ、特別な意味のない文字リテラルとして解釈されます。特殊文字の前にバックスラッシュをつけた組み合わせのことを、エスケープシーケンスと呼び、特殊文字の前にバックスラッシュをつけることを文字をエスケープすると言います。エスケープシーケンスはひとつの要素と解釈されます。もちろんただ単に特殊文字をエスケープするだけでないエスケープシーケンスも存在します。たとえば "\t" はタブ文字を表します。エスケープシーケンスについては、また後で説明します。ちなみに、この特殊文字についてですが、この入門書の中で何か機能があると紹介する文字はどれも特殊文字であり、したがって文字リテラルとしたいときにはエスケープする必要があると考えてください。

一番便利なセットは何ですか？

聞かれると思ってました。便利なセットといえば、1 桁の数字にマッチする [0-9]、小文字 1 個にマッチする [a-z]、大文字 1 個にマッチする [A-Z]、アルファベット 1 文字にマッチする [a-zA-Z]、そして英数字 1 文字にマッチする [a-zA-Z0-9] などがあります。略記法としてエスケープシーケンスを使用することもできます:

\d

[0-9] と同等

\w

[a-zA-Z0-9_] と同等

\s

空白と同等

注釈

"空白" とは印刷されないものすべてを表す用語です。空白にはスペース、タブ文字、改行、改ページ、行頭復帰、ノーブレークスペースなどが含まれます。

注釈

検索で大文字と小文字を区別しない設定が有効になっていると、大文字と小文字のセットは大文字と小文字の両方にマッチします。そのような設定は、たとえば calibre 自身の Preferences->Searching や calibre 電子書籍ビューア の検索パネル、そして calibre 本の編集 ツールにあります。

セットに関する最後の注意点として、セットはある文字を 除く すべての文字と定義することが可能です。そのためには、文字 "^" をセットの 一番最初の文字として 指定します。したがって [^a] は a 以外のどの文字ともマッチします。これをセットの補完と呼びます。前述したエスケープシーケンスでの略記法にも補完できます。"\D" は数字以外のどの文字にもマッチし、[^0-9] と同等です。その他の略記法も、ご想像のとおり、それぞれ小文字の代わりに大文字を使うと補完になります。では、前の章で例として挙げた <p[^>]*> に戻ってみると、ここで使われている文字セットは山カッコを除く文字すべてにマッチさせようとしていることがわかります。

マッチさせたい文字列がいろいろあると、複雑になりますか？

心配いりません。まだまだ人生は楽勝です。次の例を考えてみましょう: 変換しようとしている本に、奇数ページごとに "タイトル" が、偶数ページごとに "著者名" が書かれているとします。印刷では見栄えがしますよね？ しかし電子書籍では邪魔なだけです。表現全体を普通のカッコで囲むとグループ化できます。そして文字 "|" を使うと右側の表現 または 左側の表現の いずれか にマッチします。これをつなげれば、おしまいです。速すぎましたか？ いいでしょう、まず最初に、奇数ページ用と偶数ページ用に表現をグループ化します。そうすると必要となる 2 つの表現として (タイトル)(著者名) が得られます。さて、これを縦線 ("|" は縦線と呼ばれます) を使って簡単にします。正規表現 (タイトル|著者名) を使うと、"タイトル" (奇数ページ) または "著者名" (偶数ページ) のいずれかにマッチするようになります。ほら、簡単でしょう？

もちろん縦線をグループ化するカッコなしで使用することも可能です。ところで量指定子はその前の要素を繰り返すと説明したときのことを覚えていますか？ 縦線の作用は少し異なります。正規表現 "タイトル|著者名" は、上でグループを使った例とまったく同じように、文字列 "タイトル" または文字列 "著者名" のいずれかに一致します。縦線は、その前の表現全部と、その後ろの表現全部のうちから選択します。したがって、もし文字列 "Calibre" と "calibre" にマッチさせたくて、大文字と小文字の "c" だけを選択したいとすると、使用する正規表現は (c|C)alibre としなくてはなりません。こうすると "c" だけが選択されることが保証されます。もし c|Calibre とすると、文字列 "c" または文字列 "Calibre" にマッチするため、望んだ結果となりません。要するに、よくわからないときには縦線を使う場合にはグループ化しましょう。

見落としがあります...

…ちょっと待ってください。最後にひとつ、グループ化でできるすばらしいことがあります。前にマッチしたグループがあれば、そのグループをその正規表現の中で後で利用できるのです。グループには 1 から始まる番号がつけられ、参照したいグループの番号をエスケープすると参照できます。ですから 5 番目のグループを参照するには \5 とします。したがって文字列 "Test Test" に対して ([^ ]+) \1 で検索すると、文字列全体にマッチします。

最初の方で、正規表現に大文字と小文字を区別させない方法があると言いましたよね？

はい、そのとおりです。傾聴とご指摘をありがとうございます。フラグと呼ばれるものを使用すると、calibre にある物事をどのように処理して欲しいのか伝えることができます。フラグを正規表現に含めるには、特殊なコンストラクト (?ここにフラグ) を使用し、"ここにフラグ" の部分を指定したいフラグで置き換えます。大文字と小文字を区別しないようにするフラグは i なので、正規表現の中に (?i) を含めます。ですから (?i)test` は "Test", "tEst", "TEst" など思いつく限りすべてのバリエーションとマッチします。

もうひとつの便利なフラグ s を使用すると、ドットを改行を 含めて すべての文字にマッチさせることができます。正規表現の中で使用したいフラグが複数ある場合には、同じ記述の中に含めてかまいません。(?is) とすると大文字と小文字を区別せず、ドットがすべての文字にマッチします。どちらのフラグを先に記述しても同じです。(?si) は上のものと同等です。

だんだん正規表現がわかってきた気がします。calibre ではどう使いますか？

変換

変換の設定から始めましょう。非常によくできています。検索と置換 の部分に、変換中に置き換えたい文字列を記述する regexp (正規表現の略称) を入力できます。よくできている部分はウィザードです。ウィザードの杖をクリックすると、変換処理中に calibre に "見える" もののプレビューが表示されます。削除したい文字列のところまで下へスクロールして、選択してコピーし、ウィンドウの上部にある正規表現フィールドに貼り付けます。もしページ番号のような可変な部分があればセットと量指定子を使用して保護し、ついでに特殊文字があれば忘れずにエスケープします。テスト とラベルのついたボタンを押すと、calibre は正規表現で置換する部分をハイライト表示します。満足できたら OK を押して変換します。もし変換ソースに次の例のようなタグがある場合には、注意してください:

Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that. </p>
<p class="calibre4"> <b class="calibre2">Generated by ABC Amber LIT Conv
<a href="http://www.processtext.com/abclit.html" class="calibre3">erter,
http://www.processtext.com/abclit.html</a></b></p>
<p class="calibre4"> It had only been two years since Addison v. Clark.
The court case gave us a revised version of what life was

(図々しくも このスレッド [https://www.mobileread.com/forums/showthread.php?t=75594"] からパクってきました) 一部のタグも削除する必要があります。この例では、タグ <b class="calibre2"> で開始することをお勧めします。次に、対応する終了タグ (開始タグ <tag> で、終了タグは </tag>) で終了する必要があります。このケースでは単純に次の </b> です。(これに関して不明な点があれば、優れた HTML マニュアルを参照するか、フォーラムで質問してください。) 開始タグは <b.*?> と表すことができ、終了タグは </b> で表せます。したがって <b.*?>.*?</b> を使用すると、このタグの間にあるものすべてを削除できます。しかしこの正規表現の使用はお勧めできません。なぜなら <b> タグで囲まれたもの (これは囲まれたテキストを太字にします) すべてを削除してしまうため、まず間違いなく本の一部が消えてしまうでしょう。代わりに囲まれた文字列の先頭部分も含めて、正規表現を <b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b> とします。見えているとおりにスペースを含める代わりに、どのような文字列の組み合わせになっていてもキャッチできるよう量指定子つきの \s が使われています。新しい正規表現をテストするときには、削除したくないものが削除されることのないよう、calibre が何を削除するかを忘れずに確認してください。1 回の出現しか確認しないと、テキストの他の場所での食い違いを見逃す可能性があります。さらに、実際に望んだよりも多いまたは少ないタグを削除してしまうことがありますが、calibre は削除を行った後に破損したコードの修復を試みます。

本の追加

正規表現を利用できるもうひとつのものは、ファイル名からの書誌の抽出です。この機能は、設定の "本の追加" にあります。ここには特別な機能があります。書誌フィールドのフィールド名を使用できるのです。たとえば (?P<title>) は calibre が文字列のこの部分を本のタイトルとして使用することを示します。使用可能なフィールド名はウィンドウの下部に、使いやすいテストフィールドとともに一覧表示されています。例を挙げましょう。たとえば Classical Texts: The Divine Comedy by Dante Alighieri.mobi (言うまでもなくこの本はすでにライブラリの中にあります。何しろイタリアの詩の古典はみんな大好きですから) や Science Fiction epics: The Foundation Trilogy by Isaac Asimov.epub のような名前の大量のファイルをインポートしたいとします。この命名方法からは、明らかに calibre は意味のあるデータを抽出できません。書誌を抽出する標準の正規表現は (?P<title>.+) - (?P<author>[^_]+) ですから。このケースで機能する正規表現は [a-zA-Z]+: (?P<title>.+) by (?P<author>.+) です。ただし書誌フィールドのグループの内側では、そのフィールドが実際に何にマッチしているのかを表す表現を使う必要があります。そしてまた、calibre の提供するテストフィールドを使用する場合には、テストするファイル名のファイル拡張子を追加する必要があります。そうでないときちんと動く正規表現を使っていても、何もマッチが得られなくなります。

書誌を一括編集

最後の部分は、書誌フィールドで使われる正規表現 検索と置換 です。ライブラリで複数の本を選択してまとめて書誌編集を行うと、これにアクセスできます。この最後の機能を使用するにあたっては、特に十分に注意してください。非常におそろしいこと がライブラリに起きる可能性があります。テストフィールドを使って正規表現が思ったとおりに動作することを二重に確認し、本当に変更したい本だけを選択してください。正規表現検索モードでは、一つのフィールドで検索し、テキストを何かと置換し、さらにはその結果を別のフィールドに書き込むことさえ可能です。実用的な例を挙げてみましょう: たとえばライブラリに Frank Herbert の Dune シリーズがあり、Dune 1 - Dune, Dune 2 - Dune Messiah のような形で名前がついているとします。さてここで Dune をシリーズ名として取得したいとします。これを行うには、タイトルフィールドを (.*?) \d+ - .* で検索し、シリーズフィールドで \1 に置き換えます。何をしたかわかりますか？ これはシリーズフィールドの置換に使われる、1 番目のグループへの参照です。これでシリーズをすべて設定できたので、タイトルフィールドでまた別の検索 .*? - を行い、"" (空の文字列) で置換し、再びそれをタイトルフィールドにすると、書誌がすっかりきれいになります。すごくありませんか？ ところで、フィールド全体を置換する代わりに、フィールドの前または後ろに追加することも可能です。ですからもし本のタイトルの先頭にシリーズ情報を つけておきたい のであれば、そうすることも可能です。そして間違いなくすでにお気づきでしょうが、大文字と小文字を区別 とラベルのついたチェックボックスがあります。ですからここでは動作を選択するためのフラグを使用する必要がありません。

さて、これで正規表現のごく簡単な紹介は終わりです。願わくば紹介した内容が、少なくとも自分で学び続けることができるに足るものでありますように。 Python documentation for regexps [https://docs.python.org/library/re.html] から始めるとよいでしょう。

ただし最後にひとつだけ警告しておきます。正規表現は強力ですが、同時にまた非常に簡単に間違いを引き起こすものでもあります。calibre は正規表現が期待どおりに動くかどうか確認するために、すばらしいテストツールを用意しています。それを利用してください。自分の足を撃ち抜くような真似はしないでください。(この表現、すてきでしょう…) けれどももし、警告があったにもかかわらず足 (または身体のどこか他の部分) にケガを負ってしまった場合には、そこから学ぶよう努めてください。

クイックリファレンス

- 正規表現の構文のクイックリファレンス

- 文字クラス
- 文字クラスの略記法
- 量指定子
- 貪欲
- 選択子
- 除外
- アンカー
- グループ
- 前後読み
- 再帰
- 特殊文字
- メタ文字
- モード

謝辞

記述や誤字修正などの助力に感謝します。

- ldolse

- kovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

正規表現についてさらに詳しく学ぶには、 The Python User Manual [https://docs.python.org/library/re.html] を参照してください。calibre で使用している正規表現ライブラリは `regex <https://bitbucket.org/mrabarnett/mrab-regex/src/hg/>`_です。これは Python 標準のライブラリに比較するといくつか便利な拡張機能を備えています。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »

- calibre で正規表現を利用するにあたって重要なこと »

- 正規表現の構文のクイックリファレンス

正規表現の構文のクイックリファレンス

このチェックリストには、calibre のほとんどのパーツで利用可能な正規表現エンジンのうち、最も一般的に利用される/覚えにくいものをまとめてあります。

目次

- 文字クラス

- 文字クラスの略記法

- 量指定子

- 貪欲

- 選択子

- 除外

- アンカー

- グループ

- 前後読み

- 再帰

- 特殊文字

- メタ文字

- モード

文字クラス

文字クラスは異なる文字グループを簡潔に表現するのに役立ちます。

例:

表現

クラス

[a-z]

小文字。アクセント記号や合字を含むことはできません

[a-z0-9]

小文字の英字 a から z もしくは数字 0 から 9

[A-Za-z-]

大文字、小文字、ダッシュ。クラスにダッシュを含めるには、文字の範囲を指定するハイフンと混合させないように、先頭または後尾でダッシュを入れる必要があります。

[^0-9]

10進数字を除いた任意の文字。クラスの先頭に (^) を置くと、そのクラス (補完クラス) の文字を除外します

[[a-z]--[aeiouy]]

小文字の子音。クラスはクラスに含めることが可能です。-- は、それに続くものを除外します

[\w--[\d_]]

すべての文字 (外国のアクセント文字を含む)。クラス内で文字クラスの略記法を使用できます。

例:

<[^<>]+> to select an HTML tag

文字クラスの略記法

表現

クラス

\d

10 進数字 (``[0-9]` と同等)

\D

非 10 進数字 ([^0-9] と同等)

\w

単語構成文字 ([a-zA-Z0-9]) アクセント記号や合字を含む

\W

非単語構成文字

\s

スペース、ノンブレーキングスペース、タブ、改行などの空白文字

\S

非空白文字

.

改行を除いた任意の文字。改行文字を含めるには「すべてドット」のチェックボックスを使用するか、正規表現修飾子 (?s) を使用してください。

量指定子

量指定子

量指定子の前に置かれる式の出現回数

?

expression の 0 回または 1 回の出現。{0,1} と同等

+

expression の 1 回以上の出現。{1,} と同等

*

expression の 0 回以上の出現。{0,} と同等

{n}

expression のちょうど n 回の出現

{min,max}

出現の回数が min 以上 max 以下

{min,}

出現の回数が min 以上

{,max}

出現の回数が 0 以上 max 以下

貪欲

デフォルトでは、量指定子を使用したときに、正規表現エンジンは貪欲です。つまり選択範囲を可能な限り広げます。最初のうちは、しばしばびっくりするような結果になります。量指定子の後ろに ? をつけると、怠惰になります。同じ式の中に 2 つを同時に入れることは避けてください。予測できない結果となる可能性があります。

量指定子を入れ子にしないよう気を付けてください。たとえばパターン (a*)* のような使い方をすると、処理時間が指数関数的に増加してしまいます。

選択子

正規表現の中の | 文字は、論理 OR です。これは前の式または後ろの式のいずれかがマッチすればよいという意味になります。

除外

方法 1
pattern_to_exclude(*SKIP)(*FAIL)|pattern_to_select

例:

"Blabla"(*SKIP)(*FAIL)|Blabla

これは Blabla または "Blabla or Blabla" の文字列中の Blabla を選択しますが、"Blabla" では選択しません。

方法 2
pattern_to_exclude\K|(pattern_to_select)

"Blabla"\K|(Blabla)

これは Blabla または "Blabla or Blabla" の文字列中の Blabla を選択しますが、"Blabla" では選択しません。

アンカー

アンカーは、文字ではなく、文字列の論理的な位置にマッチさせる方法です。テキスト処理に最も役立つアンカーは次のものです:

\b

単語の区切り、すなわち空白から非空白への遷移。たとえば \bsurd は the surd にマッチしますが absurd にはマッチしません。

^

行の先頭にマッチ (複数行モードの場合。デフォルトは複数行モード)

$

行の末尾にマッチ (複数行モードの場合。デフォルトは複数行モード)

\K

選択範囲の開始位置をパターン内のその位置にリセットします。一部の正規表現エンジンでは (ただし calibre は違います) 特に量指定子を指定した場合に、可変長の後読みが許されていません。このようなエンジンで \K が利用できる場合には、可変長の肯定後読みと同等のものを書くことによってこの制限を取り除けます。

グループ

(expression)

グループをキャプチャします。グループには選択範囲が格納され、後で 検索 や 置換 のパターンとして \n で参照できます。n はキャプチャしたグループの連番 (1 から順に読込み順) です

(?:expression)

選択範囲をキャプチャしないグループ化

(?>expression)

アトミックグループ: expression が満足したら即座に正規表現エンジンが処理を抜け、もし残りのパターンが失敗した場合には、その expression の他の組み合わせを試すためのバックトラッキングを行いません。

(?|expression)

ブランチリセットグループ: expression に含まれる代替のブランチが同じグループ番号を共有します

(?<name>expression)

“name” という名前のグループ。選択範囲は後で 検索 パターンからは (?P=name) として、 置換 では \g<name> として呼び出すことができます。2 つの別のグループが同じ名前を使用できます。

前後読み

前後読み

意味

?=

肯定先読み (選択部分の前の位置)

?!

否定先読み (選択部分の後ろの位置)

?<=

肯定後読み (選択部分の後ろの位置)

?<!

否定後読み (選択部分の前の位置)

先読みと後読みは文字を消費せず、長さゼロでキャプチャされません。どちらもアトミックグループです。条件が満たされると即座に正規表現エンジンは処理を抜け、もし残りのパターンが失敗した場合には、その他の組み合わせを試すためのバックトラッキングを行いません。

文字列の中で複数のマッチを検索するとき、それぞれのマッチを試行する最初の位置で、後読みは現在の位置の前にある文字を検査できます。したがって 123 という文字列で、(?<=\d)\d というパターン (前に数字がついている数字) は、理論的には 2 と 3 を選択するはずです。一方、\d\K\d は 2 しか選択できません。なぜなら最初の選択の後の開始位置は 3 の直前であり、2 番目のマッチに十分な長さの数字がないためです。同様に、\d(\d) は 2 しかキャプチャしません。calibre の正規表現エンジンのやり方では、肯定後読みも同じように動作し、理論に反して 2 しか選択しません。

前後読みの内側にグループを配置することも可能ですが、キャプチャが役に立つことは滅多にありません。それでももし有用である場合には、後読みの中での量指定子の使用にはとても慎重になるべきです。バックトラックがないことに関連付けられた貪欲さは、びっくりするようなキャプチャをもたらすことがあります。このような理由から、肯定後読みのキャプチャグループの中に量指定子がある場合には、肯定後読みではなく \K を使用してください。

否定先読みの例:

(?![^<>{}]*[>}])

パターンの最後に配置すると、ファイルに埋めこまれたタグやスタイルの中で選択できなくなります。

可能な限り、結果を得るのに必要なステップ数を減らすために、後読みを「アンカー」する方がよいでしょう。

再帰

表現

意味

(?R)

パターン全体の再帰

(?1)

番号で指定されたキャプチャグループのパターンのみの再帰。ここではグループ 1

再帰とは自分自身を呼び出すことです。これは均衡のあるクエリで役に立ちます。たとえば引用符で囲まれた文字列を埋めこむことのできる引用符つきの文字列などです。このように、二重引用符に囲まれた文字列の処理中に、二重引用符で囲まれた新しい文字列に当たってしまったとき、どうすればよいかもうお分かりでしょうが、自分自身を呼び出すのです。そうするとこのようなパターンになります:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

二重引用符で囲まれた文字列を、その中に埋めこまれた文字列で止まらずに選択するには、次のようにします:

“((?>[^“”]+|(?R))*[^“”]+)”

このテンプレートは、入れ子にできる一組みのタグ、たとえば <div>` タグの変更にも使用できます。

特殊文字

表現

文字

\t

タブ文字

\n

改行

\x20

(改行可能な) 空白文字

\xa0

ノーブレークスペース

メタ文字

メタ文字は、正規表現エンジンにとって特別な意味を持つ文字です。このうち次の 12 個の文字は、特殊文字として使用せずに通常の文字として扱うためには前にエスケープ文字、すなわちバックスラッシュ (\) をつける必要があります。

^ . [ ] $ ( ) * + ? | \

残りの 7 個の特殊文字はバックスラッシュを前に付ける必要はありません (ただし、つけないと結果が変わる可能性があります)

{ } ! < > = :

特殊文字はクラス内 (角カッコ [] の内側) では特殊文字ではなくなります。閉じカッコとダッシュはクラス内で特殊文字となります。クラスの外側では、ダッシュは単なる文字となりますが、閉じカッコはメタ文字のままです。

スラッシュ (/) および 番号記号 (またはハッシュ文字) (#) はメタ文字ではないため、エスケープする必要はありません。

Python エンジンを搭載した regex101.com のような一部のツールでは、二重引用符は特別な意味を持つ区切り文字のため、エスケープするかオプションを変更する必要があります。calibre のエディタでは、その必要はありません。

モード

(?s)

ドット (.) に改行文字もマッチさせます

(?m)

^ および $ アンカーを文字列全体の先頭と末尾ではなく、行の先頭と末尾にマッチさせます。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- プラグインを書いて calibre を機能拡張

プラグインを書いて calibre を機能拡張
calibre の設計は非常にモジュラーです。ほとんどすべての calibre の機能がプラグインという形で提供されます。プラグインは変換、ニュースのダウンロード (ただしこれはレシピと呼ばれます)、ユーザインタフェースのさまざまな部品、いろいろなデバイスへの接続、calibre にファイルを追加する際の処理など、多岐にわたって使用されます。ビルトインのプラグインは、環境設定 → 高度な設定 → プラグイン ですべてを確認できます。

ここでは独自のプラグインを作成して calibre に新しい機能を追加する方法を説明します。

目次

- calibre プラグインの構造

- ユーザインタフェースプラグイン

- __init__.py

- ui.py

- main.py

- プラグイン ZIP ファイルからリソースを取得

- プラグインのユーザ設定を有効にする

- 本の編集プラグイン

- main.py

- プラグインに翻訳を追加

- プラグイン API

- デバッグプラグイン

- プラグインのその他の例

- プラグインの共有

注釈

これが適用されるのは calibre のリリース 0.8.60 以降に限られます。

calibre プラグインの構造
calibre のプラグインはとてもシンプルです。Python のコードと、プラグインが必要とする画像ファイルなどのその他リソースからなる単なる ZIP ファイルにすぎません。これ以上面倒なことは避けて、基本的な例を見てみましょう。

EPUB および MOBI 形式のさまざまな電子ドキュメントを自己出版するために calibre をインストールしているとしましょう。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

これでおしまいです。あとは __init__.py を作成したフォルダで次のコードを実行するだけで、このコードを calibre にプラグインとして追加できます:

calibre-customize -b .

注釈
macOS では、コマンドラインツールは calibre にバンドルされています。たとえばcalibre を /Applications にインストールした場合には、コマンドラインツールは /Applications/calibre.app/Contents/MacOS/ にあります。

Hello World プラグインは helloworld_plugin.zip [https://calibre-ebook.com/downloads/helloworld_plugin.zip] からダウンロード可能です。

calibre で本を変換するたびにプラグインの run() メソッドが呼び出され、変換した本の発行元は "Hello World" に設定されます。これは非常に簡単なプラグインですが、ユーザインタフェースに実際にコンポーネントを追加するようなもっと複雑な例に移りましょう。

ユーザインタフェースプラグイン

このプラグインはいくつかのファイルに分散されています (コードをきれいに保つため)。ここでプラグイン ZIP ファイルからリソース (画像やデータファイル) を取得し、ユーザにプラグインの設定を許可し、calibre のユーザインタフェースに要素を作成し、calibre のデータベースにアクセスしてクエリを発行する方法を紹介します。

このプラグインは interface_demo_plugin.zip [https://calibre-ebook.com/downloads/interface_demo_plugin.zip] からダウンロード可能です。

最初に注意すべきは、この ZIP ファイルには以下に説明するとおりもっと多くのファイルが含まれているということです。特に plugin-import-name-interface_demo.txt には注意してください。

plugin-import-name-interface_demo.txt

マルチファイルプラグインマジックを有効にするために使われる空のテキストファイル。このファイルは複数の .py ファイルを使うプラグインには必ず必要です。中身は空でなくてはならず、ファイル名は plugin-import-name-**some_name**.txt の形式にする必要があります。このファイルが存在すると ZIP ファイルの中にある .py ファイルからコードをインポートできるようになります。それには次のように記述します:

from calibre_plugins.some_name.some_module import some_object

必ずプリフィックス calibre_plugins をつける必要があります。some_name は空のテキストファイルのファイル名から取得します。some_name は ZIP ファイル内の some_module.py を参照します。このインポートは Python の普通のインポートと同じくらい強力です。通常と同じように (各サボフォルダに __init__.py を定義することにより) ZIP ファイルの中に .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 ライブラリをロードすべきではありません。actual_plugin が calibre に実際のプラグインが ZIP アーカイブ内の別のファイルに見つかったことを知らせて、GUI コンテキストでのみロードされるようになります。

これを機能させるには、上で説明したようにプラグイン ZIP ファイルの中に plugin-import-name-some_name.txt が必要です。

プラグインのユーザ設定を有効にする方法は、いくつか存在します。それについては下で説明します。

ui.py

では、実際の GUI プラグインを定義している ui.py を見てみましょう。ソースコードにはコメントがちりばめられているので、読めばわかるはずです:

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

Interface Plugin Demo ダイアログを実装する実際のロジック部分
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)

This function should be called with a list of paths to files inside the
ZIP file. For example to access the file icon.png in the folder images in the ZIP file, you would use: images/icon.png. Always use a forward slash as the path separator, even on Windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name was not found in the ZIP file. If you pass in more than one name then it returns a dictionary mapping the names to bytes. If a name is not found, it will not be present in the returned dictionary.

get_icons(name_or_list_of_names, plugin_name='')

A wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. If a name is not found in the ZIP file the corresponding QIcon will be null. In order to support icon theme-ing, pass in the human friendly name of your plugin as plugin_name. If the user is using an icon theme with icons for your plugin, they will be loaded preferentially.

プラグインのユーザ設定を有効にする

ユーザがプラグインを設定できるようにするためには、プラグインの基底クラスに 3 つのメソッド、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

prefs オブジェクトが main.py で使われているのがわかります:

self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

本の編集プラグイン

それでは少し趣向を変えて、calibre の書籍エディタにツールを追加するプラグインを作成する方法を見てみましょう。プラグインはここちら: editor_demo_plugin.zip [https://calibre-ebook.com/downloads/editor_demo_plugin.zip]

すべてのプラグインと同様、最初のステップは上記 above で説明したようにインポート名の空のテキストファイルを作成することです。ここではファイルを 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 を掘り下げてみましょう。Magnify fonts と名付けたひとつのツールを定義していることがわかります。このツールはユーザに数値を尋ねて、与えられた数値を本の中のすべてのフォントサイズに掛けます。

最初に重要なのはツール名です。このツールのキーとして使用されるため、なるべくユニークな文字列を設定する必要があります。

次に重要なエントリポイントは calibre.gui2.tweak_book.plugin.Tool.create_action() です。このメソッドはプラグインツールバーとプラグインメニューに表示される QAction を生成します。オプションで、ユーザがカスタマイズ可能なショートカットを割り当てることができます。トリガーとなる QAction からのシグナルは ask_user() に接続してユーザにフォントサイズを何倍にするかをを尋ね、その後に拡大するコードを実行します。

拡大するコードにはしっかりコメントが書き込まれていて、まあまあシンプルです。注意すべき主な点は、エディタウィンドウを self.gui、エディタ Boss を self.boss として参照することです。Boss はエディタユーザインタフェースを制御するオブジェクトです。これには便利なメソッドがたくさんありますが、それについては calibre.gui2.tweak_book.boss.Boss クラスに説明があります。

最後に self.current_container があり、これは編集されようとしている本を calibre.ebooks.oeb.polish.container.Container オブジェクトへの参照です。これは本をその構成要素であるHTML/CSS/画像 ファイルの集まりとして表し、役に立ついろいろなことができる便利なメソッドを備えています。プラグインコードで再利用可能なコンテナオブジェクトとさまざまな便利なユーティリティ関数については、電子書籍編集ツール API 説明書 に説明があります。

プラグインに翻訳を追加

プラグインのユーザインタフェース上のすべての文字列は翻訳して、メインの 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 フォルダに置きます。

最後のステップは、ただ単にプラグインの .pu ファイルの先頭で関数 load_translations() を呼び出すだけです。性能上の理由からこの関数を呼び出すのは、実際に翻訳可能な文字列が含まれている .py ファイルに限るべきです。したがって典型的なユーザインタフェースプラグインではこの関数は __init__.py ではなく ui.py の先頭で呼び出します。

You can test the translations of your plugins by changing the user interface language in calibre under Preferences → Interface → Look & feel or by running calibre with the CALIBRE_OVERRIDE_LANG environment variable set. For example:

CALIBRE_OVERRIDE_LANG=de

de の部分はテスとしたい言語の言語コードに置き換えてください。

For translations with plurals, use the ngettext() function instead of _(). For example:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

プラグイン API

すでにお気づきかもしれませんが、calibre のプラグインとはクラスです。calibre プラグインの種類ごとに異なるクラスがあります。すｓべてのプラグインの基底クラスのを含め、それぞれのクラスについての詳細は プラグインの API 説明書 に記載されています。

プラグインはほぼ確実に calibre のコードを使用します。calibre のコードベースの機能についてすみからすみまで学ぶには、calibre の:ref:code_layout の章をお読みください。

デバッグプラグイン

最初の、そして最も重要なステップは calibre をデバッグモードで実行することです。これはコマンドラインから次のようにして実行できます:

calibre-debug -g

または calibre の中で 環境設定 ボタンを右クリックするか、キーボードショートカットの Ctrl+Shift+R を使用します。

コマンドラインから実行すると、デバッグ出力はコンソール上に表示されます。calibre の中から実行するとテキストファイルに保存されます。

プラグインコードのどこにでも print 文を挿入できます。これはデバッグモードで出力されます。これは Python ですから、デバッグには print 文以上のものが必要になることはありません。calibre はこのデバッグ手法だけを用いて開発されました。

プラグインへの変更は次のコマンドラインを使ってすぐにテストできます:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

これは calibre をシャットダウンし、シャットダウンの完了を待ち、calibre のプラグインを更新し、calibre を再起動します。

プラグインのその他の例

多くの洗練された calibre プラグインの一覧は こちら [https://www.mobileread.com/forums/showthread.php?t=118764] にあります。

プラグインの共有

作成したプラグインを他の calibre ユーザと共有したければ、 calibre プラグインフォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=237] に新しいスレッドとして投稿してください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- 電子書籍での数式の組版

電子書籍での数式の組版
calibre の電子書籍ビューアは、電子書籍 (EPUB および HTML ファイル) に埋め込まれた数式を表示する機能を備えています。TeX や MathML や AsciiMath を使って数式を直接組むことができます。calibre の電子書籍ビューアは数式を表示するのに優れた MathJax <https://www.mathjax.org> ライブラリを使用しています。これは、数式を含む電子書籍を calibre 電子書籍ビューアでうまく動作するよう作成するための簡単なチュートリアルです。

数式を含む単純な HTML ファイル

数式をインラインで簡単な HTML ファイルの中に書くと、calibre の電子書籍ビューアはそれを適切に組版された数式としてレンダリングします。以下の例では、数学に TeX 表記を使用しています。通常の TeX コマンドを使用できますが、アンパサンド、小なり記号、大なり記号はそれぞれ &amp; &lt; &gt; として記述する必要があるので注意してください。

最初のステップは、calibre に、数式が含まれていると伝えることです。これを行うには、次のコードスニペットを HTML ファイルの <head> セクションに追加してください:

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

詳しい情報
calibre の電子書籍ビューアは MathJax ライブラリを使用して数式をレンダリングします。したがって電子書籍での数式についてもっと知りたい場合は `MathJax ウェブサイト<https://www.mathjax.org>`_ が最も役に立つでしょう。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- AZW3 • EPUB • MOBI のカタログを作成

AZW3 • EPUB • MOBI のカタログを作成
calibre のカタログ作成機能を使えば、ライブラリのカタログをさまざまな形式で作成できます。このヘルプでは、カタログを AZW3, EPUB および MOBI 形式で作成するときのカタログオプションについて説明します。

- カタログ化する本を選択

- 含まれるセクション

- プレフィックス

- 除外する本

- 除外するジャンル

- その他のオプション

- カスタムなカタログの表紙

- その他のヘルプリソース

カタログ化する本を選択

ライブラリの すべて をカタログ化したい場合、メインウィンドウの中の検索や絞り込み条件はすべて削除してください。ある一冊の本を選択すると、ライブラリ内のすべての本がカタログ化の候補となります。個々の本はさまざまな基準により除外されることがあります。除外するジャンル の章を参照してください。

ライブラリの 一部だけ をカタログ化したい場合、方法は 2 つあります:

- カタログ化したい本を複数選択します。calibre のメインウィンドウで複数の本が選択されている場合、選択した本だけがカタログ化されます。

- 検索フィールドまたはタグブラウザを使って表示する本を絞り込みます。表示されている本だけがカタログ化されます。

カタログの生成を始めるには、メニュー項目の 本を変換 > calibre ライブラリの本からカタログを作成 を選択します。環境設定 > インタフェース > ツールバー & メニュー でツールバーに カタログ作成 ボタンを追加してカタログ作成ダイアログを簡単にアクセスできるようにすることも可能です。

カタログオプション ではカタログ形式として AZW3, EPUB または MOBI を選択します。カタログタイトル フィールドには生成されたカタログで使用する名前を与えます。同じ名前で同じ形式のカタログがすでに存在している場合、新しく生成されたカタログで置き換えられます。

デバイスにカタログを自動的に送信 を有効にすると、生成したカタログを作成完了時に接続されているダウンロードします。

含まれるセクション

チェックマークで有効にされたセクションが生成するカタログに含まれます:

- 著者 - すべての本が、著者名順に並べ替えられてリスト形式で表示されます。シリーズでない本はシリーズの本の前に表示されます。

- タイトル - すべての本が、タイトル順に並べ替えられてリスト形式で表示されます。

- シリーズ - シリーズ化されている本が、シリーズごとに並べ替えられてリスト形式で表示されます。

- ジャンル - リストの中にある個々のジャンルが、著者とシリーズで並べ替えられます。

- 最近追加された本 - すべての本が、新しい順に並べ替えられます。リストには過去 30 日以内に追加された本、次いで月ごとに追加された本が含まれます。

- 説明 - 本ごとの詳細な説明ページ。表紙のサムネイルとコメントを含みます。著者名で並べ替えられ、シリーズでない本はシリーズの本の前に置かれます。

プレフィックス

プレフィックスルールを使用すると、特定の基準が魅されたときに本のリストにプレフィックスを追加することができます。たとえば既読の本にチェックマークをつけたり、ウィッシュリストの本に X をつけたりしたくなることがあるかもしれません。

最初の列のチェックボックスでルールを有効にします。名前 与えられたルール名です。フィールド は タグ またはライブラリのカスタム列です。値 は一致する フィールド の内容です。プレフィックスルールが満たされると、本は選択した プレフィックス でマークされます。

上の例では 3 つのプレフィックスルールが指定されています:

1. Read book (読んだ本) は、Last read (最後に読んだ) と名前のついたカスタム列に何か日付が入っている本にチェックマーク記号のプレフィックスをつけるよう指定しています。

2. Wishlist (ウィッシュリスト) のは、guilabel:Wishlist タグのついた本に X 記号のプレフィックスをつけるよう指定しています。

3. Library books は、カスタム列 Available in Library の値が True (または Yes) の本に二重矢印記号のプリフィックスをつけるよう指定しています。

最初に一致したルールがプレフィックスを提供します。無効に設定されてるルール、または不完全なルールは無視されます。

除外する本

除外ルールを使用すると、カタログ化しない本を指定できます。

最初の列のチェックボックスでルールを有効にします。名前 与えられたルール名です。フィールド は タグ またはライブラリのカスタム列です。値 は一致する フィールド の内容です。除外ルールが満たされると、その本は生成されるカタログから除外されます。

上の例では 2 つの除外ルールが指定されています:

1. Catalogs ルールは、Catalog タグのついた本を生成するカタログから除外するよう指定しています。

2. Archived Books ルールは、カスタム列 Status の値が Archived の本を生成されるカタログから除外するよう指定しています。

すべてのルールはすべの本に対して評価されます。無効に設定されているルール、または不完全なルールは無視されます。

除外するジャンル

カタログが生成されるとき、データベース中のタグがジャンルとして使用されます。たとえばタグ Fiction および Nonfiction を使用できます。こうしたタグは生成されたカタログ内でジャンルとなり、割り当てられたタグに基づいてそれぞれのジャンルのリストに本が載せられます。本は、関連付けられたタグを持つジャンルのセクションすべてに載せられます。

あるタグを別の用途に使用している場合もあるでしょう。たとえば読んだ本を示す + や、[Amazon Freebie] のように本の出典を示すためにタグをカッコで囲んだりするかもしれません。除外するジャンル の正規表現を使うと、生成されるカタログの中でジャンルとして扱いたくないタグを指定することができます。デフォルトの除外正規表現パターン \[.+\]\+ は [タグ]` の形式のタグ、および 既読を表すデフォルトのタグである + タグを、生成されるカタログでジャンルとして使用されるものから除外します。

正規表現の中でタグ名そのものを使用することもできます。たとえば [Amazon Freebie] や [Project Gutenberg] です。もし複数のタグ名を除外の一覧に設定したい場合には、次のようにパイプ (縦線) 文字を間にはさんでください: [Amazon Freebie]|[Project Gutenberg]

正規表現の結果 には、データベース中のタグと入力した正規表現に基づいて、カタログ作成時にどのタグが除外されるかを示します。正規表現を変更すると、結果が更新されます。

その他のオプション

カタログの表紙 には、新しい表紙を生成するのか既存の表紙を使用するのかを指定します。カタログの表紙をカスタムで作成することが可能です。詳しくは カスタムなカタログの表紙 を参照してください。作成したカスタムな表紙を再利用したい場合には、既存の表紙を使用 を選択します。そうでなければ 新しい表紙を生成 を選択してください。

追加の概要 には、表紙のサムネイルの横にある説明ページに挿入されるカスタム列の内容を指定します。たとえば、カスタム列 Last Read を使って最後に本を読んだ日付を表示したい場合があります。さらに高度な追加の概要機能の使い方については calibre フォーラムのこの投稿 [https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395] を参照してください。

サムネイル幅 には、説明ページに含まれる表紙サムネイルの幅を指定します。異なる幅を試すには、気に入った幅がわかるまで数冊の本だけのカタログを生成してみて、それから完全なカタログを生成してください。新しいサムネイル幅で最初にカタログが生成されるときにはパフォーマンスが低下しますが、その後に続けてカタログを作るときにはサムネイルのキャッシュを利用します。

コメントと統合 では、カタログ生成時にコメント書誌と非破壊的に内容を統合するカスタム列を指定します。たとえば、Author bio というカスタム列をコメント書誌に追加したい場合があります。カスタム列の内容を挿入する位置をコメントセクションの 前 または 後 を指定することができます。またオプションで追加した内容を水平ルールセパレータで区切ることもできます。使用できるカスタム列のタイプには テキスト, コメント, および複合 が含まれます。

カスタムなカタログの表紙

Generate Cover plugin [https://www.mobileread.com/forums/showthread.php?t=124219] をインストールすると、カタログのカスタム表紙を作成できます。プラグインをインストールするには、環境設定 > 高度な設定 > プラグイン > 新しいプラグインを取得 へ行きます。

その他のヘルプリソース
calibre のカタログ機能の詳細については、MobileRead フォーラムのスティッキー Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556] を参照してください。そこにカタログテンプレートのカスタマイズ方法とバグ報告の仕方が載っています。

calibre のカタログ機能について他のユーザに質問したり議論するには、MobileRead フォーラム Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236] にアクセスしてください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- チュートリアル »
- 仮想ライブラリ

仮想ライブラリ
calibre では、仮想ライブラリによって通常のライブラリの一部だけを開くよう calibre に指示することができます。たとえば、ある著者の本だけ、あるいはあるタグをつけた本だけを利用したい場合です。仮想ライブラリを利用するのが、多数の本のコレクションの中から一部のコレクションを切り分けて利用するのに好ましい方法です。コレクション全体を検索したい場合などには全ライブラリに戻ればよいだけなので、小さなライブラリに分割してしまうよりも優れています。calibre では複数の別々のライブラリを同時に検索する方法はありません。

仮想ライブラリは、単純な検索とは異なります。検索によってできるのは、本のリスト上に表示する本を制限するだけです。仮想ライブラリでも表示の制限は行いますが、それに加えて左側の タグブラウザ に表示するタグ、著者、シリーズ、出版社などもその仮想ライブラリ内の本のものに限られます。つまり仮想ライブラリは、そのライブラリ内に制限された本だけが存在しているかのように振る舞うのです。

仮想ライブラリの作成

仮想ライブラリを使うには、検索バーの左側にある Virtual library をクリックして 仮想ライブラリを作成 を選択してください。最初の例として、ある特定の作者による本だけを表示する仮想ライブラリを作成してみましょう。下の画像に示すように 著者 リンクをクリックして著者名を選び、OK をクリックしてください。

仮想ライブラリの作成ダイアログに必要事項を入力しました。OK をクリックすれば、新しい仮想ライブラリが作成され、自動的にそれに切り替わり、選択した著者の本だけが表示されているのがわかるでしょう。calibre に関する限り、ライブラリには選択した著者の本だけが含まれているのと変わりありません。

ライブラリ全体にはいつでも戻れます。もう一度 仮想ライブラリ をクリックして <なし> という名前のエントリをクリックしてください。

仮想ライブラリは 検索 に基づいています。どのような検索でも仮想ライブラリの基礎として使用できます。仮想ライブラリには、指定されたその検索に一致する本だけが含まれます。まず、検索バーに使用したい検索を入力するか、タグブラウザ を使って検索条件を構築してください。返された結果に満足したら、仮想ライブラリ ボタンをクリックし、ライブラリを作成 を選択して新しい仮想ライブラリの名前を入力します。そうすることで、入力されている検索条件に基づいた仮想ライブラリが作成されます。検索は非常に強力です。検索でどのようなことができるかの例については、検索インタフェース を参照してください。

便利な仮想ライブラリの例

- 昨日 calibre に追加された本:

date:>1daysago

- 先月 calibre に追加された本:

date:>30daysago

- 星 5 評価の本:

rating:5

- 星 4 評価以上の本:

rating:>=4

- 評価のない本:

rating:false

- calibre の "ニュースを取得" 機能によってダウンロードした定期刊行物:

tags:=News and author:=calibre

- タグのない本:

tags:false

- 表紙のない本:

cover:false

仮想ライブラリの活用

仮想ライブラリ をクリックして動作を選択すれば、以前作成した仮想ライブラリを編集したり削除したりすることができます。

現在のライブラリを開いたときに常にある特定の仮想ライブラリを適用することもできます。それは、環境設定 → インタフェース → 動作 で設定できます。

現在の検索を一時的に仮想ライブラリとして使用するには、 仮想ライブラリ ボタンをクリックして *現在の検索 を選択します。

利用できるすべての仮想ライブラリを本の一覧の上にタブとして表示することもできます。仮想ライブラリ間を頻繁に行き来したい場合には特に便利です。仮想ライブラリ ボタンをクリックして 仮想ライブラリタブを表示 を選択してください。タブをドラッグ&ドロップして位置を入れ替えることができ、必要ないものは閉じることも可能です。閉じてしまったタブはタブバーの上で右クリックすれば元に戻すことができます。

検索における仮想ライブラリの使用

仮想ライブラリ内にある本はプリフィックスに vl: を使って検索することができます。たとえば vl:読む と指定すると 読む という名の仮想ライブラリ内のすべての本を検索します。vl:読む and vl:"サイエンスフィクション" と指定すると 読む および サイエンスフィクション という名前の仮想ライブラリ内のすべての本を検索します。

vl: に続く値は仮想ライブラリの名前でなくてはなりません。仮想ライブラリ名がスペースを含む場合には引用符で囲ってください。

仮想ライブラリの用途のひとつはコンテンツサーバです。環境設定 → ネット共有 → ユーザ名とパスワードを要求 によりユーザに表示される calibre を制限できます。表示されるライブラリごとに検索式を指定して、どの本を表示するかをさらに制限することも可能です。仮想ライブラリの本に制限するには、vl:"仮想ライブラリ名" を使ってください。

追加の制限の使用

仮想ライブラリ内に表示する本をさらに制限するには、追加制限 を使用します。追加制限は、仮想ライブラリに表示される本をさらに制限するために現在の仮想ライブラリに適用するために作っておかれる、保存済み検索です。たとえば 歴史小説 とタグをつけた本からなる仮想ライブラリがあり、未読の本を表示する保存済み検索があるとすると、仮想ライブラリ ボタンをクリックして 追加制限 を選択すれば未読の歴史小説だけを表示することができます。保存済み検索については、検索の保存 を参照してください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- The calibre:// URL スキーム

The calibre:// URL スキーム
calibre は自分自身を calibre:// URL のハンドラプログラムとして登録します。したがって本を開いたり本を検索したりといった動作を他のプログラムやドキュメント、またはコマンドラインから実行することが可能です。たとえばコマンドラインで次のように実行します:

calibre calibre://switch-library/Some_Library

これは Some Library という名前のライブラリを calibre で開きます。ライブラリ名はライブラリフォルダのフォルダ名のスペースをアンダースコアで置き換えたものです。特殊値 _ は現在のライブラリを意味します。URL のさまざまなタイプについては下のドキュメントを参照してください。

このリンクは HTML ファイルは Word ドキュメントなどの中にも入れられ、OS が自動的に calibre を実行して指定された動作を行います。

- 指定したライブラリに切替え

- 指定した本を calibre で開く

- 指定した本を電子書籍ビューアで指定した位置で開く

- 本を検索

- Open a book details window on a book in some library

- Open the notes associated with an author/series/etc.

- URL パラメータの 16 進エンコード

指定したライブラリに切替え

URL の構文は:

calibre://switch-library/Library_Name

ライブラリ名はライブラリのフォルダ名からスペースをアンダースコアで置き換えたものです。特殊値 _ は現在のライブラリを意味します。またライブラリ名には :ref:`16 進数エンコード <hex_encoding>`も使えます。URL エンコードが必要になるような特殊文字が使われているライブラリ名に便利です。16 進数エンコードのライブラリ名は次のようになります:

_hex_-AD23F4BC

_hex_- プリフィックスの後ろの部分が UTF-8 としてエンコードされたライブラリ名で、すべてのバイトは 2 つの16進文字で表されます。

指定した本を calibre で開く

URL の構文は:

calibre://show-book/Library_Name/book_id

これは calibre で book_id (数字) の本を開きます。本の ID は calibre のインタフェース上で 本の詳細 パネルで クリックして開く の上にマウスカーソルを置くと確認できます。

calibre で現在表示している本にリンクをコピーするには、本の詳細 パネルで右クリックして 本のリンクをコピー を選択します。

If a search is active and the book is not matched by the search then the search is cleared.

If a Virtual library is selected, calibre will use it when showing the book. If the book isn't found in that virtual library then the virtual library is cleared.

If you want to switch to a particular Virtual library when showing the book, use:

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

replacing spaces in the Virtual library name by %20. If the book isn't found in that virtual library then the virtual library is ignored.

指定した本を電子書籍ビューアで指定した位置で開く

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

ここでクエリには、有効な 検索式 であれば何でも指定可能です。検索式が複雑な場合は 16 進文字列としてエンコード し、代わりに eq を使用します。クエリを空にすると現在の検索がクリアされます。

デフォルトでは、仮想ライブラリが選択されている場合、すべての本を見つけられるよう検索前に解除します。仮想ライブラリを保持したい場合には、次のようにします:

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 としてコピー を選択します。

Open a book details window on a book in some library

URL の構文は:

calibre://book-details/Library_Name/book_id

This opens a book details window on the specified book from the specified library without changing the current library or the selected book.

Open the notes associated with an author/series/etc.

URL の構文は:

calibre://book-details/Library_Name/Field_Name/id_Item_Id

This opens a window showing the notes of the specified item.
The easiest way to create such URLs is to show the notes you want in calibre and click the Copy URL button to copy the URL to the clipboard and paste it wherever you need.

Here Field_Name is the name of the columns such as authors or tags.
For user created columns, replace the leading # in the field name with an underscore, so #mytags becomes _mytags.

In addition to specifying items by id using Item_Id you can also specify them by name using either val_Item_Name or hex_Hex_Encoded_Item_Name.
For example:

calibre://book-details/Library_Name/authors/val_John%20Doe

URL パラメータの 16 進エンコード

URL パラメータの 16進エンコードは、まずパラメータを UTF-8 バイト列としてエンコードし、各バイトをそのバイトを表す 2 つの 16 進文字で置き換えることによって行われます。たとえば abc という文字列は UTF-8 で 0x61 0x62 and 0x63 というバイト列になり、エンコードされると 616263 という文字列になります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- calibreをカスタマイズ

calibreをカスタマイズ
calibre は高度にモジュラーな設計となっています。パーツのいろいろな部分がカスタマイズ可能です。詳しくはこちら:

- calibre の振る舞いをカスタマイズするために環境変数と Tweaks を使用する方法

- アイコンやテンプレートなど独自の静的リソースでデフォルトを上書きする方法

- calibre に機能追加するために プラグイン を使用する方法

- 他の calibre ユーザとアイコンテーマとプラグインを共有する方法

- レシピ の作成方法と calibre への新しいオンラインコンテンツのソースの追加方法については、お気に入りのニュースサイトを追加 セクションを参照してください。

注釈

アイコンテーマやプラグインは目録が作成されていて calibre 内蔵のアップデータからダウンロード可能ですが、calibre の一部ではありません。サポートが受けられる正しい場所およびソースコードは Mobileread forums <https://www.mobileread.com/forums/forumdisplay.php?f=166> のサポートスレッドです。

- 環境変数

- Tweaks

- アイコンやテンプレートなどを上書き

- calibre 用に独自のアイコンテーマを作成

- calibre をプラグインでカスタマイズ

環境変数

- CALIBRE_CONFIG_DIRECTORY - 設定ファイルを保存/読み込みするフォルダを設定

- CALIBRE_TEMP_DIR - calibre が使用する一時フォルダを設定

- CALIBRE_CACHE_DIRECTORY - calibre がデータをキャッシュするために使用するフォルダを設定

- CALIBRE_OVERRIDE_DATABASE_PATH - metadata.db へのフルパスを指定可。この変数によって metadata.db をライブラリフォルダ以外の場所に置くことができるようになります。ライブラリフォルダがネットワーク上に置かれていてファイルの排他制御機能をサポートしていない場合に有用です。

- CALIBRE_DEVELOP_FROM - calibre の開発環境から実行するために使用。 calibre 開発環境の構築 を参照。

- CALIBRE_OVERRIDE_LANG - インタフェースで使用する言語を矯正 (ISO 639 言語コード)

- CALIBRE_TEST_TRANSLATION - 翻訳 .po ファイルをテストするのに使用 (.po ファイルへのパス)

- CALIBRE_NO_NATIVE_FILEDIALOGS - ファイル/フォルダの選択時に calibre にネイティブのファイルダイアログを使用させない

- CALIBRE_NO_NATIVE_MENUBAR - Ubuntu Uniti や類似の linux デスクトップ環境において calibre にネイティブ (グローバル) メニューを作成させない。メニューは従来どおりウィンドウの内側に配置されます。

- CALIBRE_USE_SYSTEM_THEME - Linux ではデフォルトで calibre はビルトインの Qt スタイルを使用します。この設定は calibre がビルドされた Qt のバージョンとシステムの Qt のバージョンの非互換性によって引き起こされるクラッシュを回避するためのものです。ただしシステムの外観と操作感に calibre が従うとは限りません。Linux 上でこの環境変数を使用する場合、calibre はシステムのテーマを使用するようになります。クラッシュやフリーズに注意してください。

- CALIBRE_SHOW_DEPRECATION_WARNINGS - calibre に非推奨の警告を標準出力に表示させる。calibre の開発者に役立ちます。

- CALIBRE_NO_DEFAULT_PROGRAMS - Windows が扱えるファイルタイプを calibre に自動的に登録させない

- QT_QPA_PLATFORM - Linux では、これを wayland に設定してキャリバーに強制的に Wayland を使用させ、xcb に設定して X11 を強制的に使用させます。

- SYSFS_PATH - sysfs が /sys 以外の場所にマウントされている場合に使用します。

- http_proxy, https_proxy - Linux 上で HTTP(S) のプロキシを指定するために使用

How to set environment variables in Windows [https://www.computerhope.com/issues/ch000549.htm] を参照してください。macOS を使用している場合には ~/Library/Preferences/calibre/macos-env.txt を作成して環境変数を設定することができます。環境変数は 1 行に 1 つずつ記述します。使用例:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Tweaks

Tweak は、calibre のいろいろな動作を細かく制御するための変更です。設定 -> 高度な設定 -> Tweaks から設定ができます。Tweak のデフォルト値を以下に示します

#!/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 をインストールした場所のサブフォルダに保存されます。Windows では、これは通常 C:\Program Files\Calibre2\app\resources です。macOS では、/Applications/calibre.app/Contents/Resources/resources/ です。Linux では、calibre の Web サイトからバイナリインストーラを使用している場合には、/opt/calibre/resources になります。これらのパスは、calibre をどこにインストールしたかによって変わる可能性があります。

このリソースフォルダ内のファイルは変更しないでください。変更しても、calibre をアップデートすると変更内容が上書きされてしまいます。変更したい場合は、環境設定 > 高度な設定 > その他 に移動し、calibre の設定フォルダを開く をクリックします。この設定用フォルダに resources という名前のサブフォルダを作成し、上書きしたいファイルをそのフォルダに配置します。ファイルは適切なサブフォルダに配置してください (たとえば画像は resources/images に配置します)。次回の起動時から自動的に calibre は、組み込みのファイルではなくカスタムファイルを使用するようになります。

For example, if you wanted to change the icon for the Remove books action, you would first look in the built-in resources folder and see that the relevant file is resources/images/remove_books.png. Assuming you have an alternate icon in PNG format called my_remove_books.png you would save it in the configuration folder as resources/images/remove_books.png. All the icons used by the calibre user interface are in resources/images and its sub-folders. Placing an override file here will have even higher priority than a custom icon theme.

calibre 用に独自のアイコンテーマを作成

美しいアイコン セットを作成し、calibre のビルトイン アイコン テーマ サポートを介して他の caliber ユーザーと共有したい場合は、アイコンをテーマに簡単にパッケージ化できます。これを行うには、 Preferences-Miscellaneous-Create icon theme に移動し、アイコンを配置したフォルダーを選択します。次に、テーマのメタデータを入力し、[OK] をクリックします。これにより、テーマ アイコンを含む ZIP ファイルが作成されます。それを `Mobileread https://www.mobileread.com/forums/forumdisplay.php?f=166`__ のキャリバー フォーラムにアップロードしてください。その後、キャリバーの組み込みのアイコン テーマ システムを介してあなたのテーマを利用できるようにします。デフォルトでは、作成したばかりのアイコン テーマは、calibre の現在のテーマとしてもインストールされます。テーマをテストしている場合は、テーマのアイコンが使用されるように、 resources/images フォルダから画像を削除することを忘れないでください。

Calibre 6以降、ライトモードとダークモードのカスタムアイコンを使用できます.アイコンの 2 つのバージョンを作成し、拡張子 -for-dark-theme と -for-light-theme をファイルに付けます。たとえば、 modified-for-dark-theme.png と modified-for-light-theme.png です。その後、caliber は現在のテーマに基づいて適切なアイコンを自動的に使用します。

calibre をプラグインでカスタマイズ
calibre の設計は非常にモジュラーです。ほとんどすべての calibre の機能がプラグインという形で提供されます。プラグインは変換、ニュースのダウンロード (ただしこれはレシピと呼ばれます)、ユーザインタフェースのさまざまな部品、いろいろなデバイスへの接続、calibre にファイルを追加する際の処理など、多岐にわたって使用されます。ビルトインのプラグインは、環境設定  →  高度な設定  →  プラグイン ですべてを確認できます。

独自のプラグインを作成して、calibre の動作をカスタマイズしたり拡張することができます。calibre のプラグインアーキテクチャは非常に簡単です。チュートリアルを参照してください プラグインを書いて calibre を機能拡張。

プラグインを書いたら、 Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237] にある calibre のプラグインフォーラムにアップロードできます。そうすることで、calibre のビルトインのプラグインアップデータから利用できるようになります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- calibreをカスタマイズ »
- プラグインの API 説明書

プラグインの API 説明書

強力なプラグインを作成するためにサブクラス化できるさまざまな抽象基底クラスを定義します。便利なクラスは次のとおりです:

- プラグイン

- FileTypePlugin

- 書誌プラグイン

- カタログプラグイン

- 書誌ダウンロードプラグイン

- 変換プラグイン

- デバイスドライバ

- ユーザインタフェースアクション

- 設定プラグイン

プラグイン
class calibre.customize.Plugin(plugin_path)[ソース]

calibre のプラグイン。便利なメンバーは次のとおりです:

- self.installation_type: プラグインのインストール方法を保存します。

- self.plugin_path: このプラグインを含む ZIP ファイルへのパスを格納

またはビルトインプラグインなら None

- self.site_customization: ユーザの入力したカスタマイズ文字列を

格納

サブクラスでオーバーライドする必要があるメソッド:

- initialize()

- customization_help()

便利なメソッド:

- temporary_file()

- __enter__()

- load_resources()

supported_platforms = []

このプラグインが動作するプラットフォームのリスト。例:['windows', 'osx', 'linux']

name = 'Trivial Plugin'

このプラグインの名前。動作させるには、Trivial Plugin 以外のものを設定する必要があります。

version = (1, 0, 0)

このプラグインのバージョン。3 タプル (メジャー, マイナー, リビジョン)

description = 'まったく何もしません'

このプラグインの機能を説明する短い文字列

author = '不明'

このプラグインの作成者

priority = 1

ファイルタイプに複数のプラグインが存在する場合、プラグインは優先度の高い順に実行されます。優先度の高い方のプラグインが先に実行されます。指定可能な最高の優先順位は sys.maxsize です。デフォルトの優先度は1です。

minimum_calibre_version = (0, 4, 118)

このプラグインに必要な calibre の最低限のバージョン

installation_type = None

このプラグインのインストール方法

can_be_disabled = True

False にすると、ユーザがこのプラグインを無効にできなくなります。注意して使用してください。

type = '基本'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

initialize()[ソース]

calibre プラグインが初期化されるときに 1 回呼び出されます。プラグインは、新しいプラグインが追加されるたびに再初期化されます。また、本の追加などのワーカープロセスでプラグインが実行されている場合には、プラグインは新しいワーカープロセスごとに初期化されます。

プラグインの ZIP ファイルからリソースを抽出するなど、プラグイン固有の初期化をここで実行します。ZIP ファイルへのパスは self.plugin_path として利用できます。

ただし self.site_customization はこの時点では使用可能では ありません。

config_widget()[ソース]

このメソッドと save_settings() をプラグインに実装して、単純な文字列ベースのデフォルトのカスタマイズに依存するのではなく、カスタム構成ダイアログを使用します。

このメソッドは、実装された場合には QWidget を返す必要があります。ウィジェットにはオプションのメソッド validate() を含めることができます。このメソッド引数をとらず、ユーザが OKをクリックした直後に呼び出されます。メソッドが True を返す場合にのみ、変更が適用されます。

何らかの理由でこの時点で設定変更を実行できない場合は、2つの文字列 (メッセージ, 詳細)のタプルを返します。これらはユーザに警告ダイアログとして表示され、プロセスは中止されます。

save_settings(config_widget)[ソース]

config_widget を使用してユーザが指定した設定を保存します。

パラメータ:

config_widget -- config_widget() が返すウィジェット

do_user_config(parent=None)[ソース]

このメソッドは、このプラグインの設定ダイアログを表示します。ユーザが OK をクリックすると 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 変数に存在します。

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[ソース]

ベースクラス: Plugin

特定のファイルタイプのセットに関連付けられているプラグイン。

file_types = {}

このプラグインを実行する必要があるファイルタイプのセット。すべてのファイルタイプに '*' を使用します。例:`` {'lit'、'mobi'、'prc'} ``

on_import = False

True ならば、このプラグインは本がデータベースに追加されたときに実行されます

on_postimport = False

True のとき、このプラグインは、本がデータベースに追加された後に実行されます。その場合には、プラグインの postimport メソッドと postadd メソッドが呼び出されます。

on_postconvert = False

If True, this plugin is run after a book is converted.
In this case the postconvert method of the plugin is called.

on_postdelete = 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_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)[ソース]

インポート後、つまり本ファイルがデータベースに追加された後に呼び出されます。ただしこれは、本レコードが初めて作成されるときに呼び出される postadd() とは異なります。このメソッドは、新しいファイルが本レコードに追加されるたびに呼び出されます。新しく追加したファイルの内容に基づいて本レコードを変更する場合に便利です。

パラメータ:

- book_id -- 追加された本のデータベース ID。

- book_format -- 追加された本のファイルタイプ。

- db -- ライブラリデータベース。

postconvert(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.

パラメータ:

- book_id -- 追加された本のデータベース ID。

- book_format -- 追加された本のファイルタイプ。

- db -- ライブラリデータベース。

postdelete(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.

パラメータ:

- book_id -- 追加された本のデータベース ID。

- book_format -- 追加された本のファイルタイプ。

- db -- ライブラリデータベース。

postadd(book_id, fmt_map, db)[ソース]

本がデータベースに追加された後に呼び出されます。ただしこれは、ひとつの本ファイルが本に追加された後に呼び出される postimport() とは異なります。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)

このプラグインのバージョン。3 タプル (メジャー, マイナー, リビジョン)

author = 'Kovid Goyal'

このプラグインの作成者

type = '書誌の読み込み'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

get_metadata(stream, type)[ソース]

ストリームで表されるファイル(読み取りをサポートするオブジェクトのようなファイル)の書誌を返します。入力データにエラーがある場合は、例外を発生させます。

パラメータ:

type -- ファイルのタイプ。 file_types のエントリの1つであることが保証されています。

戻り値:

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)

このプラグインのバージョン。3 タプル (メジャー, マイナー, リビジョン)

author = 'Kovid Goyal'

このプラグインの作成者

type = '書誌の書き出し'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

set_metadata(stream, mi, type)[ソース]

ストリームで表されるファイル(読み取りをサポートするオブジェクトのようなファイル)の書誌を設定します。入力データにエラーがある場合は、例外を発生させます。

パラメータ:

- type -- ファイルのタイプ。 file_types のエントリの1つであることが保証されています。

- mi -- calibre.ebooks.metadata.book.Metadata オブジェクト

カタログプラグイン

class calibre.customize.CatalogPlugin(plugin_path)[ソース]

ベースクラス: Plugin

カタログジェネレータを実装するプラグイン。

file_types = {}

このプラグインを実行する必要のある出力ファイルタイプ。例: 'epub' や 'xml'

type = 'カタログ生成'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

cli_options = []

このプラグインに固有の CLI パーサーオプション。namedtuple Option として宣言されています:

from collections import namedtuple Option = namedtuple('Option', 'option, default, dest, help') cli_options = [Option('--catalog-title', default = 'マイカタログ', dest = 'catalog_title', help = (_('生成されるカタログのタイトル. nデフォルト:') + " '" + '%default' + "'"))] cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()

initialize()[ソース]

プラグインがビルトインでない場合は、プラグインの .ui ファイルと .py ファイルを ZIP ファイルから $TMPDIR にコピーします。タブは動的に生成され、calibre.gui2.dialogs.catalog.py:Catalog のカタログオプションダイアログに追加されます

run(path_to_output, opts, db, ids, notification=None)[ソース]

プラグインを実行します。サブクラスで実装する必要があります。file_types で指定された形式でカタログを生成し、生成されたカタログファイルへの絶対パスを返す必要があります。エラーが発生した場合は、例外を発生させます。

生成されたカタログファイルは、temporary_file() メソッドを使用して作成する必要があります

パラメータ:

- path_to_output -- 生成されたカタログファイルへの絶対パス。

- opts -- キーワード引数の辞書

- db -- LibraryDatabase2 オブジェクト

書誌ダウンロードプラグイン
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[ソース]

ベースクラス: Plugin

type = '書誌のソース'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

author = 'Kovid Goyal'

このプラグインの作成者

supported_platforms = ['windows', 'osx', 'linux']

このプラグインが動作するプラットフォームのリスト。例:['windows', 'osx', 'linux']

capabilities = frozenset({})

このプラグインでサポートされている機能のセット。便利な機能は次のとおりです: 'identify', 'cover'

touched_fields = frozenset({})

識別フェーズ中にこのプラグインによってダウンロードされる可能性のある書誌フィールドのリスト

has_html_comments = False

プラグインが HTML 形式のコメントを返す場合は、これを True に設定します

supports_gzip_transfer_encoding = False

これをTrueに設定すると、ブラウザオブジェクトは gzip 転送エンコーディングをサポートしていることを示します。これによりダウンロードを高速化できますが、最初にソースが実際にgzip転送エンコーディングを正しくサポートしていることを確認してください

ignore_ssl_errors = False

このソースに接続するときに HTTPS 証明書エラーを無視するには、これを True に設定します。

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 がないものもある場合、ISBN のない結果は無視されます。

is_configured()[ソース]

プラグインを使用する前に設定する必要がある場合は、False を返します。たとえば、ユーザ名/パスワード/APIキーが必要になる場合があります。

customization_help()[ソース]

このプラグインをカスタマイズする方法についてのヘルプを提供する文字列を返します。デフォルトでは NotImplementedError を発生させます。これは、プラグインがカスタマイズを必要としないことを示します。

このメソッドをサブクラスに再実装すると、ユーザはこのプラグインのカスタマイズとして文字列を入力するように求められます。カスタマイズ文字列は、self.site_customization として利用できます。

サイトのカスタマイズは、たとえば、ユーザのコンピュータで必要なバイナリへのパスなど、何でもかまいません。

パラメータ:

gui -- True なら HTML ヘルプを返し、そうでなければプレーンテキストのヘルプを返します。

config_widget()[ソース]

このメソッドと save_settings() をプラグインに実装して、単純な文字列ベースのデフォルトのカスタマイズに依存するのではなく、カスタム構成ダイアログを使用します。

このメソッドは、実装された場合には QWidget を返す必要があります。ウィジェットにはオプションのメソッド validate() を含めることができます。このメソッド引数をとらず、ユーザが OKをクリックした直後に呼び出されます。メソッドが True を返す場合にのみ、変更が適用されます。

何らかの理由でこの時点で設定変更を実行できない場合は、2つの文字列 (メッセージ, 詳細)のタプルを返します。これらはユーザに警告ダイアログとして表示され、プロセスは中止されます。

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)[ソース]

mi オブジェクトで null である self.touched_fields から最初のフィールドを返します

clean_downloaded_metadata(mi)[ソース]

プラグインの identify メソッドでこのメソッドを呼び出して、書誌オブジェクトを 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)[ソース]

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 get_book_url().

get_cached_cover_url(identifiers)[ソース]

Return cached cover URL for the book identified by the identifiers dictionary or None if no such URL exists.

このメソッドは、検証済みの 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 などで本を識別します。

ID が指定されていて、一致するものが見つからず、この書誌ソースに関連するすべてのID (たとえば、本のすべてのISBN)が保存されていない場合、このメソッドは、タイトルと作成者のみで再試行する必要があります(指定されている場合)。

この書誌ソースが表紙も提供している場合は、表紙への URL をキャッシュして、同じISBN/特別な IDを使用した表紙取得 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 -- 他の ID の辞書、最も一般的なのは {'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 を使用する必要があります。キャッシュされたデータが存在しない場合、ほとんどのプラグインは単に identify を呼び出し、その結果を使用します。

パラメータ get_best_cover が True であり、このプラグインが複数の表紙を取得できる場合、「最良の」表紙のみを取得する必要があります。

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[ソース]

検索クエリを指定して、書誌オブジェクトの関連性を比較するためのソートキーを生成します。これは、異なるソース間ではなく、同じ書誌ソースからの結果を比較するためにのみ使用されます。

ソートキーは、昇順のソートが関連性の低いオーダーによるソートであることを保証します。

アルゴリズムは:

- クエリと同じ ID が少なくともひとつある結果を優先する

- キャッシュされた表紙 URL で結果を優先する

- 使用可能なすべてのフィールドに入力した結果を優先する

- 現在のユーザインタフェース言語と同じ言語の結果を優先する

- クエリと完全に一致するタイトルの結果を優先する

- コメントが長い (10％以上長い) 結果を優先する

- 書誌ソースの検索エンジンによって報告された結果の関連性を使用する

engine

変換プラグイン
class calibre.customize.conversion.InputFormatPlugin(*args)[ソース]

ベースクラス: Plugin

InputFormatPlugins は、ドキュメントを 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 ファイルごとに1つずつ、画像のコレクションを生成します。入力ファイルが画像コレクションと非画像コレクションの両方である場合は、convert メソッドでこれを動的に設定できます。これを True に設定する場合は、画像のリストを返す get_images() メソッドを実装する必要があります。

core_usage = 1

このプラグインで使用される CPU コアの数。値 -1 は、使用可能なすべてのコアを使用することを意味します

for_viewer = False

Trueに設定すると、入力プラグインは特別な処理を実行して、出力を表示に適したものにします。

output_encoding = 'utf-8'

この入力プラグインがファイルを作成するエンコーディング。None の値は、エンコーディングが未定義であり、個別に検出する必要があることを意味します。

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

すべての入力形式プラグインで共有されるオプション。サブクラスでオーバーライドしないでください。代わりに options を使用してください。すべてのオプションは、OptionRecommendation のインスタンスである必要があります。

options = {}

このプラグインの動作をカスタマイズするためのオプション。すべてのオプションは、OptionRecommendation のインスタンスである必要があります。

recommendations = {}

フォームの 3 タプルのセット(option_name, recommended_value, recommendation_level)

get_images()[ソース]

この入力プラグインが画像コレクションを表す場合は、画像への絶対パスのリストを返します。画像のリストは、背表紙および目次と同じ順序です。

convert(stream, options, file_ext, log, accelerators)[ソース]

このメソッドは、サブクラスで実装する必要があります。作成された OPF ファイルまたは OEBBook インスタンスへのパスを返す必要があります。すべての出力は現在のフォルダに含まれている必要があります。このプラグインが現在のフォルダの外にファイルを作成する場合、このメソッドが戻る前に、それらを削除するか、削除のマークを付ける必要があります。

パラメータ:

- stream -- 入力ファイルを含むオブジェクトのようなファイル。

- options -- 変換プロセスをカスタマイズするためのオプション。このプラグインによって宣言されたすべてのオプションに対応する属性を持つことが保証されています。さらに、ゼロから上に向かって整数値を取る冗長属性があります。数値が大きいほど、冗長であることを意味します。もう1つの便利な属性は、calibre.customize.profiles.InputProfile のインスタンスである input_profile です。

- file_ext -- 入力ファイルの拡張子(ピリオドなし)。このプラグインでサポートされている file_types の1つであることが保証されています。

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

すべての入力形式プラグインで共有されるオプション。サブクラスでオーバーライドしないでください。代わりに options を使用してください。すべてのオプションは、OptionRecommendation のインスタンスである必要があります。

options = {}

このプラグインの動作をカスタマイズするためのオプション。すべてのオプションは、OptionRecommendation のインスタンスである必要があります。

recommendations = {}

フォームの 3 タプルのセット(option_name, recommended_value, recommendation_level)

property description

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

定されたオブジェクトから新しい文字列オブジェクトを作成します。encoding または errors が指定されている場合、オブジェクトは、指定された encoding とエラーハンドラを使用してデコードされるデータバッファを公開する必要があります。それ以外の場合は、object .__ str __() (定義されている場合) または repr(object) の結果を返します。encoding のデフォルトは sys.getdefaultencoding() です。errors のデフォルトは 'strict' です。

convert(oeb_book, output, input_plugin, opts, log)[ソース]

oeb_book (calibre.ebooks.oeb.OEBBook のインスタンス) の内容を、output で指定されたファイルにレンダリングします。

パラメータ:

- output -- オブジェクトまたは文字列のようなファイル。文字列の場合は、存在する場合と存在しない場合があるフォルダへのパスです。出力プラグインは、その出力をそのフォルダに書き込む必要があります。オブジェクトのようなファイルの場合、出力プラグインはその出力をファイルに書き込む必要があります。

- input_plugin -- 変換パイプラインの開始時に使用された入力プラグイン。

- opts -- 変換オプション。このプラグインの OptionRecommendations に対応する属性を持つことが保証されています。

- log -- ロガー。これを使用して、デバッグ/情報メッセージなどを印刷します。

specialize_options(log, opts, input_fmt)[ソース]

変換パイプラインで使用されるように、変換オプションの値を変更するために使用できます。

specialize_css_for_output(log, opts, item, stylizer)[ソース]

Can be used to make changes to the CSS during the CSS flattening process.

パラメータ:

- 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 は、BCD に基づいてデバイスを区別しないように None にするか、このドライバでサポートされているすべてのデバイスの 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']

本の書誌を GUI で設定できるかどうか。

CAN_DO_DEVICE_DB_PLUGBOARD = False

デバイスが device_db 書誌プラグボードを処理できるかどうか

path_sep = '/'

デバイス上の本へのパスのパス区切り文字

icon = 'reader.png'

このデバイスのアイコン

UserAnnotation

:py:class:`~calibre.devices.interface.Annotation`の別名です。

OPEN_FEEDBACK_MESSAGE = None

ステータスバーに None でない場合、GUI はこれをメッセージとして表示します。開くのに時間がかかる場合に便利です

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

デバイス上の「仮想本」であるため、ライブラリに表示/保存/追加できない拡張機能のセット。例:frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

仮想本拡張機能についてユーザに表示するメッセージ。

NUKE_COMMENTS = None

デバイスに送信された本のコピーのコメントを削除するかどうか。None でない場合、これはコメントが置き換えられる短い文字列である必要があります。

MANAGES_DEVICE_PRESENCE = False

True のとき、このドライバがデバイスの検出や取り出しなどを完全に管理していることを示します。これを True に設定する場合は、detect_managed_devices メソッドと debug_managed_device_detection メソッドを実装する必要があります。これを true に設定したドライバは、デバイスの検出、デバイスのブラックリスト、排出されたデバイスのリストなどを管理します。calibre は定期的に detect_managed_devices() メソッドを呼び出し、それが検出されたデバイスを返す場合、calibre は open() を呼び出します。open() は、open() の以前の呼び出しが失敗した場合でも、デバイスが返されるたびに呼び出されるため、ドライバは失敗したデバイスの独自のブラックリストを維持する必要があります。同様に、イジェクトするとき、calibreはeject() を呼び出し、次に detect_managed_devices() への次の呼び出しが None を返すと仮定して、post_yank_cleanup() を呼び出します。

SLOW_DRIVEINFO = False

True に設定すると、本のリストが読み込まれた後、calibre は get_driveinfo() メソッドを呼び出してdriveinfoを取得します。

ASK_TO_ALLOW_CONNECT = False

True に設定すると、calibre は、最初に検出されたときに、ユーザに calibre を使用してデバイスを管理するかどうかを尋ねます。これを True に設定する場合は、get_device_uid() と ignore_connected_device() と get_user_blacklisted_devices`と :meth:`set_user_blacklisted_devices() を実装する必要があります

user_feedback_after_callback = None

これを {'title':title, 'msg':msg, 'det_msg':detailed_msg} の形式の辞書に設定して、いくつかのコールバックが実行された後にユーザにメッセージをポップアップさせるようにします(現在は 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() によって返される USB デバイス

can_handle(device_info, debug=False)[ソース]

Unixバージョンの can_handle_windows()。

パラメータ:

device_info -- (vid, pid, bcd, 製造元, 製品名, シリアル番号) のタプルです

open(connected_device, library_uuid)[ソース]

デバイス固有の初期化を実行します。デバイスが検出された後、デバイスと通信する他の機能の前に呼び出されます。例: USBマスストレージデバイスとして表示されるデバイスの場合、この方法でデバイスをマウントするか、デバイスが自動マウントされている場合は、マウントされている場所を確認します。メソッド calibre.devices.usbms.device.Device.open() には、USB マスストレージデバイスの良い例として役立つこの関数の実装があります。

このメソッドは、OpenFeedback 例外を発生させて、ユーザにメッセージを表示することができます。

パラメータ:

- connected_device -- 開こうとしているデバイス。(ベンダ ID, 製品 ID, BCD, メーカー名, 製品名, デバイスのシリアル番号)のタプルです。ただし、一部のデバイスにはシリアル番号がなく、Windows では最初の 3 つのフィールドのみが表示され、残りは None です。

- library_uuid -- 現在の calibre ライブラリの UUID。ライブラリがない場合 (たとえば、コマンドラインから使用する場合)は None にすることができます。

eject()[ソース]

デバイスを OS からアンマウント/イジェクトします。これは、デバイスと通信する必要のある保留中の GUI ジョブがあるかどうかをチェックしません。

注:このメソッドは、他のデバイスメソッドと同じスレッドで呼び出されない場合があります。

post_yank_cleanup()[ソース]

デバイス上の本が calibre のデータベース内の本と一致するときに、本の一致中に呼び出されます。このメソッドは、デバイスからのデータを calibre のデータベースに同期する役割を果たします (必要な場合)。

set_progress_reporter(report_progress)[ソース]

進捗情報を報告する機能を設定します。

パラメータ:

report_progress -- さまざまなタスクの進行率 (0〜100の数値) で呼び出される関数。-1 で呼び出された場合、タスクに進行状況情報がないことを意味します

get_device_information(end_session=True)[ソース]

デバイスにデバイス情報を問い合わせます。L{DeviceInfoQuery} を参照してください。

戻り値:

(デバイス名, デバイスバージョン, デバイス上のソフトウェアバージョン, MIMEタイプ) タプルには、オプションで、ドライブ情報辞書である 5 番目の要素を含めることができます。例については、usbms.driver を参照してください。

get_driveinfo()[ソース]

driveinfo ディクショナリを返します。通常は get_device_information() から呼び出されますが、このドライバの driveinfo のロードが遅い場合は、SLOW_DRIVEINFO を設定する必要があります。この場合、このメソッドは、本リストがロードされた後に calibre によって呼び出されます。デバイススレッドでは呼び出されないため、ドライバはドライブ情報を books() メソッドにキャッシュし、この関数はキャッシュされたデータを返す必要があることに注意してください。

card_prefix(end_session=True)[ソース]

カードのパスにプレフィックスの 2 要素リストを返します。カードが存在しない場合、カードのプレフィックスには None が設定されます。例: ('/place', '/place2') (None, 'place2') ('place', None) (None, None)

total_space(end_session=True)[ソース]

マウントポイントで利用可能な合計スペースを取得します:

1. メインメモリ

2. メモリカード A

3. メモリカード B

戻り値:

バイト単位の合計スペースが (1、2、3) の3要素リスト。特定のデバイスにこれらの場所がない場合は、0 を返す必要があります。

free_space(end_session=True)[ソース]

マウントポイントで利用可能な空きスペースを取得します。

1. メインメモリ

2. カード A

3. カード B

戻り値:

バイト単位の空き領域が (1, 2, 3) の 3 要素リスト。特定のデバイスにこれらの場所がない場合は、-1 を返す必要があります。

books(oncard=None, end_session=True)[ソース]

デバイス上の電子書籍のリストを返します。

パラメータ:

oncard -- 'carda' または 'cardb' が特定のストレージカード上の電子書籍のリストを返す場合、それ以外の場合はデバイスのメインメモリにある電子書籍のリストを返します。カードが指定されていて、カードに本がない場合は、空のリストを返します。

戻り値:

本のリスト。

upload_books(files, names, on_card=None, end_session=True, metadata=None)[ソース]

本のリストをデバイスにアップロードします。ファイルがデバイスにすでに存在する場合は、それを置き換える必要があります。デバイスに十分な空き領域がない場合、このメソッドは FreeSpaceError を発生させる必要があります。FreeSpaceError のテキストには、on_card が None でない場合は "card" という単語が含まれている必要があります。それ以外の場合は、"memory" という単語が含まれている必要があります。

パラメータ:

- files -- パスのリスト

- names -- 本がデバイスにアップロードされたときに必要なファイル名のリスト。len(names)== len(files)

- metadata -- None でない場合は、Metadata オブジェクトのリストです。アイデアは、書誌を使用して、本を置くデバイスの場所を決定することです。len(metadata)== len(files)。通常の表紙(表紙へのパス)とは別に、サムネイル属性もある場合があります。これは優先的に使用する必要があります。サムネイル属性の形式は (幅, 高さ, JPEG の cover_data) です。

戻り値:

3 要素タプルのリスト。このリストは、add_books_to_metadata() に渡されることを意図しています。

classmethod add_books_to_metadata(locations, metadata, booklists)[ソース]

本リストに場所を追加します。この機能はデバイスと通信してはなりません。

パラメータ:

- locations -- L{upload_books} への呼び出しの結果

- metadata -- upload_books() と同じ、Metadata オブジェクトのリスト。

- booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard='cardb')`) への呼び出しの結果を含むタプル。

delete_books(paths, end_session=True)[ソース]

デバイス上のパスにある本を削除します。

classmethod remove_books_from_metadata(paths, booklists)[ソース]

書誌リストから本を削除します。この機能はデバイスと通信してはなりません。

パラメータ:

- paths -- デバイス上の本へのパス。

- booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard='cardb')`) への呼び出しの結果を含むタプル。

sync_booklists(booklists, end_session=True)[ソース]

デバイスの書誌を更新します。

パラメータ:

booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`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 オブジェクトには、デバイスのフォーマットの順序付きリストである少なくとも1つの属性 format_map が必要です。

set_plugboards(plugboards, pb_func)[ソース]

ドライバに現在のプラグボードのセットと特定のプラグボードを選択する機能を提供します。このメソッドは、add_books および sync_booklists の直前に呼び出されます。

pb_funcは、次のシグネチャを持つ呼び出し可能です:

def pb_func(device_name, format, plugboards)

現在のデバイス名 (クラス名または DEVICE_PLUGBOARD_NAME のいずれか)、関心のある形式('real' 形式または 'device_db')、およびプラグボード (これらは set_plugboards によって与えらます。このメソッドが与えられたのと同じ場所です)。

戻り値:

None または単一のプラグボードインスタンス。

set_driveinfo_name(location_code, name)[ソース]

driveinfo ファイルのデバイス名を 'name' に設定します。この設定は、ファイルが再作成されるか、名前が再度変更されるまで保持されます。

非ディスクデバイスは、get_device_information() メソッドによって返されるロケーションコードに基づいてこのメソッドを実装する必要があります。

prepare_addable_books(paths)[ソース]

パスのリストを指定すると、別のパスのリストを返します。これらのパスは、追加可能なバージョンの本を指しています。

本の準備中にエラーが発生した場合は、パスではなく、その本の返されるリスト内の位置を3 タプルにする必要があります:(original_path, 例外インスタンス, traceback)

startup()[ソース]

calibre がデバイスを起動しているときに呼び出されます。必要な初期化を行います。クラスの複数のインスタンスをインスタンス化できるため、__ init__ を複数回呼び出すことができますが、このメソッドが呼び出されるのは1つのインスタンスのみであることに注意してください。このメソッドは、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)[ソース]

デバイスが特定の設定を上書きする場合は、このメソッドを実装します。オーバーライド可能な設定が必要なすべての呼び出しサイトで、prefs['something'] ではなく device_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のデータベースに同期する役割を果たします(必要な場合)。

このメソッドは、2 つの値のタプルを返す必要があります。最初の値は、calibre のデータベースが変更された場合に変更された calibre 本 ID のセットであり、データベースが変更されていない場合は None です。最初の値が空のセットである場合、デバイス上の本の書誌は calibre の書誌で更新され、デバイスに戻されますが、その本の GUI 更新は行われません。これは、calibre データが正しいが、デバイスに送信する必要がある場合に役立ちます。

2 番目の値は、それ自体が 2 値のタプルです。タプルの最初の値は、本の形式をデバイスに送信するかどうかを指定します。目的は、デバイス上の本が calibre の本と同じであるかどうかを検証できるようにすることです。本を送信しない場合は、この値を None にする必要があります。そうでない場合は、デバイスのベースファイル名 (foobar.epub などの文字列) を返します。名前には必ず拡張子を含めてください。デバイスサブシステムは、戻り値が None でないすべての本に対して send_books ジョブを作成します。注: 後で拡張子を取得する場合を除いて、デバイスがテンプレートを使用してファイル名を生成する場合、名前は無視されます。ほとんどの場合、この名前は無視されます。返されたタプルの 2 番目の値は、その形式が未来の日付であるかどうかを示しています。そうである場合は 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. title

2. authors

3. size (本のファイルサイズ)

4. datetime (a UTC 時刻タプル)

5. path (デバイス上の本へのパス)

6. thumbnail (None でもかまいません)サムネイルは、画像データを含むstr / bytesオブジェクトであるか、画像への絶対 (プラットフォームネイティブ)パスを格納する属性 image_path を持っている必要があります

7. tags (文字列のリスト、空にすることができます)

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 は、BCD に基づいてデバイスを区別しないように None にするか、このドライバでサポートされているすべてのデバイスの 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文字列でデバイスの 2 番目のカードを識別する文字列。これは、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 要素リストを返します。カードが存在しない場合、カードのプレフィックスには None が設定されます。例: ('/place', '/place2') (None, 'place2') ('place', None) (None, None)

total_space(end_session=True)[ソース]

マウントポイントで利用可能な合計スペースを取得します:

1. メインメモリ

2. メモリカード A

3. メモリカード B

戻り値:

バイト単位の合計スペースが (1、2、3) の3要素リスト。特定のデバイスにこれらの場所がない場合は、0 を返す必要があります。

free_space(end_session=True)[ソース]

マウントポイントで利用可能な空きスペースを取得します。

1. メインメモリ

2. カード A

3. カード B

戻り値:

バイト単位の空き領域が (1, 2, 3) の 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() によって返される USB デバイス

open(connected_device, library_uuid)[ソース]

デバイス固有の初期化を実行します。デバイスが検出された後、デバイスと通信する他の機能の前に呼び出されます。例: USBマスストレージデバイスとして表示されるデバイスの場合、この方法でデバイスをマウントするか、デバイスが自動マウントされている場合は、マウントされている場所を確認します。メソッド calibre.devices.usbms.device.Device.open() には、USB マスストレージデバイスの良い例として役立つこの関数の実装があります。

このメソッドは、OpenFeedback 例外を発生させて、ユーザにメッセージを表示することができます。

パラメータ:

- connected_device -- 開こうとしているデバイス。(ベンダ ID, 製品 ID, BCD, メーカー名, 製品名, デバイスのシリアル番号)のタプルです。ただし、一部のデバイスにはシリアル番号がなく、Windows では最初の 3 つのフィールドのみが表示され、残りは None です。

- library_uuid -- 現在の calibre ライブラリの UUID。ライブラリがない場合 (たとえば、コマンドラインから使用する場合)は None にすることができます。

eject()[ソース]

デバイスを OS からアンマウント/イジェクトします。これは、デバイスと通信する必要のある保留中の GUI ジョブがあるかどうかをチェックしません。

注:このメソッドは、他のデバイスメソッドと同じスレッドで呼び出されない場合があります。

post_yank_cleanup()[ソース]

デバイス上の本が calibre のデータベース内の本と一致するときに、本の一致中に呼び出されます。このメソッドは、デバイスからのデータを calibre のデータベースに同期する役割を果たします (必要な場合)。

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

:py:class:`~calibre.devices.usbms.books.BookList`の別名です。

book_class

:py:class:`~calibre.devices.usbms.books.Book`の別名です。

FORMATS = []

サポートされている形式を順に並べ替えられたリスト

CAN_SET_METADATA = []

本の書誌を GUI で設定できるかどうか。

get_device_information(end_session=True)[ソース]

デバイスにデバイス情報を問い合わせます。L{DeviceInfoQuery} を参照してください。

戻り値:

(デバイス名, デバイスバージョン, デバイス上のソフトウェアバージョン, MIMEタイプ) タプルには、オプションで、ドライブ情報辞書である 5 番目の要素を含めることができます。例については、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 を発生させる必要があります。FreeSpaceError のテキストには、on_card が None でない場合は "card" という単語が含まれている必要があります。それ以外の場合は、"memory" という単語が含まれている必要があります。

パラメータ:

- files -- パスのリスト

- names -- 本がデバイスにアップロードされたときに必要なファイル名のリスト。len(names)== len(files)

- metadata -- None でない場合は、Metadata オブジェクトのリストです。アイデアは、書誌を使用して、本を置くデバイスの場所を決定することです。len(metadata)== len(files)。通常の表紙(表紙へのパス)とは別に、サムネイル属性もある場合があります。これは優先的に使用する必要があります。サムネイル属性の形式は (幅, 高さ, JPEG の cover_data) です。

戻り値:

3 要素タプルのリスト。このリストは、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 -- upload_books() と同じ、Metadata オブジェクトのリスト。

- booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard='cardb')`) への呼び出しの結果を含むタプル。

delete_books(paths, end_session=True)[ソース]

デバイス上のパスにある本を削除します。

remove_books_from_metadata(paths, booklists)[ソース]

書誌リストから本を削除します。この機能はデバイスと通信してはなりません。

パラメータ:

- paths -- デバイス上の本へのパス。

- booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard='cardb')`) への呼び出しの結果を含むタプル。

sync_booklists(booklists, end_session=True)[ソース]

デバイスの書誌を更新します。

パラメータ:

booklists -- (books(oncard=None)(), books(oncard='carda')(), :meth`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 モジュールを参照してください。

2 つの 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'

プラグイン名。同じ名前のプラグインが2つ存在する場合は、優先度の高いプラグインが優先されます。

priority = 1

プラグインの優先度。同じ名前のプラグインが2つ存在する場合は、優先度の高いプラグインが優先されます。

popup_type = 1

このプラグインがツールバーに追加されたときのメニューポップアップタイプ

auto_repeat = False

ショートカットキーを押したときにこのアクションを自動的に繰り返すかどうか。

action_spec = ('text', 'icon', None, None)

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_shortcut_name = 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_add_menu = False

True のとき、メニューが自動的に作成され、self.qaction に追加されます。

action_menu_clone_qaction = False

True のとき、self.qaction のクローンが self.qaction のメニューに追加されます。このアクションのテキストを self.qaction のテキストとは異なるものにする場合は、この変数を新しいテキストに設定します。

dont_add_to = frozenset({})

このアクションを追加してはならない場所のセット。可能な場所のリストについては、all_locations を参照してください

dont_remove_from = frozenset({})

このアクションを削除してはならない場所のセット。可能な場所のリストについては、all_locations を参照してください

action_type = 'global'

アクションのタイプ 'current' は現在のビューに作用することを意味します 'global' は現在のビューに作用するのではなく、全体として calibre に作用するアクションを意味します

accepts_drops = False

True のとき、この InterfaceAction はドラッグアンドドロップイベントと対話する機会があります。詳細については、メソッド accept_enter_event(), :meth`:accept_drag_move_event`, drop_event() を参照してください。

accept_enter_event(event, mime_data)[ソース]

このインタフェースアクションがドラッグイベントを処理できる場合、このメソッドは True を返す必要があります。イベントで accept/ignore を呼び出さないでください。これは calibre UI によって処理されます。

accept_drag_move_event(event, mime_data)[ソース]

このインタフェースアクションがドラッグイベントを処理できる場合、このメソッドは True を返す必要があります。イベントで accept/ignore を呼び出さないでください。これは calibre UI によって処理されます。

drop_event(event, mime_data)[ソース]

このインタフェースアクションがドロップイベントを処理できる場合、このメソッドはいくつかの有用なアクションを実行し、True を返す必要があります。イベントで accept/ignore を呼び出さないでください。これは 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 を返します。このアクションには、1つの追加属性 calibre_shortcut_unique_name があります。これは、None でない場合、このアクションがキーボードマネージャに登録される一意の名前を参照します。

パラメータ:

- menu -- 新しく作成されたアクションが追加される QMenu

- unique_name -- このアクションの一意の名前。これはグローバルに一意である必要があるため、できるだけわかりやすくします。疑わしい場合は、UUID を追加してください。

- text -- アクションのテキスト。

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

- shortcut -- 文字列、文字列のリスト、None または False。False の場合、このアクションのキーボードショートカットは登録されません。None の場合、デフォルトのキーバインドのないキーボードショートカットが登録されます。文字列と文字列のリストは、指定されたデフォルトのキーバインドでショートカットを登録します。

- description -- このアクションの説明。ツールチップを設定するために使用されます。

- triggered -- 作成されたアクションのトリガーされたシグナルに接続されている呼び出し可能オブジェクト。

- shortcut_name -- このアクションのキーボードショートカットをカスタマイズするときにユーザに表示されるテキスト。デフォルトでは text` の値に設定されています。

- persist_shortcut -- `` persist_shortcut`` が True に設定されていない限り、他のキーボードショートカットを編集すると、常に表示されるとは限らない、またはライブラリに依存するアクションのショートカットが表示されなくなる場合があります。

load_resources(names)[ソース]

このプラグインがZIPファイル (ユーザが追加したプラグイン) で提供されている場合、このメソッドを使用すると、ZIPファイルからリソースをロードできます。

画像を読み込む例:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

パラメータ:

names -- / を区切り文字として使用した、ZIPファイル内のリソースへのパスのリスト

戻り値:

{name : file_contents} 形式の辞書。ZIPファイルで見つからなかった名前は、辞書に表示されません。

genesis()[ソース]

このプラグインを設定します。初期化中に 1 回だけ呼び出されます。self.gui が利用可能です。action_spec で指定されたアクションは、self.qaction として使用できます。

location_selected(loc)[ソース]

calibre に表示されている本リストが変更されるたびに呼び出されます。現在、loc の値は、``library, main, card and cardb``です。

このメソッドは、場所に応じて、このアクションとそのサブアクションを有効/無効にする必要があります。

library_about_to_change(olddb, db)[ソース]

現在のライブラリが変更されるたびに呼び出されます。

パラメータ:

- olddb -- 前のライブラリに関連する LibraryDatabase。

- db -- 新しいライブラリに関連する LibraryDatabase。

library_changed(db)[ソース]

現在のライブラリが変更されるたびに呼び出されます。

パラメータ:

db -- 現在のライブラリに対応する LibraryDatabase。

gui_layout_complete()[ソース]

メイン GUI のレイアウトが完了すると、アクションごとに1回呼び出されます。アクションでレイアウトを変更する必要がある場合は、initialization_complete() ではなく、ここで変更する必要があります。

initialization_complete()[ソース]

メイン GUI の初期化が完了すると、アクションごとに 1 回呼び出されます。

tag_browser_context_action(index)[ソース]

タグブラウザにコンテキストメニューを表示するときに呼び出されます。`` index`` は、右クリックされたタグブラウザアイテムを指す QModelIndex です。index.valid() を使用して有効性をテストし、index.data(Qt.ItemDataRole.UserRole) を使用して基になる TagTreeItem オブジェクトを取得します。このメソッドによって生成されたアクションオブジェクトはすべて、コンテキストメニューに追加されます。

shutting_down()[ソース]

メインGUIがシャットダウン中の場合、プラグインごとに1回呼び出されます。使用済みのリソースを解放しますが、シャットダウンを長期間ブロックしないようにしてください。

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

環境設定ダイアログに表示されるウィジェットを表すプラグイン。

このプラグインには、重要なメソッドが1つだけあります。create_widget() です。プラグインのさまざまなフィールドは、UI でのプラグインの分類方法を制御します。

supported_platforms = ['windows', 'osx', 'linux']

このプラグインが動作するプラットフォームのリスト。例:['windows', 'osx', 'linux']

author = 'Kovid Goyal'

このプラグインの作成者

type = '環境設定'

このプラグインのタイプ。GUI でプラグインを分類するために使用されます

can_be_disabled = False

False にすると、ユーザがこのプラグインを無効にできなくなります。注意して使用してください。

config_widget = None

ConfigWidgetInterface を実装する ConfigWidget という名前のクラスを含むモジュールへのパスをインポートします。create_widget() によって使用されます。

category_order = 100

カテゴリのリストのどこに、このプラグインの category があるべきか。

name_order = 100

カテゴリ内の名前のリストで、このプラグインの gui_name は次のようになります

category = None

このプラグインが含まれるべきカテゴリ

gui_category = None

このプラグインのユーザに表示されるカテゴリ名

gui_name = None

このプラグインのユーザに表示される名前

icon = None

このプラグインのアイコンは絶対パスである必要があります

description = None

ツールチップなどに使用される説明

create_widget(parent=None)[ソース]

このプリファレンスのグループを設定するために使用される実際の Qt ウィジェットを作成して返します。ウィジェットは calibre.gui2.preferences.ConfigWidgetInterface を実装する必要があります。

デフォルトの実装では、config_widget を使用してウィジェットをインスタンス化します。

class calibre.gui2.preferences.ConfigWidgetInterface[ソース]

このクラスは、環境設定ダイアログに表示されるすべてのウィジェットが実装する必要のあるインタフェースを定義します。このインタフェースを実装し、さまざまな便利なメソッドも定義する基底クラスについては、ConfigWidgetBase を参照してください。

changed_signal = None

このシグナルは、ユーザがこのウィジェットの値を変更するたびに発行される必要があります

supports_restoring_to_defaults = True

restore_to_defaults() メソッドが実装されている場合は、True に設定します。

restore_defaults_desc = 'デフォルト値に設定を戻します。デフォルト設定を実際に保存するには「適用」をクリックしてください。'

"デフォルトに戻す" ボタンのツールチップ

restart_critical = False

True のとき、環境設定ダイアログでは、ユーザはこれ以上設定を設定できません。commit() が True を返す場合にのみ効果があります。

genesis(gui)[ソース]

ウィジェットが表示される前に 1 回呼び出され、必要なセットアップを実行する必要があります。

パラメータ:

gui -- メイン calibre のグラフィカルユーザインタフェース

initialize()[ソース]

すべての構成値を初期値(構成ファイルに保存されている値)に設定する必要があります。"return" 文はオプションです。ダイアログを表示しない場合は False を返します。

restore_defaults()[ソース]

すべての構成値をデフォルトに設定する必要があります。

commit()[ソース]

変更した設定を保存します。変更で再起動が必要な場合は True を返し、それ以外の場合はFalseを返します。AbortCommit 例外を発生させて、エラーが発生したことを示します。あなたは、エラーが何であるか、そしてそれを修正する方法についてユーザにフィードバックを与える責任があります。

refresh_gui(gui)[ソース]

このウィジェットがコミットされた後に 1 回呼び出されます。変更された設定を GUI に再読み取りさせる責任があります。ただしデフォルトでは、GUI はとにかくさまざまな要素を再初期化するため、ほとんどのウィジェットはこのメソッドを使用する必要がありません。

initial_tab_changed()[ソース]

Called if the initially displayed tab is changed before the widget is shown, but after it is initialized.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[ソース]

チェックボックス、コンボボックス、テキストフィールドなどの標準構成ウィジェットを簡単に追加するためのコードを含む基底クラス。register() メソッドを参照してください。

このクラスは、登録された設定の変更通知、デフォルトへのリセット、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 -- この設定が複数選択 (コンボボックス) ベースの設定である場合、選択肢のリスト。このリストは、次の形式の 2 つの要素タプルのリストです: [(gui name, value), ...]

- setting -- この設定の管理を担当するクラス。デフォルトのクラスはほとんどすべてのケースを処理するため、このパラメータが使用されることはめったにありません。

initialize()[ソース]

すべての構成値を初期値(構成ファイルに保存されている値)に設定する必要があります。"return" 文はオプションです。ダイアログを表示しない場合は False を返します。

commit(*args)[ソース]

変更した設定を保存します。変更で再起動が必要な場合は True を返し、それ以外の場合はFalseを返します。AbortCommit 例外を発生させて、エラーが発生したことを示します。あなたは、エラーが何であるか、そしてそれを修正する方法についてユーザにフィードバックを与える責任があります。

restore_defaults(*args)[ソース]

すべての構成値をデフォルトに設定する必要があります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース

コマンドライン・インタフェース

注釈

On macOS, the command line tools are inside the calibre bundle, for example, if you installed calibre in /Applications the command line tools are in /Applications/calibre.app/Contents/MacOS/. So, for example, to run ebook-convert you would use: /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

文書化されていないコマンドの使用方法を知るには、端末で引数なしで実行してください。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibre

calibre

calibre [オプション] [電子書籍へのパスまたは calibre URL ...]

メインの calibre グラフィカルユーザインタフェースを起動し、指定があれば path_to_ebook
に置かれた電子書籍をデータベースに追加します。calibre URL を指定することにより、本を追加するだけでなく別の動作を行わせることも可能です。例えば次のように指定した場合
calibre://view-book/test_library/1842/epub
calibre の "test_library" という名のライブラリにある EPUB 形式かつ ID が1842 の本を電子書籍ビューアで開きます。ライブラリ名にはライブラリのフォルダ名のスペースをアンダースコアで置換したものを指定します。URL ベースのさまざまな動作についての詳細はユーザマニュアルを参照してください。

スペースを含む引数を calibre に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--detach

制御端末(あれば)から分離 (Linuxのみ)

--help, -h

このヘルプメッセージを表示して終了

--ignore-plugins

カスタムプラグインを無視。プラグインでcalibreの起動ができない場合に有用。

--no-update-check

アップデートをチェックしない

--shutdown-running-calibre, -s

実行中のcalibreをシャットダウン。注意: ジョブが動作中であっても警告なしに終了するため、注意して実行してください。

--start-in-tray

システム･トレイに最小化して起動

--verbose, -v

無視されました。使用されません。歴史的な理由のためだけに存在します。

--version

プログラムのバージョン番号を表示して終了

--with-library

指定したパスにあるライブラリを使用する。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibre-customize

calibre-customize

calibre-customize オプション

外部プラグインをロードしてcalibreをカスタマイズします。

スペースを含む引数を calibre-customize に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--add-plugin, -a

ZIPファイルを指定してプラグインを追加

--build-plugin, -b

プラグイン開発者向け: プラグインを開発しているフォルダへのパス。このコマンドは自動的にプラグインを圧縮し、calibreで更新します。

--customize-plugin

Customize plugin. Specify name of plugin and customization string separated by a comma. The customization string is the same as you would enter when customizing the plugin in the main calibre GUI.

--disable-plugin

名前のついたプラグインを無効化

--enable-plugin

名付けたプラグインを有効にする

--help, -h

このヘルプメッセージを表示して終了

--list-plugins, -l

インストールされたプラグインを一覧表示

--remove-plugin, -r

カスタムのプラグインを名前で削除します。組み込みのプラグインには影響しません。

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibre-debug

calibre-debug

calibre-debug [options]

Various command line interfaces useful for debugging calibre. With no options, this command starts an embedded Python interpreter. You can also run the main calibre GUI, the calibre E-book viewer and the calibre editor in debug mode.

It also contains interfaces to various bits of calibre that do not have dedicated command line tools, such as font subsetting, the E-book diff tool and so on.

You can also use calibre-debug to run standalone scripts. To do that use it like this:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 ...

Everything after the -- is passed to the script. You can also use calibre-debug as a shebang in scripts, like this:

#!/usr/bin/env -S calibre-debug -e -- --

スペースを含む引数を calibre-debug に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--add-simple-plugin

プラグインのコードを含むpyファイルのpathを指定し、単純なプラグイン(単一の.pyファイルのみで構成されるプラグイン)を追加

--command, -c

Pythonコードを実行

--debug-device-driver, -d

デバイス認識をデバッグ

--default-programs

Windows の既定のプログラムに登録/登録解除 --default-programs = (register|unregister)

--diff

差分ツールを実行。例: calibre-debug --diff file1 file2

--edit-book

書籍編集ツールをデバッグモードで起動

--exec-file, -e

ファイル中のPythonコードを実行

--explode-book, -x

指定されたディレクトリに本を分解。 使用例: -x file.epub output_dir 本をHTMLファイルと書誌のまとまりとしてエクスポートし、標準的なHTML編集ツールで編集できるようにします。EPUB, AZW3, HTMLZ, DOCX に利用可能です。

--export-all-calibre-data

Export all calibre data (books/settings/plugins). Normally, you will be asked for the export folder and the libraries to export. You can also specify them as command line arguments to skip the questions. Use absolute paths for the export folder and libraries. The special keyword "all" can be used to export all libraries. Examples: calibre-debug --export-all-calibre-data # for interactive use 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 # export all known libraries

--fix-multiprocessing

内部用

--gui, -g

GUIをデバッグモードで実行。デバッグの出力はstdoutとstderrに出力されます。

--gui-debug

デバッグコンソールでGUIを実行し、指定されたpathにログを記録。内部での使用に限定し、GUIをデバッグモードで実行するには -g オプションを使用してください。

--help, -h

このヘルプメッセージを表示して終了

--implode-book, -i

分解された本を連結。 使用例: -i output_dir file.epub :option:`--explode-book`により分割されたファイルの置かれた output_dir を読み込んで連結し、本にします。必ず分割したときと同じファイルタイプを指定してください。

--import-calibre-data

以前エクスポートしたデータをインポート

--inspect-mobi, -m

指定されたパスのMOBIファイルを検査

--paths

calibreの環境設定に必要なpathを出力

--run-plugin, -r

コマンドラインインタフェースを提供するプラグインを実行。例: calibre-debug -r "Plugin name" -- file1 --option1 -- 以降にあるものは、すべてプラグインに引数として渡されます。

--run-test, -t

Run the named test(s). Use the special value "all" to run all tests. If the test name starts with a period it is assumed to be a module name. If the test name starts with @ it is assumed to be a category name.

--run-without-debug

Don't run with the DEBUG flag set

--shutdown-running-calibre, -s

実行中のcalibreをシャットダウン。注意: ジョブが動作中であっても警告なしに終了するため、注意して実行してください。

--subset-font, -f

指定されたフォントをサブセット化。このオプションの後に "--" をつけるとフォント・サブセットプログラムにオプションを渡します。

--test-build

ビルド中にバイナリーモジュールをテスト

--version

プログラムのバージョン番号を表示して終了

--viewer, -w

電子書籍ビューアをデバッグモードで実行

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibre-server

calibre-server

calibre-server [オプション] [ライブラリフォルダへのパス...]

calibreのコンテンツサーバを開始。コンテンツサーバはcalibreライブラリをインターネット上に公開します。calibre-server の引数としてライブラリフォルダへのパスを指定することができます。pathの指定がない場合には、calibreプログラムの知っているすべてのライブラリが対象となります。

スペースを含む引数を calibre-server に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--access-log

アクセスログファイルへのパス。このログにはクライアントからサーバへの接続や、要求についての情報が記録されます。デフォルトでは、アクセスログは記録しません。

--ajax-timeout

クエリ実行時にサーバからの応答を待つ最大時間 (秒指定).

--auth-mode

使用する認証タイプ. サーバが使用するHTTP認証モード。SSLプロキシを介してサーバを公開する場合には "basic" を設定してください。そうでない場合には "auto" にしてください。"auto" は、SSLが設定されている場合には "basic" を使用し、それ以外の場合には "digest" を使用します。

--auto-reload

ソースコードが変更されたとき自動的にサーバの再読み込みを行います。デバッグ時に有用です。シャットダウンタイムアウトに小さい値を指定するのもよいでしょう。

--ban-after

利用停止するまでのログイン失敗回数. ログインに何回失敗したらIPアドレスを遮断するかを指定。

--ban-for

連続してログインに失敗したIPアドレスを遮断. 繰り返しログインに失敗したIPアドレスを、指定した時間 (分単位) だけ一時的に遮断します。ブルートフォース攻撃への対策として有効です。0 を設定すると、遮断しません。

--book-list-mode

デフォルトの本の一覧モードを選択. 新規ユーザ向けのデフォルトの本の一覧モードを設定します。個々のユーザは自分で設定することによりデフォルトのモードを上書きできます。初期設定ではカバーグリッドを使用します。

--compress-min-size

応答がデータ圧縮を使用する最小サイズ (バイト単位).

--custom-list-template

カスタムな本一覧モードのテンプレートを格納した JSON へのパス。このテンプレートを作成する最も簡単な方法は、calibreの 環境設定->ネット共有->本の一覧テンプレート へ行ってテンプレートを作成し、エクスポートすることです。

--daemonize

プロセスをバックグラウンドでデーモンとして起動 (Linuxのみ)

--displayed-fields

表示するユーザ定義フィールドを限定. コンテンツサーバ /opds および /mobile ビューで表示させるユーザ定義の書誌フィールドを、カンマ区切りで指定。このオプションを指定したとき、リストにないフィールドは表示されなくなります。例: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

ソケットの事前割り当て。たとえばsystemdソケットアクティベーションなどと使用する場合。. デフォルトでは、このオプションは有効に設定されています。

--enable-auth, --disable-auth

サーバアクセスにパスワード認証を使用. 通常、サーバへのアクセスには制限を設けず、誰にでもアクセスできるようになっています。このオプションを使うと、アクセスを事前に定義したユーザに限定することができます。 デフォルトでは、このオプションは無効に設定されています。

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

自動認識したインタフェースにフォールバック. 何らかの理由によりlisten_onオプションで指定されたインタフェースにサーバがバインドできない場合、外部に接続するインタフェースを検出してバインドしようと試みます。 デフォルトでは、このオプションは有効に設定されています。

--enable-local-write, --disable-local-write

認証されていないローカルコネクションでの変更を許可. 通常、認証を有効にしていない場合には、匿名ユーザがcalibreライブラリに変更を加えることができないよう、サーバは読み取り専用モードで動作します。このオプションを有効にすると、サーバと同じコンピュータからの接続の場合には、誰でも変更を加えることができるようになります。認証なしでサーバを運用しながら、calibreライブラリを変更するのにcalibredbを利用する場合に便利です。 デフォルトでは、このオプションは無効に設定されています。

--enable-log-not-found, --disable-log-not-found

HTTP 404 (Not Found) 要求をログに保存. 通常、サーバはリソースが見つからなかったときのHTTP要求をログ保存します。しかしBOTの標的にされた場合には、ログがスパムであふれることになります。ログを止めるためにこのオプションを利用してください。 デフォルトでは、このオプションは有効に設定されています。

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

接続を待ち受けするインタフェース. The default is to listen on all available IPv6 and IPv4 interfaces. You can change this to, for example, "127.0.0.1" to only listen for IPv4 connections from the local machine, or to "0.0.0.0" to listen to all incoming IPv4 connections.

--log

サーバログファイルへのパス。このログにはサーバ情報とエラーが記録されますが、アクセスログは記録しません。デフォルトでは標準出力に出力されます。

--manage-users

このサーバへの接続が許可されているユーザのデータベースを管理します。-- を追加することで自動モードで使用できます。詳細については calibre-server --manage-users -- help を参照してください。また --userdb オプションも参照してください。

--max-header-line-size

単一のHTTPヘッダの最大サイズ (KB単位).

--max-job-time

ワーカープロセスの最大時間. ワーカープロセスの実行を許可する最大時間 (分単位)。制限しない場合にはゼロを指定。

--max-jobs

ワーカープロセスの最大数. ワーカープロセスは本の閲覧準備/追加/変換などの大きなジョブを扱うために、必要に応じて起動されます。通常、こうしたプロセスの最大数はCPUコア数に基づいて決定します。この設定では、そのプロセス最大数を制御できます。

--max-log-size

ログファイルの最大長 (MB単位). サーバが精製するログファイルの最大サイズ。ログが指定のサイズを超えると、先頭から上書きされます。0を設定すると、上書きしなくなります。

--max-opds-items

OPDSフィード中の最大冊数. サーバが単一のOPDSフィード内に返す本の最大数

--max-opds-ungrouped-items

OPDS フィード内のグループ化されていない項目の最大数. 著者やタグなどのカテゴリの1文字目の数がここで指定した数を超えた場合にグループ化します。0を指定するとグループ化しません。

--max-request-body-size

サーバへのアップロードを許可する最大サイズ (MB単位).

--num-per-page

1ページあたりに表示する本の数. ブラウザ1ページあたりに表示する本の数

--pidfile

プロセスPIDを書き込むファイルを指定

--port

コネクションの接続待ち用ポート.

--search-the-net-urls

「インターネットで検索」で使用する、URLを記述したJSONファイルへのパス。環境設定->ネット共有->インターネットで検索 へ行き、URLを作成してエクスポートすると簡単にファイルを作成できます。

--shutdown-timeout

クリーンシャットダウンを行う前の待ち時間 (秒単位).

--ssl-certfile

SSL 認証ファイルへのパス.

--ssl-keyfile

SSL プライベートキーファイルへのパス.

--timeout

アイドルな接続を切断するまでの時間 (秒単位).

--trusted-ips

指定したIPからの非認証接続での変更を許可. 通常、認証を有効にしない場合にはサーバは読み取り専用モードで動作し、匿名ユーザがキャリバーライブラリに変更を加えることを許可しません。このオプションを使用すると、指定した IP アドレスから接続しているすべてのユーザに変更を許可します。TRUSTED_IP はアドレスまたはネットワークをカンマ区切りで指定します。認証なし実行しているサーバ上で calibredb を使用して calibre ライブラリに変更を加えたい場合に使える機能です。このオプションを有効にした場合には、指定した IP アドレスから接続するユーザは誰でもライブラリに変更を加えることができることに注意してください。

--url-prefix

すべてのURLに追加するプレフィックス. リバースプロキシをはさんでサーバを運用する場合に有用です。使用例: URLプレフィックスとして /calibre

--userdb

認証に使用するユーザデータベースへのパス。データベースは SQLite ファイルです。データベース作成には:option:`--manage-users`を使用します。ユーザ管理についての詳細は https://manual.calibre-ebook.com/ja/server.html#managing-user-accounts-from-the-command-line-only を参照してください。

--version

プログラムのバージョン番号を表示して終了

--worker-count

要求を処理するワーカースレッド数.

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibre-smtp

calibre-smtp

calibre-smtp [options] [from to text]

SMTPプロトコルを用いてメールを送信します。 calibre-smtp は2つのモードで動作します。
compose モードでは from, to, text を指定し、それを利用してメッセージを組み立て送信を行います。
filter モードでは calibre-smtp は標準入力から完全な電子メールメッセージを読み込んでそれを送信します。

text は電子メールメッセージの本体です。
text が指定されなかった場合、電子メールメッセージは標準入力から読み込まれます。
from は送信者のメールアドレスで、to は電子メールの受取人のアドレスです。
text が標準入力から読み込まれた場合、from と to はSMTP のネゴシエーションでのみ使用され、メッセージのヘッダは変更されません。

スペースを含む引数を calibre-smtp に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--fork, -f

Forkしてバックグラウンドでメッセージの配送を行います。このオプションを使用する場合は、:option:`--outbox`で配送に失敗したメッセージを受け取れるようにしてください。

--help, -h

このヘルプメッセージを表示して終了

--localhost, -l

localhostのホスト名。SMTPサーバとの接続時に使用します。

--outbox, -o

送信に失敗したメッセージを保存する maildir フォルダのパス

--timeout, -t

接続タイムアウト

--verbose, -v

出力メッセージを増やす

--version

プログラムのバージョン番号を表示して終了

COMPOSE MAIL

電子メールを組み立てる際のオプション。textが指定されなかった場合は無視されます。

--attachment, -a

電子メールに添付するファイル

--subject, -s

電子メールの件名

SMTP RELAY

メールの送信にSMTPリレーサーバを使う場合のオプション。--relayが指定されていなかった場合、calibreは電子メールを直接送信するよう試みます。

--cafile

TLS接続時のサーバ認証に使用するPEM形式のCA証明書ファイルへのパス。デフォルトではシステムのCA証明書を使用します。

--dont-verify-server-certificate

TLSを使った接続時にサーバ証明書の確認を行いません。これはバージョン3.27以前のcalibreにおけるデフォルトの動作でした。自己署名証明書あるいは不正な証明書を使った中継を利用している場合に、このオプションにより3.27以前と同様に動作させることができます。

--encryption-method, -e

リレーサーバへの接続時に使用する暗号化方式。TLS, SSL, NONEが指定できます。デフォルトはTLSです。警告: NONEは大変危険です。

--password, -p

リレーサーバのパスワード

--port

リレーサーバとの接続に使用するポート。デフォルトは、SSLで暗号化を行う場合は 465 、そうでなければ 25です。

--relay, -r

メールの送信に使うSMTPリレーサーバ。

--username, -u

リレーサーバのユーザ名

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- calibredb

calibredb

calibredb command [options] [arguments]

calibredb は、calibre データベースへのコマンドラインインタフェースです。
以下に説明するようなサブコマンドがあります。

calibredb はパスで指定した calibre データベース、またはローカルまたはインターネット上で実行中の calibre コンテンツサーバ に対して操作を行うのに利用できます。コンテンツサーバ を起動するには calibre-server プログラムを、使用するか、またはメインの calibre プログラム上で 接続/共有  →  コンテンツサーバを開始をクリックします。calibredb は calibre ライブラリへの変更を行うことができるため、まずサーバ上に認証を設定しておく必要があります。それには次の 2 種類の方法があります。

- 同じコンピュータ上で動作するサーバにのみアクセスする予定であれば、コンテンツサーバのオプションとして --enable-local-write を指定するだけでかまいません。これにより calibredb を含めてローカルなコンピュータ上で実行されるすべてのプログラムが calibre ののデータに変更を加えることができるようになります。サーバをメインの calibre プログラムから起動した場合、このオプションは 設定 → ネット共有 → 詳細 にあります。

- インターネット越しの接続を許可したい場合、サーバ上にユーザアカウントを設定して --username および --password を使用して calibre へのアクセスを許可してください。calibre-server の認証は --enable-auth オプションを使用し、--manage-users を使ってユーザアカウントを作成します。
メイン calibre からサーバを実行している場合には、環境設定 → ネット共有 → ユーザ名とパスワードを要求を使用してください。

実行中のコンテンツサーバに接続するには、--with-library オプションにサーバへの URL を渡します。このオプションについての詳しい説明や使用例はドキュメントを参照してください。

- グローバルオプション

- 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ライブラリへのパス。デフォルトでは設定画面で指定されたpathが使用されます。リモートライブラリに対して操作を行うためにcalibreコンテンツサーバに接続することも可能です。コンテンツサーバに接続するには、http://hostname:port/#library_id の形式のURLを指定します。例えば http://localhost:8080/#mylibrary のように指定します。library_id はコンテンツサーバ上の接続したいライブラリのIDです。特殊な library_id として - を指定すると、そのサーバ上で利用可能なライブラリの一覧を表示できます。コンテンツサーバ経由でのアクセスを設定する方法について詳しくは https://manual.calibre-ebook.com/ja/generated/ja/calibredb.html を参照してください。

--password

calibreコンテンツサーバ接続用パスワード。標準入力からパスワードを読み取るには、特殊文字 <stdin> を使ってください。パスワードをファイルから読み込むには <f:/path/to/file> を使ってください。 (例えば先頭の <f: に続けてファイルへのフルパスを記述し、最後に > で閉じます。) カギ括弧は必須です。使用するシェルによっては、必要に応じてエスケープしたり引用符で囲ってください。

--timeout

ネットワーク経由でキャリバーライブラリに接続するときのタイムアウト。秒単位。デフォルトは2分です。

--username

calibreコンテンツサーバ接続用ユーザ名

--version

プログラムのバージョン番号を表示して終了

list

calibredb list [options]

calibreデータベースに存在する本のリスト

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--ascending

昇順に並び替え

--fields, -f

データベース中の本の一覧を表示するときに表示するフィールド。フィールドをカンマ区切りで指定します。 指定可能なフィールド: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid デフォルト: title,authors 特殊フィールド名 "all" を指定するとすべてのフィールドを選択します。上記のビルトインフィールドの他、カスタムフィールドも *field_name として指定可能です。たとえば、カスタムフィールド #rating を表示させたいときには *rating と指定してください。

--for-machine

JSON形式で出力します。JSONはコンピュータでの処理により適した形式です。このオプションを指定すると行幅とセパレーターオプションは無視されます。

--limit

表示する最大結果数。デフォルトはすべて

--line-width, -w

出力ファイル中の、1行の最大の長さ。デフォルトは画面サイズから判別。

--prefix

すべてのファイルパスのプレフィックス。デフォルトはライブラリフォルダへの絶対パス。

--search, -s

検索文字列で結果を絞り込む。検索文字列の形式については、ユーザマニュアルの検索関連のドキュメントを参照してください。デフォルトでは絞り込みを行いません。

--separator

区切り文字(デフォルトは半角スペース）

--sort-by

The field by which to sort the results. You can specify multiple fields by separating them with commas. Available fields: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Default: id

--template

The template to run if "template" is in the field list. Default: None

--template_file, -t

Path to a file containing the template to run if "template" is in the field list. Default: None

--template_heading

Heading for the template column. Default: template. This option is ignored if the option --for-machine is set

add

calibredb add [options] file1 file2 file3 ...

指定したファイルを本としてデータベースに追加します。フォルダを指定することもできます。参照以下のフォルダ関連のオプション。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--authors, -a

追加する本の著者を設定

--automerge, -m

似たようなタイトルと著者の本が見つかった場合は、受信した形式（ファイル）を既存の本のレコードに自動的にマージします。 'ignore'の値は、重複する形式が破棄されることを意味します。 'overwrite'の値は、ライブラリ内の重複する形式が新しく追加されたファイルで上書きされることを意味します。 'new_record'の値は、重複する形式が新しいブックレコードに配置されることを意味します。

--cover, -c

追加する本の表紙をパスで指定

--duplicates, -d

本がすでにデータベースに存在する場合でも追加します。本のタイトルと著者に基づいて比較します。--automerge オプションが優先します。

--empty, -e

空の本を追加する（形式が1つも無い本）

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

idsに指定された本をデータベースから削除。idsには数字をカンマ区切りで指定します (本のIDはsearchコマンドで取得できます)。例: 23,34,57-85 (範囲で指定した場合、範囲の終端は含まれません)

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--permanent

ゴミ箱は使用しないでください

add_format

calibredb add_format [options] id ebook_file

ebook_file で指定する電子書籍ファイルを、id で指定された本の利用可能な形式として追加します。idはsearch コマンドで取得できます。形式がすでに存在する場合は、置換しないオプションを有効にした場合をのぞいて、置換されます。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--as-extra-data-file

Add the file as an extra data file to the book, not an ebook format

--dont-replace

形式がすでに存在する場合に置換しない

remove_format

calibredb remove_format [options] id fmt

fmt で指定した形式を id で指定した本から削除します。id は search コマンドで取得できます。fmt はLRF, TXT, EPUB のようにファイルの拡張子で指定してください。本に指定した形式がない場合、なにもしません。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

show_metadata

calibredb show_metadata [options] id

id で指定された本の、calibreのデータベースに保存された書誌を表示します。id はsearch コマンドで取得できるID番号です。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--as-opf

OPF 形式 (XML) で書誌を表示

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

Set the metadata stored in the calibre database for the book identified by book_id from the OPF file metadata.opf. book_id is a book id number from the search command. You can get a quick feel for the OPF format by using the
--as-opf switch to the show_metadata command. You can also set the metadata of individual fields with the --field option. If you use the --field option, there is no need to specify an OPF file.

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--field, -f

フィールドに設定する値。field_name:value の形式で指定してください。例: --field tags:tag1,tag2。使用可能なフィールド名のリストを得るには --list-fields を使用してください。複数のフィールドの値を設定するためには、このオプションを複数指定してください。注意: 言語を指定する場合は ISO639言語コードを使用してください(英語は en、フランス語は fr 等)。識別子は以下の書式で指定してください --field identifiers:isbn:XXXX,doi:YYYYY。Boolean型(yes/no)のフィールドは、true, false またはyes, no で指定してください。

--list-fields, -l

--field オプションで使用可能な書誌フィールド名のリストを表示

export

calibredb export [options] ids

Export the books specified by ids (a comma separated list) to the filesystem.
The export operation saves all formats of the book, its cover and metadata (in an OPF file). Any extra data files associated with the book are also saved.
You can get id numbers from the search command.

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--all

IDのリストを無視してデータベースからすべての本をエクスポートします。

--dont-asciiize

ファイル名の英語以外の文字をすべて英語相当の文字に calibre に変換させます。Unicode ファイル名を完全にサポートしていない古いファイルシステム上に保存するときに役に立ちます。 このスイッチ指定すると、この挙動をOFFにできます。

--dont-save-cover

通常、calibreは表紙を電子書籍ファイルとは別のファイルに保存します。 このスイッチ指定すると、この挙動をOFFにできます。

--dont-save-extra-files

Save any data files associated with the book when saving the book このスイッチ指定すると、この挙動をOFFにできます。

--dont-update-metadata

通常、calibreはライブラリ内のファイルの書誌をアップデートしますが、これはディスクへの保存を遅くします。 このスイッチ指定すると、この挙動をOFFにできます。

--dont-write-opf

通常、calibreは電子書籍ファイル本体に付随する別個のOPFファイルに書誌を書き込みます。 このスイッチ指定すると、この挙動をOFFにできます。

--formats

各本を保存する時のカンマ区切りの形式のリスト。デフォルトではすべての存在する形式が保存されます。

--progress

進捗状況を表示

--replace-whitespace

空白をアンダースコアに置換

--single-dir

すべての書籍を1つのフォルダにエクスポート

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

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--ids, -i

カタログに対するカンマ区切りのデータベースID。 宣言する場合は、:option:`--search`は無視されます。 デフォルト: all

--search, -s

結果を検索文字列で絞り込みします。検索文字列の形式については、ユーザマニュアルの検索関連ドキュメントを参照してください。 デフォルト: 絞り込みなし

--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>','Tags','<comma-separated list of tags>') または ('<rule name>','<custom column>','<pattern>'). 使用例: (('Archived books','#status','Archived'),) 上記例では、カスタム列 'status' の値が 'Archived' の本を除外します。 ルールが複数指定されている場合には、すべて適用されます。 デフォルト: "(('Catalogs','Tags','Catalog'),)" 適用: AZW3, EPUB, MOBI出力形式

--generate-authors

カタログに 'Authors' セクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--generate-descriptions

カタログに 'Description' セクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--generate-genres

カタログに 'Genres' セクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--generate-recently-added

カタログに 'Recently Added' セクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--generate-series

カタログにシリーズセクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--generate-titles

カタログに 'Titles' セクションを含める。 デフォルト: 'False' 適用: AZW3, EPUB, MOBI出力形式

--genre-source-field

'Genre' セクション用のソースフィールド。 デフォルト: 'タグ' 適用: AZW3, EPUB, MOBI出力形式

--header-note-source-field

Descriptionヘッダに挿入するノートを格納したカスタムフィールド。 デフォルト: '' 適用: 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

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

add_custom_column

calibredb add_custom_column [options] label name datatype

カスタム列を作成。labelはマシン・フレンドリな列の名前。スペースやコロンを含めません。
nameは人間に読みやすい列の名前。
datatypeは以下のいずれか: bool, comments, composite, datetime, enumeration, float, int, rating, series, text

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--display

列のデータがどのように翻訳されるかをカスタマイズする辞書を指定するオプションです。JSON文字列で指定します。列挙型の列の場合、以下を使用してください:option:--display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" 。 値の表示方法には、様々なオプションがあります。列の型ごとのオプションは以下の通りです。 合成型: composite_template, composite_sort, make_category,contains_html, use_decorations 日付: date_format 列挙型: enum_values, enum_colors, use_decorations 整数、浮動小数点数: number_format テキスト: is_names, use_decorations 型とオプションの正しい組み合わせを知るには、GUIから適切な型のカスタム列を作成して、出力されるバックアップのOPFファイルを見てみるのが一番よい方法です(列を追加して以降にOPFが再生成されていることを確認してください)。OPFの新しい列に "display" というJSONが見つかるでしょう。

--is-multiple

この列はタグ状のデータ（例: 複数のカンマ区切りの値）を保持します。datatypeがtextの時のみ有効です。

custom_columns

calibredb custom_columns [options]

利用可能なカスタム列の一覧を表示。列ラベルとIDを表示します。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--details, -d

各列の詳細を表示

remove_custom_column

calibredb remove_custom_column [options] label

labelで指定されるカスタム列を削除。
custom_columns コマンドで存在するカスタム列を見ることができます。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--force, -f

確認を表示しない

set_custom

calibredb set_custom [options] column id value

idで指定された本のカスタム列の値を設定します。
search コマンドでIDのリストが取得できます。
custom_columns コマンドでカスタム列の名前のリストが取得できます。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--append, -a

もし列に複数の値を設定できる場合、値を指定した値に入れ替えるのではなく、すでにある値に付け加えます。

restore_database

calibredb restore_database [options]

calibreライブラリの各ディレクトリに保存されている OPF ファイルから書誌を読み取り、データベースを復元します。
これはmetadata.dbファイルが壊れてしまった時に有用です。

注意: このコマンドを実行すると、データベースを完全に再生成します。すべての保存済み検索、ユーザカテゴリ、書誌変換ルール、書誌ごとの変換設定、カスタムレシピは失われます。復元される書誌は、OPFファイルの内容と同等となります。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/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

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/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]

データベース中のカテゴリ情報のレポートを作成します。情報はタグブラウザに表示されるものと同等です。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--categories, -r

カンマ区切りのカテゴリの検索名リスト。デフォルト: all

--csv, -c

CSVで出力

--dialect

DSVファイルのタイプ。選択肢: excel, excel-tab, unix

--item_count, -i

カテゴリ内の項目別の数ではなく、カテゴリにある項目数のみを出力します。

--width, -w

出力ファイル中の、1行の最大の長さ。デフォルトは画面サイズから判別。

backup_metadata

calibredb backup_metadata [options]

データベースに格納されている書誌を、本フォルダにある個別の OPF ファイルにバックアップします。通常は自動的に行われますが、--all オプションをつけてコマンドを実行すれば、OPF ファイルを強制的に再生成することができます。

ただし普通はこれを行う必要はありません。OPF ファイルは書誌が変更されるたびに自動的にバックアップされるからです。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--all

通常このコマンドは、古くなったOPFファイルに対してのみ実行されます。このオプションを指定すると、すべての本に対して実行されます。

clone

calibredb clone path/to/new/library

現在のライブラリの複製を作成します。現在のライブラリと列、仮想ライブラリ、その他の設定が同じである、新しい空のライブラリが作成されます。

複製されたライブラリに本は含まれません。すべての本を含む複製を作成したい場合には、単にライブラリのフォルダをコピーしてください。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

embed_metadata

calibredb embed_metadata [options] book_id

calibreライブラリ内に保存されている電子書籍ファイルの書誌を calibreデータベースの書誌で更新します。
通常、電子書籍ファイルの書誌の更新は、エクスポート時のみ行われますが、このコマンドはすぐに更新を行いたい場合に便利です。
ファイル形式毎にサポートする書誌のフィールドが異なることに留意ください。
book_id に特別な値 'all' を指定すると、すべての本が更新されます。
また、スペース区切りで複数のidを指定すること、ハイフン区切りで範囲を指定することができます。
例: calibredb embed_metadata 1 2 10-15 23

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--only-formats, -f

指定した形式のファイルのみ書誌を更新します。複数の形式を指定する場合は、オプションを複数回指定してください。デフォルトでは、すべての形式が更新されます。

search

calibredb search [options] search expression

検索条件を指定してライブラリを検索し、結果を本のIDのカンマ区切りリストで返します。出力された結果は、本のIDを引数にとる他のコマンドの入力として使用するのに便利です。以下のようにcalibreの強力な検索用言語が使用できます。例: calibredb search author:asimov 'title:"i robot"'

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--limit, -l

返却する最大結果数。デフォルトはすべての結果。

fts_index

calibredb fts_index [options] enable/disable/status/reindex

全文検索用インデックス生成処理の設定

enable

このライブラリーの FTS インデックスを有効にする

disable

このライブラリーの FTS インデックスを無効にする

status

現在のインデックス化状態を表示

reindex

特定のブックまたはライブラリー全体の再インデックス化を行えます。特定ブックの再インデックス化の場合はブックのidを reindex コマンドの後ろに引数として付けてください。ブックid が指定されない場合はライブラリー全体が再インデックス化されます。

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--indexing-speed

The speed of indexing. Use fast for fast indexing using all your computers resources and slow for less resource intensive indexing. Note that the speed is reset to slow after every invocation.

--wait-for-completion

全てのブックのインデックス生成までお待ちください。インデックス化の進捗は定期的に表示されます

fts_search

calibredb fts_search [options] search expression

Do a full text search on the entire library or a subset of it.

スペースを含む引数を calibredb に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

--do-not-match-on-related-words

Only match on exact words not related words. So correction will not match correcting.

--include-snippets

Include snippets of the text surrounding each match. Note that this makes searching much slower.

--indexing-threshold

How much of the library must be indexed before searching is allowed, as a percentage. Defaults to 90

--match-end-marker

The marker used to indicate the end of a matched word inside a snippet

--match-start-marker

The marker used to indicate the start of a matched word inside a snippet

--output-format

The format to output the search results in. Either "text" for plain text or "json" for JSON output.

--restrict-to

Restrict the searched books, either using a search expression or ids. For example: ids:1,2,3 to restrict by ids or search:tag:foo to restrict to books having the tag foo.

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [オプション]

電子書籍をある形式から別の形式に変換します。

input_file には入力ファイル、output_file には出力ファイルを指定します。コマンドの最初の 2 つの引数として両方とも指定する必要があります。

出力する電子書籍の形式は、output_file のファイル拡張子から推測します。output_fileは特別な形式 EXT (EXT は出力ファイル拡張子) にすることもできます。この場合、出力ファイルの名前は入力ファイルの名前から導き出されます。ファイル名の先頭の文字にハイフンは使用できません。output_fileに拡張子がない場合にはフォルダとみなし、HTMLファイルにより構成される「open e-book」(OEB) がそのフォルダに書き込まれます。これらのファイルは通常、出力プラグインに渡されるためのものです。

入力ファイルと出力ファイルを指定した後に、さまざまなオプションを指定して変換をカスタマイズすることが可能です。どのオプションを使用できるかは、入力ファイルと出力ファイルの種類によって決まります。使用可能なオプションのヘルプを表示するには、入力ファイルと出力ファイルを指定した後に -h オプションを指定してください。

変換システムの詳しい説明は次を参照してください。電子書籍変換

スペースを含む引数を ebook-convert に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

指定できるオプションとデフォルト値は、入力形式と出力形式の両方に依存するので、以下で確認するようにしてください:

ebook-convert myfile.input_format myfile.output_format -h

下記のオプションは、ますすべての変換に共通するオプション、続いて入力形式と出力形式に固有のオプションです。

- 入力オプション

- 出力オプション

- 外観

- ヒューリスティック処理

- 検索と置換

- 構造の検出

- 目次

- 書誌

- デバッグ

--help, -h

このヘルプメッセージを表示して終了

--input-profile

入力プロファイルを指定する。入力プロファイルは、変換のためのシステム情報を提供し、様々な入力文書の情報をどのように解釈するかを指定する。たとえば、解像度に依存する長さ（つまり、ピクセルごとの長さ）などがあります。選択肢: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

ビルトイン・レシピ名の一覧を表示。以下のようにしてビルトイン・レシピから電子書籍を作成できます。: ebook-convert "Recipe Name.recipe" output.epub

--output-profile

出力プロファイルを指定します。出力プロファイルは変換システムに、指定したデバイス毎のドキュメントの最適化を指定します。特定のデバイス用の最適化を行うことも可能ですが、普通はそこまで必要にはならないでしょう。選択肢:cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, ipad, ipad3, irexdr1000, irexdr800, jetbook5, kindle, kindle_dx, kindle_fire, kindle_oasis, kindle_pw, kindle_pw3, kindle_scribe, kindle_voyage, kobo, msreader, mobipocket, nook, nook_color, nook_hd_plus, pocketbook_inkpad3, pocketbook_lux, pocketbook_hd, pocketbook_900, pocketbook_pro_912, galaxy, sony, sony300, sony900, sony-landscape, sonyt3, tablet, generic_eink, generic_eink_hd, generic_eink_large

--version

プログラムのバージョン番号を表示して終了

入力オプション

入力ファイル mobi の処理制御オプション

--input-encoding

入力された文書の文字エンコーディングを指定します。このオプションを設定した場合、文書内で宣言されるエンコーディングは無視されます。特に、文書がエンコーディング宣言を行っていない場合や、エンコーディング宣言に問題がある場合に有効です。

出力オプション
epub出力処理をコントロールするためのオプション

--dont-split-on-page-breaks

改ページでのファイル分割をしません。通常、入力ファイルは自動的に改ページの部分で2つのファイルに分割されます。これによりパースが速く、使用リソースの少ない電子書籍が出力されます。しかしながら分割作業は遅いので、ファイルに大量の改ページがある場合は改ページでの分割をしないでください。

--epub-flatten

このオプションはFBReaderJでEPUBを見る場合のみに必要になります。EPUB内のファイルのレイアウトをフラットにし、すべてのファイルをトップレベルに配置します。

--epub-inline-toc

インラインの目次を本の本文の一部として挿入します。

--epub-max-image-size

The maximum image size (width x height). A value of none means use the screen size from the output profile. A value of profile means no maximum size is specified. For example, a value of 100x200 will cause all images to be resized so that their width is no more than 100 pixels and their height is no more than 200 pixels. Note that this only affects the size of the actual image files themselves. Any given image may be rendered at a different size depending on the styling applied to it in the document.

--epub-toc-at-end

本の最初ではなく最後にインラインの目次を挿入します。

--epub-version

生成するEPUBファイルのバージョンです。互換性が最も高いのはEPUB2です。EPUB3は、どうしてもそれが必要とわかっているときに限り指定してください。

--extract-to

指定したフォルダに生成された EPUB ファイルの内容を解凍。フォルダの中身は最初に削除されるので、注意してください。

--flow-size

指定した容量(KB)以上のHTMLファイルを分割します。大抵のEPUBリーダは大きなファイルサイズを扱えないのでこのオプションは重要です。デフォルトの 260KBは Adobe Digital Editions 向けのサイズです。0を設定すると分割を無効にします。

--no-default-epub-cover

通常、入力ファイルに表紙がなく指定しなかった場合、タイトルや著者などからデフォルトの表紙が作成されます。このオプションでは表紙の生成を無効にします。

--no-svg-cover

SVGを本の表紙に使用しない。このオプションはiPhoneや JetBook LiteのようなSVGをサポートしないデバイスで使用する場合に使ってください。指定しないと、それらのデバイスでは表紙が白紙になってしまいます。

--preserve-cover-aspect-ratio

SVGの表紙を使う場合、このオプションは縦横比を保ったまま、表紙を画面のサイズに広げます。これにより表紙の両端や上下に白い部分ができる事がありますが、表紙は歪みません。このオプションを使わないと表紙がいくらか歪みますが白い部分はできません。

--pretty-print

指定した場合は、出力プラグインはできる限り人が読めるような出力を生成しようとします。一部の出力プラグインには、なにも効果がない可能性があります。

--toc-title

すべての生成されたインラインの目次に使われるタイトル。

外観

出力の外観を調整するためのオプション

--asciiize

Unicode文字をASCII表記に翻訳します。Unicode文字がASCIIで置き換わることになるため注意して使ってください。例えば "Pelé" は "Pele"に置換されます。また文字に複数の表記がある場合（例えば中国語と日本語で共有されている文字）、表記は現在のcalibreのインタフェース言語が使われます。

--base-font-size

フォントサイズの単位はptです。作成された本の中のすべてのフォントサイズは、ここで指定したフォントサイズに従って再指定されます。大きいサイズを指定すれば出力されたファイル中のフォントが大きくなり、逆に小さいサイズを指定すれば小さくなります。デフォルトの値である0を指定した場合には、ベースとするフォントサイズは指定した出力プロファイルに従って自動的に選択されます。

--change-justification

テキスト揃えを変更します。「左」に設定すると揃えるべきすべてのテキストを左寄せにします。「揃える」にすると揃っていないすべてのテキストを揃えます。「オリジナル」（デフォルト）にすると元ファイルの揃えを変えません。注: 一部の形式のみで揃えがサポートされている事に注意してください。

--disable-font-rescaling

すべてのフォントサイズの拡大縮小を止める

--embed-all-fonts

入力ドキュメントから参照されているフォントで、まだ埋め込まれていないフォントをすべて埋め込みます。この機能は、システム上のフォントを検索し、見つかればそれを埋め込みます。フォントの埋め込みは、変換先の形式が埋め込みをサポートしている場合、例えば EPUB, AZW3, DOCX, PDF の場合にしか機能しません。ドキュメントにフォントを埋め込むための、適切なライセンスを所有していることを確認してください。

--embed-font-family

書籍に指定したフォントを埋め込みます。これは書籍に使われる「ベース」フォントを指定します。もし入力ドキュメントがフォントを指定していた場合には、それがベースフォントより優先されます。（そうしたくない場合には、スタイル情報絞り込みを使って、入力ドキュメントからフォントを取り除いてください）。注意：フォント埋め込みはEPUB, AZW3, DOCX のようないくつかの形式でしか機能しません。

--expand-css

デフォルトで calibre は、CSSのmargin, padding, border, 等を指定する際に、短縮形式を使用します。このオプションを有効にすると、短縮形式の代わりにすべて展開された形式を使用します。NookプロファイルはNookが短縮形式を取り扱えないため、この設定にかかわらず常に展開された形式を使用する設定になっていることに留意ください。

--extra-css

CSSスタイルシートへのパスかCSS自身を指定します。このCSSは入力ファイルのスタイル･ルールに追加されるので、既存のルールをオーバーライドすることもできます。

--filter-css

CSSスタイルルールから削除する、すべてのCSSプロパティのカンマ区切りのリスト。一部のスタイル情報があるとデバイスが受け付けない場合に便利です。例：font-family,color,margin-left,margin-right

--font-size-mapping

CSSフォント名からポイントでのフォントサイズへのマッピング。（例：12,12,14,16,18,20,22,24）xx-small から xx-largeまでのマッピングで、最後のサイズになると巨大なフォントになります。フォントの再変換アルゴリズムはこれらのサイズを賢く変換します。デフォルトでは、選択した出力プロファイルに基づいたマッピングが使用されます。

--insert-blank-line

段落の間に空行を挿入します。入力ファイルが段落（<p>や<div>タグ）を使っていない場合は動作しません。

--insert-blank-line-size

挿入される空行の高さを設定（単位em）。段落の間の行高さはこの設定値の倍の高さになります。

--keep-ligatures

入力ドキュメント内の合字をそのままにします。合字とは2つの文字を組み合わせて表現する特別な文字です。（例：ff,fi,fl等）大半のリーダは標準のフォントで合字をサポートしないので、これらが正しく表示される可能性は低いです。デフォルトではcalibreは合字を2つの通常文字に変換します。このオプションはそれらをそのままにします。

--line-height

ポイントでの行の高さ。連続するテキストの行と行の間のスペースを調整します。行の高さ情報のないエレメントにのみ適用されます。たいていの場合には最低の行の高さの設定のほうが便利でしょう。デフォルトではこの行の高さの設定はありません。

--linearize-tables

一部のデザインの良くない文書では、ページのテキストレイアウトにテーブルを使用しているものがあります。こういった文書を変換すると、ページがはみ出したり乱れることがよくあります。このオプションを使うと、テーブルから内容を抜き出して直線的な配置に置き換えます。

--margin-bottom

下の余白をpt単位で指定。デフォルトは5.0。この値に0以下の数値を指定すると余白を設定しません (オリジナルのドキュメント内に設定された余白を使用します)。注: PDFやDOCXなど固定ページ形式では、その形式での指定が優先します。

--margin-left

左の余白をpt単位で指定。デフォルトは5.0。この値に0以下の数値を指定すると余白を設定しません (オリジナルのドキュメント内に設定された余白を使用します)。注: PDFやDOCXなど固定ページ形式では、その形式での指定が優先します。

--margin-right

右の余白をpt単位で指定。デフォルトは5.0。この値に0以下の数値を指定すると余白を設定しません (オリジナルのドキュメント内に設定された余白を使用します)。注: PDFやDOCXなど固定ページ形式では、その形式での指定が優先します。

--margin-top

上の余白をpt単位で指定。デフォルトは5.0。この値に0以下の数値を指定すると余白を設定しません (オリジナルのドキュメント内に設定された余白を使用します)。注: PDFやDOCXなど固定ページ形式では、その形式での指定が優先します。

--minimum-line-height

計算されたフォントサイズのパーセントに基づく、最小の行の高さです。入力文章の指定にかかわらず、calibreはすべてのエレメントで最低限この行の高さになることを保障します。ゼロにすると無効になります。デフォルトは120％です。この意味がよくわかっている場合にのみ、設定からこの行の高さの設定を直接指定してください。例えば、「2倍の高さ」を指定する場合には設定を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 GUI 上でウィザードを使用してルールを作成することです。外観 -> HTML 変換セクションで変換ダイアログを開いてください。ルールを作成した後は「エクスポート」ボタンを使ってファイルに保存できます。

--unsmarten-punctuation

ファンシークオート、ダッシュ、省略記号をそれぞれの標準的に対応する物に変換する。

ヒューリスティック処理

共通のパターンを使ってドキュメントのテキストと構造を変更します。デフォルトでは無効になっています。 --enable-heuristicsを使って有効にします。個々のアクションは、--disable-* オプションで無効にすることができます。

--disable-dehyphenate

ドキュメント中のハイフネーションされた単語を分析します。ドキュメント自身を辞書として使い、ハイフンをそのままにするか取り除くかを決定します。

--disable-delete-blank-paragraphs

2つの段落に挟まれた、空白の段落を取り除きます。

--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.4で、真ん中より少し小さいあたりです。もし少しの行しか折り返しする必要が無い場合にはこの値を下げてください。

--replace-scene-breaks

シーン区切りを指定したテキストで置き換えます。デフォルトでは入力ドキュメントからのテキストが使われます。

検索と置換

ユーザ定義のパターンを使って文章や構造を変更します。

--search-replace

検索と置換の正規表現を含むファイルのパスを指定します。ファイルの内容は、検索の正規表現の行と、置換パターン(置換パターンは空行も可)の行が交互になるようにしてください。正規表現はPythonの文法に従い、ファイルのエンコーディングは UTF-8 でなくてはいけません。

--sr1-replace

検索1-検索パターンから置き換わる、置換文字列

--sr1-search

検索1-置換文字列と置き換えられる、検索パターン（正規表現）

--sr2-replace

検索2-検索パターンから置き換わる、置換文字列

--sr2-search

検索2-置換文字列と置き換えられる、検索パターン（正規表現）

--sr3-replace

検索3-検索パターンから置き換わる、置換文字列

--sr3-search

検索3-置換文字列と置き換えられる、検索パターン（正規表現）

構造の検出

ドキュメント構造の自動判別を制御

--chapter

章タイトルを検出するためのXPathの式。デフォルトでは、<h1> または <h2> タグのうち "chapter", "book", "section", "prologue", "epilogue", "part" を含むもの、および class="chapter" のものを章タイトルとします。使用する式は、エレメントのリストを評価するものでなくてはなりません。章の検出を無効にするには、"/" 式を使用します。XPathのチュートリアルは、calibreのユーザマニュアルを参照してください。

--chapter-mark

見つかった章をどのようにマーキングするか指定します。「改ページ」を選ぶと章の前に改ページを挿入します。「ルール」を選ぶと章の前に行を挿入します。「なし」を選ぶとマーキングを行いません。「両方」を選ぶと改ページと行の両方を挿入します。

--disable-remove-fake-margins

いくつかの文章は、各段落に左右のマージンを指定することで、ページのマージンを指定しています。calibreはそれらのマージンを見つけて削除しようとしますが、たまに、これが原因でマージン全体がなくなってしまうことがあります。そのような場合、削除を無効にすることができます。

--insert-metadata

本の書誌を本の最初に奥付として挿入する。これは電子書籍リーダが、書誌を直接表示したり検索したりといった機能を持たない場合に有効です。

--page-breaks-before

XPath式を指定します。XPathで指定された要素の直前にページブレークが挿入されます。無効にするには、式: / を使用してください。

--prefer-metadata-cover

指定した表紙よりもソースファイルから検知した表紙を優先して使います。

--remove-first-image

入力電子書籍から最初の画像を取り除きます。入力ドキュメントに表紙として認識されない表紙がある場合に有用です。その際、calibreで表紙を指定すると表紙が2つになってしまうのを、このオプションを指定することで避けることができます。

--start-reading-at

読書を開始する場所を指定するXPath式です。いくつかの電子書籍リーダ(最も典型的なにはKindle)は、本を開いたときに、ここで指定した場所から開きます。この機能を使用するより詳しい情報は、calibreのユーザマニュアルのXPathチュートリアルを参照してください。

目次

目次の自動生成をコントロールします。デフォルトでは、入力ファイルに目次があった場合に優先的に自動生成されます。

--duplicate-links-in-toc

もし目次を入力ドキュメントのリンクから作成する場合、重複するエントリーを許す。例えば、1つ以上のリンクが同じテキストで、違う場所を指している場合など。

--level1-toc

目次のレベル 1 に追加するタグをすべて指定するXPath式です。これが指定された場合、他の目次の自動検出により優先されます。XPathのチュートリアルは、calibreのユーザマニュアル等を参照してください。

--level2-toc

目次のレベル 2 に追加するタグをすべて指定するXPath式です。指定された項目は、直前のレベル 1 の項目の下に追加されます。XPathのチュートリアルは、calibreのユーザマニュアル等を参照してください。

--level3-toc

目次のレベル 3 に追加するタグをすべて指定するXPath式です。指定された項目は、直前のレベル 2 の項目の下に追加されます。XPathのチュートリアルは、calibreのユーザマニュアル等を参照してください。

--max-toc-links

目次に入れられる最大のリンク数。0にすると無限になります。デフォルト: 50 。リンクは、最低の章の数の設定以下の場合にのみ、追加されるます。

--no-chapters-in-toc

目次に自動判定された章を追加しない

--toc-filter

目次から削除する項目を指定するための正規表現による指定。マッチした項目とその子項目は削除されます。

--toc-threshold

この数以下の章が見つかった場合、目次にリンクが追加されます。デフォルト: 6

--use-auto-toc

元のファイルに目次がある場合、通常、設定に従い自動生成されます。このオプションは常に自動生成を行います。

書誌

出力の書誌の設定オプション

--author-sort

著者で並び替える時に使う文字列。

--authors

著者を設定します。複数の著者はアンド記号でつなげてください。

--book-producer

本の製作者を設定します。

--comments

電子書籍の説明を設定

--cover

表紙を指定したファイルまたはURLに設定する

--isbn

本のISBNコードを設定します。

--language

言語を設定します。

--pubdate

発行日を設定 (タイムゾーンが明示的に指定されない限り、ローカルなタイムゾーンとみなします)

--publisher

電子書籍の発行者を設定

--rating

１～５の番号で、評価を設定します。

--read-metadata-from-opf, --from-opf, -m

指定したOPFファイルから書誌を読み込む。このファイルから読み込んだ書誌は、ソースファイルの書誌を上書きします。

--series

この本のシリーズ名を指定

--series-index

このシリーズの本の巻数を設定します。

--tags

本にタグを設定します。カンマ区切りのリストである必要があります。

--timestamp

本のタイムスタンプを設定（他では一切使われません）

--title

タイトルの設定

--title-sort

タイトルのバージョンを並び替えに使う。

デバッグ

変換をデバッグする補助用のオプション

--debug-pipeline, -d

変換パイプラインのさまざまな段階からの出力を指定されたフォルダーに保存します。変換プロセスのどの段階でバグが発生しているかわからない場合に便利です。

--verbose, -v

デバッグ出力の量のレベル指定します。複数回指定すると、出力量が増えます。2回指定すると出力量が最大になります。1回が中程度の出力量で、指定しなければ一番少ない出力量になります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- ebook-edit

ebook-edit

ebook-edit [opts] [path_to_ebook] [name_of_file_inside_book ...]

calibreの書籍編集ツールを起動します。
オプションで、本の内部のファイルを指定して、編集のため自動的に開かれるようにすることができます。

スペースを含む引数を ebook-edit に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--detach

制御端末(あれば)から分離 (Linuxのみ)

--help, -h

このヘルプメッセージを表示して終了

--select-text

本を編集用に開いたときに本の中で選択するテキスト

--version

プログラムのバージョン番号を表示して終了

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

異なるファイル形式は異なる種類の書誌をサポートします。
そのファイル形式がサポートしていない書誌を設定しようとすると、書誌は単純に無視されます。

スペースを含む引数を ebook-meta に渡す場合には、クォーテーションマークでくくってください。例: "/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:http://acme.com --identifier isbn:12345 識別子削除する場合は、値なしを指定 --identifier isbn: EPUBファイルでは、パッケージ識別子としてマークされた識別子は削除できないことに留意してください。

--index, -i

このシリーズの本の巻数を設定します。

--isbn

本のISBNコードを設定します。

--language, -l

言語を設定します。

--lrf-bookid

BookIDをLRFファイルに設定

--publisher, -p

電子書籍の発行者を設定

--rating, -r

１～５の番号で、評価を設定します。

--series, -s

この本のシリーズ名を指定

--tags

本にタグを設定します。カンマ区切りのリストである必要があります。

--title, -t

タイトルの設定

--title-sort

整列に使う別タイトル。指定しない場合、タイトル情報があればそこから自動生成されます。

--to-opf

OFP ファイルの名前を指定。書誌は OPF ファイルに書き出されます。

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- ebook-polish

ebook-polish

ebook-polish [options] input_file [output_file]

本の最適化(polishing) とは、要するにあなたが端正こめて作成した電子書籍を、ぴかぴかに磨き上げることです。
本の最適化は、あなたの電子書籍への変更を最小限にとどめるよう動作します。本の変換と異なり、CSSのフラット化、ファイル名の変更、フォントサイズの変更、余白の調整等を、行いません。求められる効果に対して、最小限の変更のみを行います。

本の最適化は、本の作成プロセスの最終段階で使用すべきものです。

本の最適化は、AZW3 or EPUB 形式に対してのみ動作することに留意してください。

スペースを含む引数を ebook-polish に渡す場合には、クォーテーションマークでくくってください。例: "/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

使用されていないCSSルールをスタイルシートと<style> tags.タグから削除します。テンプレートから作成された本のような場合、使われていないCSSルールが大量にあることがあります。そのような不要なルールは、すべてパースする必要があるため、リーダの動作を遅くする場合があります。

--smarten-punctuation, -p

プレーンテキストのダッシュ、省略記号、引用符、連続するハイフンなどを、 タイポグラフィー的に正しいとされる同等物に変換します。 変換のアルゴリズムは、時々間違った結果を生成するので注意してください。 特にシングルクォートが短縮形の単語の先頭にくるものを含んでいる場合、注意が必要です。

--subset-fonts, -f

フォントのサブセット化とは、埋め込みフォントを実際に本の中で 使用されている文字だけを含むようにすることです。これにより、 フォントのサイズを大きく削減することができます(半分になることもよくあります)。 例えば本が見出しに特定のフォントを使用していた場合、サブセット化により 実際に見出しで使用されている文字のみを含むよう削減することができます。 また、太字やイタリックのフォントを埋め込んでいるもののほとんど またはまったく使われていない場合、サブセット化により太字やイタリックの フォントををほんの数文字だけにしたり完全に削除したりできます。 サブセット化の唯一の欠点は、後で本にテキストを追加した場合に、追加したテキストがサブセット化されたフォントに含まれていない文字を使うことがあり得る、と言う点です。

--upgrade-book, -U

可能であればブックの内部構造をアップグレードします。例えば EPUB 2 ブックを EPUB 3 ブックにアップグレードします。

--verbose

より多くのメッセージを出力します。デバッグに便利です。

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- ebook-viewer

ebook-viewer

ebook-viewer [options] file

電子書籍を表示。

スペースを含む引数を ebook-viewer に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--continue

最後に開いた本から読書を再開

--detach

制御端末(あれば)から分離 (Linuxのみ)

--force-reload

現在開いているすべての本を強制的に再読み込み

--full-screen, --fullscreen, -f

指定した場合は、ビューアーウィンドウは起動時に全画面表示しようとします。

--help, -h

このヘルプメッセージを表示して終了

--new-instance

Open a new viewer window even when the option to use only a single viewer window is set

--open-at

The position at which to open the specified book. The position is a location or position you can get by using the Go to->Location action in the viewer controls. Alternately, you can use the form toc:something and it will open at the location of the first Table of Contents entry that contains the string "something". The form toc-href:something will match the href (internal link destination) of toc nodes. The matching is exact. If you want to match a substring, use the form toc-href-contains:something. The form ref:something will use Reference mode references. The form search:something will search for something after opening the book. The form regex:something will search for the regular expression something after opening the book.

--raise-window

指定した場合は、ビューアーウィンドウは起動時に前面へ表示しようとします。

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [options]

オンラインのソースから書誌を取得します。title, authors, ISBN のうち、少なくとも1つを指定してください。

スペースを含む引数を fetch-ebook-metadata に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--allowed-plugin, -p

使用する書誌ダウンロードプラグインの名前を指定。デフォルトでは、すべての書誌プラグインが仕様されます。複数のプラグインを指定するために複数個指定可能です。使用可能なプラグインは、Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search です。

--authors, -a

本の著者

--cover, -c

ファイル名を指定。もし表紙が取得できた場合、そのファイルに保存されます。このオプションを指定しなかった場合、表紙はダウンロードされません。

--help, -h

このヘルプメッセージを表示して終了

--identifier, -I

ASIN や Goodreads ID のような ID。複数の ID を複数回指定することができます。例: --identifier asin:B0082BAJA0

--isbn, -i

本のISBN

--opf, -o

書誌を人間の読みやすいテキストではなく、OPF 形式で出力します。

--timeout, -d

タイムアウト(秒)。デフォルトは30

--title, -t

本のタイトル

--verbose, -v

ログをコンソールに出力します(標準エラー)

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- lrf2lrs

lrf2lrs

lrf2lrs book.lrf

LRFファイルをLRS (XML UTF-8 encoded)ファイルに変換します

スペースを含む引数を lrf2lrs に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--dont-output-resources

埋め込まれているイメージとフォント・ファイルをディスクに保存しません。

--help, -h

このヘルプメッセージを表示して終了

--output, -o

LRSファイルの出力

--verbose

出力メッセージを増やす

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- lrfviewer

lrfviewer

lrfviewer [options] book.lrf

LRFの電子書籍 book.lrf を読みます

スペースを含む引数を lrfviewer に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--disable-hyphenation

ハイフネーションを無効にします。描画の速度が大きく向上するはずです。

--help, -h

このヘルプメッセージを表示して終了

--profile

LRF描画をプロファイルします

--verbose

描画処理についての情報のより多く出力します

--version

プログラムのバージョン番号を表示して終了

--visual-debug

描画エンジンデバッグ用の追加情報表示を有効にします

--white-background

デフォルトの背景は私が目に優しいと思うオフホワイトです。このオプションを指定すると背景色が完全な白になります。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- lrs2lrf

lrs2lrf

lrs2lrf [options] file.lrs

LRSファイルをLRFファイルにコンパイルします。

スペースを含む引数を lrs2lrf に渡す場合には、クォーテーションマークでくくってください。例: "/some path/with spaces"

[オプション]

--help, -h

このヘルプメッセージを表示して終了

--lrs

LRSをLRSに変換します。デバッグに便利です。

--output, -o

出力ファイルのパス

--verbose

処理中のメッセージを出力

--version

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- コマンドライン・インタフェース »
- web2disk

web2disk

web2disk URL

URLは、例えば https://google.com のように指定します。

スペースを含む引数を web2disk に渡す場合には、クォーテーションマークでくくってください。例: "/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

プログラムのバージョン番号を表示して終了

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- calibre 開発環境の構築

calibre 開発環境の構築
calibre は完全にオープンソースであり、 GNU GPL v3 [https://www.gnu.org/licenses/gpl.html] のもとにライセンスされています。これは自由にプログラムをダウンロードして、思う存分変更できることを意味します。この章では、選んだ OS 上で calibre の開発環境を構築するにはどうすればよいかを学べます。calibre は主に `Python <https://www.python.org>`_で書かれており、一部は速度とシステムインタフェースのために C/C++ のコードで書かれています。calibre には Python 3.8 以上が必要です。

目次

- 設計の哲学

- コードレイアウト

- コードを入手

- 変更を取り入れてもらうために投稿

- Windows 開発環境

- macOS 開発環境

- Linux 開発環境

- "通常" と "開発用" の calibre を同じコンピュータ上に別々にインストール

- デバッグのコツ

- print 文を使用

- インタラクティブな Python インタプリタの利用

- Python デバッガをリモートデバッガとして使用

- 好みの Python IDE でデバッガを使用

- calibre の Python 環境で任意のスクリプトを実行

- プロジェクトで calibre を使用

- calibre のバイナリインストール

- Linux 上のソースインストール

- calibre のパーツの API 説明書

設計の哲学
calibre のルーツは UNIX の世界にあります。つまり、その設計は高度にモジュール化されています。モジュールは明確に定義されたインタフェースを介して互いにコミュニケーションをとります。これによって calibre に新しい機能を追加してバグを修正することが容易になり、開発のペースが大きく向上します。このルーツのおかげで、calibre にはすべての機能に対して包括的なコマンドラインが備わっています。コマンドラインについては、コマンドライン・インタフェース を参照してください。

calibre のモジュール化された設計は、プラグイン の形で表されます。calibre のプラグイン作成に関しては チュートリアル があります。たとえば calibre に新しいデバイスを追加するためには、通常はデバイスドライバプラグインの形で 100 行未満くらいのコードを書く必要があります。 ビルトインドライバ [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices] を参照できます。同様に、新しい形式への変換をサポートしたい場合にも、その形式の入力/出力プラグインを書く必要があります。モジュール化された設計のもうひとつの例は、ニュース取得の レシピシステム です。calibre に機能追加するために設計されたプラグインのその他の例については、`プラグイン目録 <https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767>`_を参照してください。

コードレイアウト

All the calibre Python code is in the calibre package. This package contains the following main sub-packages

- devices - すべてのデバイスドライバ。ビルトインドライバのいくつかを調べて、どのように機能するかを調べてみてください。

- 詳しくは、デバイスドライバがサポートするインタフェースを定義する devices.interface および USBMS デバイスに接続する汎用ドライバを定義する devices.usbms を参照してください。calibre のすべての USBMS ベースのドライバは、これを継承しています。

- e-books - すべての電子書籍変換/書誌のコード。ebook-convert コマンドを起動するモジュールである calibre.ebooks.conversion.cli から始めるとよいでしょう。変換処理は conversion.plumber を介して行われます。形式に依存しないコードはすべて ebooks.oeb にあり、形式依存のコードは ebooks.format_name にあります。

- 書誌の読み書き、およびダウンロードはすべて ebooks.metadata にあります。

- 変換はパイプラインで行われます。パイプラインの構造については はじめに を参照してください。パイプラインは、入力プラグイン、種々の変換、および出力プラグインで構成されています。パイプラインを構築して動かすコードは plumber.py にあります。パイプラインは電子書籍の中身に対して機能します。中身とは、たとえばマニフェスト、スパイン、目次、ガイド、HTML コンテンツなどを含む、解凍された EPUB のことです。この中身を管理するクラスは、ebooks.oeb.base にある OEBBook です。変換中に本に対して施される種々の変形は、oeb/transforms/*.py にあります。また入力および出力プラグインは conversion/plugins/*.py にあります。

- 電子書籍の編集は、別のコンテナオブジェクトを用いて行われます。これは 電子書籍編集ツール API 説明書 に記載されています。

- db - データベースのバックエンド。calibre ライブラリへのインタフェースは データベースの API 説明書 を参照してください。

- コンテンツサーバ: 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 構造体を確認してください。

コードの理解に手助けが必要な場合には、 開発フォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=240] に投稿してください。おそらく数多くの calibre 開発者の中から誰かが手を貸してくれるでしょう。

コードを入手
calibre のソースコードを入手する方法は 2 つあります。バージョン管理システムを利用する方法と、直接 tarball [https://calibre-ebook.com/dist/src] をダウンロードする方法です。

calibre は分散バージョン管理システムである Git [https://www.git-scm.com/] を使用しています。Git は calibre がサポートしているすべてのプラットフォーム上で利用可能です。Git をインストールした後、次のコマンドで calibre のソースコードを入手できます:

git clone https://github.com/kovidgoyal/calibre.git

Windows では C:\Program Files\Git\git.exe のような完全なパス名が必要です。

calibre は非常に大規模なプロジェクトであり、ソース管理の履歴も長いため、上記には時間がかかる場合があります (インターネットの速度に応じて 10 分から 1 時間)。

コードの入手に時間をかけたくない場合は、最新リリースのソースコードはいつでも archive [https://calibre-ebook.com/dist/src] で入手可能です。

ブランチを最新のコードに更新するには、次のコマンドを使用します:

git pull --no-edit

コードを GitHub [https://github.com/kovidgoyal/calibre] で参照することもできます。

変更を取り入れてもらうために投稿

もしいくつかのちょっとした変更を行いたいだけなら、変更を加えてマージディレクティブを作成し、それを calibre の バグトラッカー [https://bugs.launchpad.net/calibre] のチケットに添付することができます。それを行うには、変更を加えてから、次のように実行します:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

これは現在のフォルダに my-changes ファイルを作成し、calibre バグトラッカー [https://bugs.launchpad.net/calibre] のチケットにそれを添付します。ただしこれは、コミットしたもの すべて を含めます。一部のコミットのみを送信したい場合には、上の origin/master を変更する必要があります。最後のコミットのみを送信するには、次のようにします:

git format-patch HEAD~1 --stdout > my-changes

最後の n 個のコミットを送信するには、1 を n に変更します。たとえば最後の 3 コミットの場合には次のようにします:

git format-patch HEAD~3 --stdout > my-changes

HEAD~n を使用するときにはマージを含めないよう注意してください。

calibre で多くの開発を行うつもりであれば、最良の方法は GitHub [https://github.com] アカウントを作成することです。下に示す基本的なガイドに従ってフォークを作成すれば、メインの calibre リポジトリにプルリクエストを送れるようになります:

- 次の記事に従ってマシン上に Git を準備します: Setup Git [https://help.github.com/articles/set-up-git]

- GitHub の認証用に SSH の鍵を 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

上の <username> は GitHub のユーザ名で置き換えてください。これでフォークがローカルにチェックアウトされます。

- いつでも好きなときに変更を加えてコミットできます。作業したものがマージできるようになったら、次のようにします:

git push

そして https://github.com/<username>/calibre に行き、Pull Request ボタンをクリックしてマージできるようプルリクエストを発行します。

- 次の手順を実行することで、メインのリポジトリからのコードでローカルコピーを更新できます:

git pull upstream

また、 開発フォーラム [https://www.mobileread.com/forums/forumdisplay.php?f=240] にも注目しておくべきです。大きな変更を加える前に、フォーラムで話し合うか、または直接 Kovid に連絡してください。 (メールアドレスはソースコードのいたるところに埋めこまれています)

Windows 開発環境

注釈

上で説明したように calibre のソースコードを別に入手する必要があります。

Windows インストーラ [https://calibre-ebook.com/download_windows] を使用して通常どおりにインストールします。次にコマンドプロンプトを開き、先にチェックアウトしておいた calibre のコードに移動します。例:

cd C:\Users\kovid\work\calibre

calibre は src と resources サブフォルダを含むフォルダです。

次のステップは、環境変数 CALIBRE_DEVELOP_FROM に src フォルダの絶対パスを設定することです。上の例では C:\Users\kovid\work\calibre\src になります。Windows 上での環境変数の設定方法は、 こちらのガイド [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] を参照してください。

環境変数を設定したらコマンドプロンプトを開き、正しく設定されていることを確認するために次のコマンドを実行します:

echo %CALIBRE_DEVELOP_FROM%

この環境変数を設定すると、calibre はすべての Python コードを指定された場所からロードするようになります。

これでおしまいです。calibre コードのハックを開始できるようになりました。たとえば src\calibre\__init__.py ファイルを好みのエディタで開き、次の行をファイルの先頭近くに追加します:

print("Hello, world!")

次に calibredb コマンドを実行します。最初の行の出力は Hello, world! となるでしょう。

必要に応じて calibre 開発環境を無償の Microsoft Visual Studio にセットアップすることも可能です。 こちら [https://www.mobileread.com/forums/showthread.php?t=251201] の手順に従ってください。

macOS 開発環境

注釈

上で説明したように calibre のソースコードを別に入手する必要があります。

提供された .dmg [https://calibre-ebook.com/download_osx] を使用して通常どおりに calibre をインストールします。次にターミナルを開いて、先にチェックアウトしておいた calibre のコードのフォルダに移動します。例:

cd /Users/kovid/work/calibre

calibre は src と resources サブフォルダを含むフォルダです。calibre のコマンドラインツールは calibre アプリのバンドルの中の /Applications/calibre.app/Contents/MacOS にあります。コマンドラインツールを簡単に実行するには、このフォルダを環境変数 PATH に追加する必要があります。

次のステップは、calibre をデバッグモードで実行するときに環境変数 CALIBRE_DEVELOP_FROM に src フォルダの絶対パスを設定するための bash スクリプトを作成することです。

プレーンテキストファイルを作成します:

#!/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 上で開発されています。開発環境の構築には、2 つの選択肢があります。calibre のバイナリを通常どおりインストールして、それを開発用のランタイム環境として使用することができます。この手法は Windows や macOS で使われるものとよく似ています。もうひとつの方法は、calibre をソースからインストールすることです。ソースから開発環境を構築する手順はソースツリーの中の INSTALL ファイルにあります。ここではバイナリをランタイムとして使用する方法を取り上げます。こちらが推奨される方法です。

バイナリインストーラ [https://calibre-ebook.com/download_linux] を使用して calibre を通常どおりにインストールします。次にターミナルを開き、先にチェックアウトしておいた calibre のコードのフォルダに移動します。例:

cd /home/kovid/work/calibre

calibre は src と resources サブフォルダを含むフォルダです。

次のステップは、環境変数 CALIBRE_DEVELOP_FROM に src フォルダの絶対パスを設定することです。上の例では /home/kovid/work/calibre/src になります。環境変数の設定方法は、Linux のディストリビューションや使用しているシェルによって異なります。

注釈

上流で提供されたバイナリインストーラを使用することを推奨します。どうしても自分のディストリビューションで提供するパッケージを使用したい場合には、 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 のソースツリーは非常に安定していて壊れることは滅多にありません。しかしソースから実行したものはテスト用のライブラリに使用し、日常的に使用するライブラリにはリリース版の calibre を使用する必要があると感じる場合には、calibre を起動する .bat ファイルやシェルスクリプトを使うことで簡単に実現できます。下の例では、Windows 上で .bat ファイルを使ってこれを行う手順を紹介します (他のプラットフォームでの手順も同じです。.bat ファイルの代わりにシェルスクリプトを使用してください)

日常的に使用するライブラリ用にリリース版の calibre を起動するには次のようにします:

calibre-normal.bat:

calibre.exe "--with-library=C:\path\to\everyday\library folder"

calibre-dev.bat:

set CALIBRE_DEVELOP_FROM=C:\path\to\calibre\checkout\src
calibre.exe "--with-library=C:\path\to\test\library folder"

デバッグのコツ

Python は動的型付けの言語であり、内省のための優れた機能を備えています。Kovid は calibre の中核となるコードを一度もデバッガを使用せずに書き上げました。calibre のコードをデバッグするには多くの戦略があります:

print 文を使用

Kovid のお気に入りのデバッグ方法は、これです。気になる場所に print 文を挿入し、プログラムをターミナルから実行するだけです。たとえば GUI をターミナルから次のように開始します:

calibre-debug -g

同様にして、電子書籍ビューアは次にように開始します:

calibre-debug -w /path/to/file/to/be/viewed

The e-book editor can be started as:

calibre-debug --edit-book /path/to/be/edited

インタラクティブな Python インタプリタの利用

次に示す 2 行のコードを挿入すると、その場所でインタラクティブな Python セッションを開始できます:

from calibre import ipython
ipython(locals())

コマンドラインから実行すると、これはローカルに定義された変数 (ローカルスコープ内の変数) にアクセスできるインタラクティブな Python インタプリタを開始します。インタラクティブプロンプトはオブジェクトプロパティの タブ 補完機能も備えており、さまざまな Python 機能を dir(), type(), repr() などの内省に使用できます。

Python デバッガをリモートデバッガとして使用

ビルトインの Python デバッガ (pdb) をリモートデバッガとしてコマンドラインから使用することができます。まず、次のように calibre コードの中の気になる場所でリモートデバッガを開始します:

from calibre.rpdb import set_trace
set_trace()

次に calibre を実行します。通常どおりに実行しても、または前の章で説明したようにcalibre-debug コマンドのひとつを使用してもかまいません。コード内の上記の場所に到達したら、calibre はフリーズしてデバッガの接続を待ちます。

次にターミナルまたはコマンドプロンプトを開き、デバッグセッションを開始するために次のコマンドを使用します:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Python デバッガの使い方は Python stdlib docs for the pdb module [https://docs.python.org/library/pdb.html#debugger-commands] を読んでください。

注釈

デフォルトではリモートデバッガはポート 4444 に接続しようとします。ポート番号をパラメータとして set_trace() および cli() 関数の両方に渡してやると、使用するポートを変更できます。set_trace(port=1234) や cli(port=1234) のように指定します。

注釈

Python デバッガは複数スレッドを処理できないため、スレッドごとに set_trace を呼び出してそれぞれ別のポート番号を指定してやる必要があります。

好みの Python IDE でデバッガを使用

リモートデバッグをサポートしていれば、お気に入りの IDE でビルトインデバッガを使用することも可能です。最初のステップは、calibre の src を IDE の PYTHONPATH にチェックアウトすることです。別の言葉で言うと、上で CALIBRE_DEVELOP_FROM に設定したフォルダを、IDE の PYTHONPATH にする必要があります。

次に IDE のリモートデバッガモジュールを、チェックアウトした calibre ソースコードの src サブフォルダに配置します。calibre の気になる場所、たとえば main 関数でリモートデバッガを起動するのに必要なコードを追加します。通常の方法で calibre を実行します。これで IDE は calibre の内部で実行されているリモートデバッガに接続できるようになるはずです。

calibre の Python 環境で任意のスクリプトを実行
calibre-debug コマンドには calibre のモジュールにアクセスしてコードを実行するための便利なスイッチがいくつかあります:

calibre-debug -c "some Python code"

これはコマンドラインでちょっとしたスニペットをテストするのに最適です。Python インタプリタの -c スイッチと同じように機能します:

calibre-debug myscript.py

これは Python スクリプトを実行するのに使用できます。スクリプトを Python インタプリタに渡すのと同じように機能しますが、calibre 環境が完全に初期化され、スクリプト内で calibre のすべてのコードを利用できる点が異なります。スクリプトでコマンドライン引数を利用するには、次の形式を使います:

calibre-debug myscript.py -- --option1 arg1

-- を指定すると、後続のすべての引数がスクリプトに渡されます。

プロジェクトで calibre を使用

Python のプロジェクトで calibre 関数/コードを直接利用することが可能です。それには 2 つの方法があります:

calibre のバイナリインストール
calibre のバイナリインストールがある場合には、calibre に付属している Python インタプリタを次のようｊにして使用できます:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Linux 上のソースインストール

上記の手法に加えて、Linux 上にソースインストールを行う場合は、次のようにして直接 calibre をインポートすることができます:

import init_calibre
import calibre

print(calibre.__version__)

重要なのは、他の calibre モジュール/パッケージの前に init_calibre をインポートすることです。インタプリタを calibre コードとして実行するようセットアップするためです。

calibre のパーツの API 説明書

- レシピの API 説明書
- プラグインの API 説明書
- データベースの API 説明書
- 電子書籍編集ツール API 説明書

© Copyright 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 GUI の一部である 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, format)

formats_removed = 3

次の引数とともに本から形式が削除されるとき: (book_id から、削除される本の形式のリストへのマッピング)

indexing_progress_changed = 9

When the indexing progress changes

items_removed = 7

タグや著者などの項目を一部の本から削除するとき。引数: (field_name, affected book ids, ids of removed items)

items_renamed = 6

タグや著者などの項目の名前を一部またはすべての本で変更するとき。引数: (field_name, affected book ids, map of old item id to new item id)

metadata_changed = 1

次の引数とともに一部の本で一部の書誌を変更するとき: (変更したフィールド名, 影響する本の ID のセット)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[ソース]

指定した本をライブラリに追加。本は 2 タプルの繰り返し可能なもので、各 2 タプルは (mi, format_map) のフォームです。mi は Metadata オブジェクトで、format_map はフォーム {fmt: path_or_stream} の辞書です。例: {'EPUB': '/path/to/file.epub'}

リストのペア (ids, duplicates) を返します。ids にはデータベースに新しく作成されたすべての本の ID が含まれます。duplicates には、has_book() が使用する簡単な重複検出ヒューリスティックに従って、データベースにすでにある本の (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 extra data files

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)[ソース]

このデータベースで特定のアクションが実行された後に呼び出されるコールバック関数を登録します。関数は 3 つの引数が必要です: (EventType, library_id, event_type_specific_data)

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

all_book_ids(type=<class 'frozenset'>)[ソース]

すべての既知の本の ID の凍結セット。

all_field_for(field, book_ids, default_value=None)[ソース]

field_for と同じですが、一度に複数の本を操作します

all_field_ids(name)[ソース]

フィイールド name のすべての値の凍結セット。

all_field_names(field)[ソース]

すべてのフィールド名の凍結セット (多対 1 と多対多のフィールドにのみ使用)

author_data(author_ids=None)[ソース]

著者データをキー付きの辞書として返します: name, sort, link

指定された ID を持つ著者が見つからない場合は、空の辞書が返されます。 a uthor_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)[ソース]

表紙画像または None を返します。デフォルトでは表紙をバイト列で返します。

警告: as_path を使用すると、表紙が一時ファイルにコピーされ、一時ファイルへのパスが返されます。使い終わったら、一時ファイルを削除する必要があります。

パラメータ:

- as_file -- True のとき、開いているファイルオブジェクト(SpooledTemporaryFile) として画像を返します

- as_image -- True のとき、QImage オブジェクトとして画像を返します

- as_pixmap -- If True return the image as a QPixmap object

- as_path -- True のとき、一時ファイルを指すパスとして画像を返します

data_for_find_identical_books()[ソース]

データベースにアクセスすることなくワーカープロセスで find_identical_books() を実装するのに使えるデータを返します。実装については db.utils を参照してください。

data_for_has_book()[ソース]

has_book() での使用に適したデータを返します。これは、db にアクセスすることなくワーカープロセスで has_book() を実装するために使用できます。

delete_custom_book_data(name, book_ids=())[ソース]

name で指定されたデータを削除します。デフォルトではすべてのデータを削除します。一部の本の ID のデータのみ削除したい場合には、本の ID のリストを渡します。

delete_trash_entry(book_id, category)[ソース]

Delete an entry from the trash. Here category is 'b' for books and 'f' for formats.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[ソース]

指定された book_ids のすべての形式の書誌を、データベース内の現在の書誌に更新します。

expire_old_trash()[ソース]

Expire entries from the trash that are too old

export_note(field, item_id) → str[ソース]

Export the note as a single HTML document with embedded images as data: URLs

fast_field_for(field_obj, book_id, default_value=None)[ソース]

field_for と同じですが、フィールドオブジェクトを取得するために余分な検索を回避します

field_for(name, book_id, default_value=None)[ソース]

book_id で指定された本のフィールド name の値を返します。そのような本が存在しない、またはフィールド名に定義された値がない、またはそのようなフィールドが存在しない場合、``default_value``が返されます。

default_value は、title、title_sort、authors、author_sort、series_indexには使用されません。これらは常にデータベースに値を持っているためです。 default_valueは、すべてのカスタム列に使用されます。

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.

field_ids_for(name, book_id)[ソース]

book_id で識別されるブックのフィールド名の値の ID を (タプルとして) 返します。値がない、またはそのような本がない、またはそのようなフィールドがない場合は、空のタプルが返されます。

field_supports_notes(field=None) → bool[ソース]

Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes.

find_identical_books(mi, search_restriction='', book_ids=None)[ソース]

mi に著者のスーパーセットがあり、タイトルが同じ本を検索します (タイトルはあいまい検索)。 data_for_find_identical_books() も参照のこと。

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[ソース]

電子書籍の形式をバイト列として返します。形式が存在しない場合、または電子書籍ファイルに書き込む権限がない場合は、None を返します。

パラメータ:

- as_file -- True のとき、電子書籍の形式がファイルオブジェクトとして返されます。ただしファイルオブジェクトは SpooledTemporaryFile のため、形式を別のファイルにコピーする場合には、性能上の理由により、代わりに copy_format_to() を使用してください。

- as_path -- 形式ファイルを一時ファイルにコピーし、一時ファイルへのパスを返します

- preserve_filename -- True かつパスを返すとき、ファイル名はライブラリで使用されているものと同じです。ただしこれを使用すると、呼び出すたびに同じ一時ファイルを作成します (毎回作り直します)

format_abspath(book_id, fmt)[ソース]

format 形式の電子書籍ファイルへの絶対パスを返します。これはこのAPIのスレッドセーフの約束を破るため、原則的に使用すべきではありません。代わりに copy_format_to() を使用してください。

現在これを使用しているのは、calibredbリスト、ビューア、編集ブック、元の形式とのcompare_format、アプリケーションを指定して開く、一括書誌編集、およびカタログ (get_data_as_dict() 経由で) のみです。

ビューア、アプリケーションを指定して本を開く、本の編集を除き、この呼び出しの結果を使用してファイル書き込み I/O を実行する人はいないと思います。

format_hash(book_id, fmt)[ソース]

指定された本の指定された形式のハッシュを返します。ハッシュの種類はバックエンドに依存しますが、通常はSHA-256です。

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[ソース]

指定した本の指定した形式のパス、サイズ、および mtime を返します。パスに直接アクセスするとこの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]][ソース]

Return all item_ids for items that have notes in the specified field or all fields if field_name is None

get_all_link_maps_for_book(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 {}.

パラメータ:

book_id -- the book id in question.

戻り値:

{field: {field_value, link_value}, ... for all fields with a field_value having a non-empty link value for that book

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[ソース]

タグブラウザの実装のために内部的に使用。

get_custom_book_data(name, book_ids=(), default=None)[ソース]

name のデータを取得します。デフォルトでは、すべての book_id のデータが返されます。一部のデータのみが必要な場合は、本の ID のリストを渡します。 book_id から値へのマップを返します。特定の値をデコードできなかった場合は、デフォルトを使用します。

get_id_map(field)[ソース]

指定したフィールドの ID 番号から値へのマッピングを返します。フィールドは多対1または多対多のフィールドである必要があります。そうでない場合、ValueErrorが発生します。

get_ids_for_custom_book_data(name)[ソース]

name がデータを持つ本の ID のセットを返します。

get_item_id(field, item_name)[ソース]

Return the item id for item_name (case-insensitive) or None if not found

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)[ソース]

Return mapping of item values to ids

get_link_map(for_field)[ソース]

Return a dictionary of links for the supplied field.

パラメータ:

for_field -- the lookup name of the field for which the link map is desired

戻り値:

{field_value:link_value, ...} for non-empty links

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[ソース]

calibre.ebooks.metadata.book.base.Metadata オブジェクトとして book_id が指定する本の書誌を返します。ただし形式のリストは検証されません。 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[ソース]

Return a dict containing the resource data and name or None if no resource with the specified hash is found

get_proxy_metadata(book_id)[ソース]

get_metadata() と同様ですが、オンデマンドでデータベースから値を読み取るだけの ProxyMetadata オブジェクトを返す点が異なります。これは、返された書誌オブジェクトから少数のフィールドにアクセスするだけでよい場合、 get_metadata よりもはるかに高速です。

get_usage_count_by_id(field)[ソース]

IDから、指定したフィールドのすべての値の使用回数へのマッピングを返します。これは、多対1または多対多のフィールドである必要があります。

has_book(mi)[ソース]

渡された Metadata オブジェクトと同じタイトルのエントリがデータベースに含まれていれば True を返します。比較で大文字と小文字は区別されません。data_for_has_book() も参照のこと。

has_format(book_id, fmt)[ソース]

ディスク上に形式が存在すれば True を返します。

has_id(book_id)[ソース]

指定した book_id が db に存在すれば True を返します

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

init()[ソース]

バックエンドからのデータでこのキャッシュを初期化します。

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

link_for(field, item_id)[ソース]

Return the link, if any, for the specified item or None if no link is found

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][ソース]

Get information about extra files in the book's directory.

パラメータ:

- book_id -- the database book id for the book

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

戻り値:

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.

merge_extra_files(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.

move_book_from_trash(book_id)[ソース]

Undelete a book from the trash directory

move_format_from_trash(book_id, fmt)[ソース]

Undelete a format from the trash directory

multisort(fields, ids_to_sort=None, virtual_fields=None)[ソース]

並べ替えられた本の ID のリストを返します。ids_to_sort が None のとき、すべての ID を返します。

fields は、そのフォームの2 タプルのリストである必要があります (field_name、ascending=TrueまたはFalse)。最も重要なフィールドは最初の 2 タプルです。

notes_data_for(field, item_id) → str[ソース]

Return all notes data as a dict or None if note does not exist

notes_for(field, item_id) → str[ソース]

Return the notes document or an empty string if not found

notes_resources_used_by(field, item_id)[ソース]

Return the set of resource hashes of all resources used by the note for the specified item

pref(name, default=None, namespace=None)[ソース]

指定した環境設定の値、または環境設定に指定がなければデフォルト値を返します。

read_backup(book_id)[ソース]

本の OPF 書誌バックアップをバイト列として返します。バックアップが存在しない場合は None を返します。

remove_books(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.

remove_formats(formats_map, db_only=False)[ソース]

指定した本から指定した形式を削除します。

パラメータ:

- formats_map -- book_id から、削除される本の形式のリストへのマッピング。

- db_only -- True のとき、その形式のレコードを db からだけ削除し、ファイルシステムにある実際の形式ファイルは削除しません。

戻り値:

A map of book id to set of formats actually deleted from the filesystem for that book

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 extra data files

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 を防ぐために必要です。検索キャッシュを更新すると排他ロックが保持されますが、複合列を検索するには、共有ロックを取得しようとする ProxyMetadata を介してフィールド値を読み取る必要があります。これのトリガーとなる別のシナリオもあるかもしれません。

このプロパティは、アクセスごとに新しいロックオブジェクトを返します。このロックオブジェクトは (性能上の理由で) 再帰的ではなく、必ず with cache.safe_read_lock: として with 文とともに使用する必要があります。そうしないと、よくないことが起こります。

save_original_format(book_id, fmt)[ソース]

指定した形式のコピーを ORIGINAL_FORMAT として保存し、既存の ORIGINAL_FORMAT を上書きします。

search(query, restriction='', virtual_fields=None, book_ids=None)[ソース]

指定したクエリをデータベースで検索し、マッチした本のIDのセットを返します。

パラメータ:

- restriction -- 指定したクエリに AND される制限。ただし制限がキャッシュされるため、AND bの 検索は、制限 b よりも遅くなります。

- virtual_fields -- 内部使用 (検索する on_device のような仮想フィールド)

- book_ids -- None でないとき、すべての本を検索するのではなく、検索される本の 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)[ソース]

Search the text of notes using an FTS index. If the query is empty return all notes.

set_conversion_options(options, fmt='PIPE')[ソース]

options は、{book_id:conversion_options} 形式のマップでなくてはなりません

set_cover(book_id_data_map)[ソース]

この本の表紙を設定します。データは、QImage、QPixmap、ファイルオブジェクト、またはバイト列のいずれかです。 None にすることも可能で、その場合、既存の表紙はすべて削除されます。

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[ソース]

name で指定したフィールドの値を設定します。変更の影響を受けたすべての本 の IDのセットを返します。

パラメータ:

- book_id_to_val_map -- book_id から、適用すべき値へのマッピング。

- allow_case_change -- True のとき、多対1または多対多フィールドの大文字と小文字が変更されます。たとえば、本にタグ tag1 があり、別の本のタグを Tag1 に設定した場合、allow_case_change が True であれば両方の本にタグ Tag1 が付けられ、そうでなければ両方にタグ tag1 が付けられます。

- do_path_update -- 内部利用。決して変更しないこと。

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

パラメータ:

- field -- the lookup name

- value_to_link_map -- dict(field_value:link, ...). Note that these are values, not field ids.

戻り値:

books changed by setting the link

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 を設定すると、miに空の値が含まれていても、強制的に set_metadata を更新します。この場合、'None' と 'empty' は区別されます。 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[ソース]

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.

set_pref(name, val, namespace=None)[ソース]

指定した環境設定に指定した値を設定します。pref() も参照のこと。

split_if_is_multiple_composite(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.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[ソース]

指定した時間より古いタグ tag を持つすべての本の ID を返します。tag の比較で大文字と小文字は区別されません。

パラメータ:

- delta -- timedelta オブジェクト、または None。None のときにはそのタグの ID がすべて返されます。

- must_have_tag -- None でないとき、マッチするリストはこのタグが付いた本に制限されます

- must_have_authors -- 著者のリスト。 None でないとき、マッチするリストは指定された著者の本に制限されます (大文字と小文字は区別されません)。

unretire_note_for(field, item_id) → int[ソース]

Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database

user_categories_for_books(book_ids, proxy_metadata_map=None)[ソース]

指定した本のユーザカテゴリを返します。 proxy_metadata_map はオプションで、本の ProxyMetadata オブジェクトがすでに存在するコンテキストでパフォーマンスを向上させるのに役立ちます。これは、book_idsから対応するProxyMetadataオブジェクトへのマッピングでないといけません。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »
- calibre 開発環境の構築 »
- 電子書籍編集ツール API 説明書

電子書籍編集ツール API 説明書

電子書籍編集ツールは、HTML +リソースファイルのコレクションとして書籍を表す calibre.ebooks.oeb.polish.container.Container オブジェクトと、コンテナで操作を実行するために使用できるさまざまなツールで構成されています。すべてのツールは、さまざまな``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

Container オブジェクト
class calibre.ebooks.oeb.polish.container.Container(rootpath, opfpath, log, clone_data=None)[ソース]

コンテナは開いている電子書籍を、OPF ファイルおよびファイルでいっぱいのフォルダとして表します。 2 つの重要な概念があります:

- ルートフォルダ。これが電子書籍のベースです。すべての電子書籍ファイルは、このフォルダまたはそのサブフォルダ内にあります。

- 名前: ルートフォルダを基準にした書籍のファイルへのパスです。それらは常に POSIX 区切り文字を含み、引用符で囲まれていません。本のファイルの正規の識別子と考えることができます。コンテナオブジェクトのほとんどのメソッドは名前で機能します。名前は常にNFC Unicode の通常の形式です。

- クローン: コンテナオブジェクトは、電子書籍エディタでチェックポイントを実装するために使用される効率的なディスク上のクローンをサポートします。これを機能させるために、ファイルシステム上のファイルには直接アクセスしないでください。代わりに、raw_data() または open() を使用して、本の中のコンポーネントファイルの読み取り/書き込みを行ってください。

href と名前を変換するときに、このクラスが提供するメソッドを使用します。このときすべての href が引用符で囲まれていると想定します。

abspath_to_name(fullpath, root=None)[ソース]

絶対パスを ルート を基準とした正規名に変換します

パラメータ:

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)[ソース]

汚くなった解析済みオブジェクトをすべてファイルシステムにコミットし、電子書籍ファイルをアウトパスに書き込みます。

パラメータ:

- output -- 保存した電子書籍ファイルを書き込むパス。 None であれば、元の本ファイルのパスが使用されます。

- keep_parsed -- True のとき、コミットされた項目の解析された表現はキャッシュに保持されます。

commit_item(name, keep_parsed=False)[ソース]

解析されたオブジェクトをディスクにコミットします (シリアル化され、基になるファイルに書き込まれます)。 keep_parsed が True なら、解析された表現はキャッシュに保持されます。parsed() も参照のこと。

dirty(name)[ソース]

name に対応する解析済みオブジェクトを汚いとマークします。parsed() も参照のこと。

exists(name)[ソース]

正規名に対応するファイル/フォルダーが存在すれば True。ただしこの関数は、基盤となる OS ファイルシステムの制限、特に (非) 感度の問題があります。したがって、大文字と小文字を区別しないファイルシステムでは、名前と基になるファイルシステムファイルで大文字と小文字が異なっていても、これは True を返します。has_name() も参照のこと。

filesize(name)[ソース]

指定された正規名で表されるファイルのサイズをバイト単位で返します。汚れた解析済みオブジェクトを自動的に処理します。parsed() も参照のこと。

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[ソース]

指定された名前から派生した href を使用してマニフェストに項目を追加します。 href と id の一意性を自動的に保証します。生成された項目を返します。

get_file_path_for_processing(name, allow_modification=True)[ソース]

open() に似ていますが、開いているファイルオブジェクトの代わりにファイルパスを返す点が異なります。

property guide_type_map

ガイドタイプの正規名へのマッピング

has_name(name)[ソース]

指定されたものと同じ正規名のファイルが存在する場合は True を返します。 exists() とは異なり、このメソッドでは常に大文字と小文字が区別されます。

href_to_name(href, base=None)[ソース]

(ベースに対して) href を名前に変換します。 base は name または None である必要があり、その場合には self.root が使用されます。

insert_into_xml(parent, item, index=None)[ソース]

item を親に挿入し (インデックスが None であれば追加し)、インデントを修正します。自己終了する item でのみ機能します。

is_dir = False

このコンテナが解凍された本 (ディレクトリ) を表すかどうか

iterlinks(name, get_line_numbers=True)[ソース]

名前のすべてのリンクを繰り返します。get_line_numbers が True のとき、結果は (link, line_number, offset) の形式で得られます。line_number はリンクが発生するline_number、offset は行の先頭からの文字数です。ただし offset は、ゼロでない場合でも実際には複数の線を含む可能性があります。

make_name_unique(name)[ソース]

name には必ずこの本に存在していないものを指定してください。すでに存在する場合は、変更された存在しないバージョンを返します。

manifest_has_name(name)[ソース]

マニフェストに名前に対応するエントリがあれば True を返します

property manifest_id_map

マニフェストIDの正規名へのマッピング

manifest_items_of_type(predicate)[ソース]

メディアタイプが predicate とマッチするすべてのマニフェスト項目の名前。predicate は、セット、リスト、文字列、または単一の引数を取る関数であり、メディアタイプで呼び出されます。

manifest_items_with_property(property_name)[ソース]

指定したプロパティを持つすべてのマニフェスト項目

property manifest_type_map

マニフェストメディアタイプの、そのメディアタイプの正規名のリストへのマッピング

property mi

Metadata オブジェクトとしてこの本の書誌。ただしこのオブジェクトは、このプロパティが要求されるたびにオンザフライで構築されるため、慎重に使用してください。

name_to_abspath(name)[ソース]

正規名を OS に依存する絶対パスに変換します

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')[ソース]

直接読み取り/書き込み用に name で指定されたファイルを開きます。ただしファイルが汚いときには、ファイルがコミットされて解析キャッシュから削除されます。このファイルは、解析されたバージョンのファイルに再度アクセスする前に終了する必要があります。そうしないと、問題が発生します。

property opf

解析された OPF ファイル

opf_get_or_create(name)[ソース]

指定した name の最初の XML 要素を返すか、まだ存在しなければ opf:package 要素の下に作成して、それを返す便利なメソッド。

property opf_version

OPF の <package> 要素に設定されているバージョン

property opf_version_parsed

OPF の <package> 要素に整数のタプルとして設定されたバージョン

opf_xpath(expr)[ソース]

OPF ファイルで XPath 式を評価するための便利なメソッドで、opf: および dc: 名前空間プレフィックスが事前定義されています。

parsed(name)[ソース]

name で指定されたファイルの解析済み表現を返します。HTML および XML ファイルの場合、lxml ツリーが返されます。CSS ファイルの場合、css_parser スタイルシートが返されます。ただし解析されたオブジェクトは、性能上の理由からキャッシュされます。解析されたオブジェクトに変更を加える場合は、dirty() を呼び出して、コンテナがキャッシュを更新することを認識できるようにする必要があります。replace() も参照のこと。

raw_data(name, decode=True, normalize_to_nfc=True)[ソース]

name で指定したファイルに対応する生データを返します

パラメータ:

- decode -- True かつ、ファイルにテキストベースの MIME タイプがあるときは、それをデコードして、raw バイトの代わりに 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)[ソース]

親から item を削除し、インデントを修正します (自己終了する item でのみ機能します）

remove_item(name, remove_from_guide=True)[ソース]

このコンテナから name で識別される項目を削除します。これにより、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 を使用して、name のすべてのリンクを置き換えます。replace_func は URL を受け入れ、置き換えられた URL を返す呼び出し可能である必要があります。また、実際の置換が行われた場合に True に設定される 'replaced' 属性が必要です。このような呼び出し可能オブジェクトは作成するには、LinkReplacer および LinkRebaser classes を使用すると便利です。

serialize_item(name)[ソース]

解析されたオブジェクト (正規名で識別される) をバイト文字列に変換します。parsed() も参照のこと。

set_spine(spine_items)[ソース]

スパインを spine_items に設定します。ここで、spine_itemsは (name, linear) の形式のイテラブルです。名前の中にマニフェストに存在しないものがあれば、エラーが発生します。

property spine_items

本のスパインのすべての項目のパスを生成するイテレータ。spine_iter および spine_items も参照のこと。

property spine_iter

本の背表紙のすべての項目に対してitem, name, is_linear をもたらすイテレータ。 item は lxml 要素、name は正規ファイル名、is_linear は項目が線形の場合は True です。spine_names および spine_items も参照のこと。

property spine_names

本の背表紙のすべての item に name と 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 -- 2 つの引数 (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)[ソース]

raw で文字列として表される HTML の解析エラーを修正します。修正は、HTML5 解析アルゴリズムを使用して行われます。

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[ソース]

raw に文字列として表された HTML をきれいに印刷します

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[ソース]

raw に文字列として表された CSS をきれいに印刷します

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[ソース]

raw に文字列として表されたXMLをきれいに印刷します。name が OPF の名前のとき、追加の OPF 固有の美化が実行されます。

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[ソース]

raw に文字列として表された HTML/CSS/XML をきれいに印刷します

本のジャケットの管理

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[ソース]

既存のジャケットがあれば、削除します。既存のジャケットが見つからなければ False を返します。

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[ソース]

本の書誌から新しいジャケットを作成するか、既存のジャケットを交換します。既存のジャケットが交換された場合は True を返します。

ファイルの分割とマージ

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[ソース]

名前で指定されたファイルを loc_or_xpath で指定された位置で分割します。分割すると、影響を受けるファイルへのすべてのリンクと参照が自動的に移行されます。

パラメータ:

- loc_or_xpath -- //h:div[@id="split_here"] などの XPath 式である必要があります。プレビューパネルで分割を実装するために内部的に使用される loc にすることもできます。

- before -- True のとき、分割は識別された要素の前に発生し、そうでなければその後に発生します。

- totals -- 内部で使用

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[ソース]

指定されたファイルを複数の場所 (指定された XPath 式に一致するすべてのタグ) で分割します。split() も参照のこと。分割すると、影響を受けるファイルへのすべてのリンクと参照が自動的に移行されます。

パラメータ:

before -- True のとき、分割は識別された要素の前に発生し、そうでなければその後に発生します。

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[ソース]

指定したファイルをひとつのファイルにマージし、影響を受けるファイルへのすべてのリンクと参照を自動的に移行します。ファイルはすべて HTML または CSS ファイルである必要があります。

パラメータ:

- category -- 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 -- None、または表紙の設定方法を制御する辞書。辞書には次のエントリを含めることができます。keep_aspect: True または False (EPUBで表紙の縦横比を保持）**no_svg**: True または False (EPUB タイトルページで SVG 表紙ラッパーを使用）

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 の場合、CSS 規則に一致しない HTML のクラス属性も削除されます。

- 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> タグから 3 階層の目次を作成します。

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

EditBook プラグインの個々のツールの基本クラス。便利なメンバーには次のものがあります:

- self.plugin: このツールが属する calibre.customize.Plugin オブジェクトへの参照。

- self. boss

- self. gui

サブクラスでオーバーライドする必要があるメソッド:

- create_action()

- register_shortcut()

name = None

これを一意の名前に設定すると、キーとして使用されます

allowed_in_toolbar = True

True のとき、ユーザはこのツールをプラグインツールバーに配置することを選択できます

allowed_in_menu = True

True のとき、ユーザはこのツールをプラグインメニューに配置することを選択できます

toolbar_button_popup_mode = 'delayed'

ツールバーボタンのメニュー (あれば) のポップアップモード。可能な値は、'delayed', 'instant', 'button' です。

property boss

calibre.gui2.tweak_book.boss.Boss オブジェクト。ユーザインタフェースを制御するために使用されます。

property gui

ユーザインタフェースのメインウィンドウ

property current_container

編集中の本を表す現在の calibre.ebooks.oeb.polish.container.Container オブジェクトを返します。

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[ソース]

指定された qaction をトリガーするキーボードショートカットを登録します。このキーボードショートカットは、自動的にエディタ設定のキーボードショートカットセクションでユーザがカスタマイズできるようになります。

パラメータ:

- qaction -- QAction オブジェクト。ユーザが構成済みのキーの組み合わせを押すとトリガーされます。

- unique_name -- このショートカット/アクションの一意の名前。内部で使用されます。このプラグインの他のアクションで共有しないでください。

- default_keys -- デフォルトのキーボードショートカットのリスト。指定しない場合、デフォルトのショートカットは設定されません。ここで指定したショートカットが、組み込みのショートカットまたはユーザ設定/他のプラグインからのショートカットと競合する場合、それらは無視されます。その場合、ユーザは環境設定からショートカットを手動で設定する必要があります。例: default_keys=('Ctrl+J', 'F9')

- short_text -- このアクションのオプションの短いな説明。指定しない場合、QAction からのテキストが使用されます。

- description -- このアクションのオプションの長めの説明。このショートカットの設定エントリで使用されます。

create_action(for_toolbar=True)[ソース]

for_toolbar に応じて、プラグインツールバーまたはプラグインメニューのいずれかに追加される QAction を作成します。例:

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)[ソース]

name で指定したファイルをエディタで開きます

パラメータ:

- 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 -- 現在のコンテナを比較するコンテナオブジェクト。 None のとき、以前にチェックポイントされたコンテナが使用されます

show_editor(name)[ソース]

name で指定されたファイルを編集しているエディターを表示します

sync_preview_to_editor()[ソース]

プレビューパネルの位置を現在のエディタの現在のカーソル位置に同期します

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 次へ |
- 前へ |
- 開始 »

- デジタル著作権管理 (Digital Rights Management, DRM)

デジタル著作権管理 (Digital Rights Management, DRM)

デジタル著作権管理 (Digital Rights Management, DRM)とは、ハードウェアの製造元、出版社、著作権者や個人が、デジタルコンテンツやデバイスの利用に制限をかけるために利用できるアクセス制御技術の総称です。これはまた侮蔑的に、Digital Restrictions Management (デジタル制限管理) と呼ばれることもあります。この呼び方は、コンテンツ提供者が予見できなかった、あるいは望まなかったにもかかわらず、デジタルコンテンツの利用 (合法であるかどうかにかかわらず) を阻害してしまうような技術を揶揄するのに使われます。DRM という用語は通常、ファイルやデバイスに変更を加えない形でのコピープロテクション、たとえばシリアル番号やキーファイルなどに対しては使われることはありません。この用語は、特定のデジタル作品やデバイスに関連付けられた制限を表現するのに使われることもあります。DRM 技術は、エンドユーザがコピーしたりフォーマット変換したりできないようデジタルメディアの利用を制限しようするものです。 Wikipedia [https://en.wikipedia.org/wiki/Digital_rights_management] を参照してください。

DRM は個人にとってどのような意味がありますか？

DRM のかかった電子書籍を購入するしても、皆さんはその本を本当の意味で所有することはできません。売り手が支持した方法で利用する権利を購入したにすぎないからです。DRM は皆さんが "購入した" 本の利用を制限します。DRM つきの本を購入した人々はしばしば、こうした制限の及ぶ範囲に気づいていません。このような制限があるせいで、フォントのサイズを変更したりといったスタイルの変更を含め、電子書籍の書式を好きなように変更することができません。DRM のない本であればそうしたことができるソフトウェアが存在するにもかかわらずです。DRM がかかっていると、あるフォーマットから別のフォーマットに変換することができないと気づいて、本を購入した人々が驚くことはよくあります。したがって Amazon Kindle を持っていて Barnes and Nobles から本を買ったとき、その電子書籍に DRM がかかっていたら手持ちの Kindle ではその本を読めないことを知っておく必要があります。ただしここで話題にしているのは購入した本についてです。盗んだものや、海賊版ではなく、購入した 本の話です。

DRMは著者のために何をしますか？

DRM つきの電子書籍の出版社は DRM とは全面的に著作者のためのものであり、芸術的な完全性を保護し、海賊版を防ぐためのものだと主張しています。しかし DRM では決して海賊版を防ぐことはできません。コンテンツの海賊版を作ろうとする人々や、それを利用とする人々は、どのみちそうしようとしますし、実際うまくやってしまいます。現在における主な DRM の手法は Amazon, Adobe, Barnes and Nobles の 3種類がありますが、この 3 種類すべてがクラックされています。DRM がすることと言ったら、正当なユーザに対して不便を強いることだけです。DRM が本当に著作者に害をもたらさないかについては、議論の余地があります。なぜなら DRM がなければ本を購入したであろう人々が 、DRM を厭うというだけで海賊版に手を出すこともあるからです。DRM がなければ海賊版を作るという人々は、DRM があろうとも海賊版を作るのです。何度でも繰り返して言いますが、重要なのは DRM は海賊版を防げない ということです。つまり DRM は購入者にとって意味がなく害のあるものでしかないばかりか、お金の無駄なのです。

DRMと自由

デジタルコンテンツは情報だけでなく創造的な作品を作成してすべての人々に簡単に提供し、人類に力を与えるために利用できるものですが、一部の出版社にとっては不利益となります。そうした出版社は、世界の発展についていけない自分たちを守りたいがために、人々を自由から遠ざけようとするのです。

calibre が DRM をサポートしないのはなぜですか？

calibre がオープンソースソフトウェアであるのに対して、まさにその性質上 DRM はクローズドです。もし calibre で DRM のかかったファイルを開いたり読んだりすることをサポートしようとするなら、DRM 解除ツールとして使用するためにちょっとした変更を加えることができなくはありませんが、現在の法のもとでは DRM 解除は違法です。オープンソースソフトウェアと DRM は対立する概念なのです。DRM とは利用者をコントロールしようとするものであり、オープンソースソフトウェアとは利用者に力を与えるものです。この二つはどうしても共存することができないのです。

コンテンツプロバイダに対する calibre の見解は？

我々は、著作者やその他のコンテンツプロバイダはその努力に報いられるべきだと固く信じていますが、DRM でそれができるわけではありません。我々がこのさまざまなソースからなる DRM フリーな電子書籍のデータベースを開発しているのは、皆さんが DRM フリーな代替手段を見つける手助けをし、また独立した著作者や出版社が DRM フリーな電子書籍としてコンテンツを出版する手助けを行うためです。お役に立てることを望み、また皆さんにはこうして利用可能となったコンテンツを海賊版とすることのないようお願いします。

DRMと戦うにはどうすればいいですか？

電子書籍を読み、購入する者として、皆さんは DRM と戦うことができます。DRM のかけられた本を買わないでください。一部の出版社は DRM フリーな電子書籍を出版しています。こうした出版社がお探しの電子書籍を出していないか、探す努力をしてください。もしもある独立した著作者が販売する DRM フリーな電子書籍を気に入ったなら、どうぞ彼らに寄付をしてください。このような電子書籍は出版社から買う DRM つきの本より安い傾向があるので (例外はあるかもしれませんが) お金のうまい使い方です。コンテンツを海賊版にして DRM フリーな電子書籍の出版社や著作者を落胆させることのないようにしてください。コンテンツの提供者は、その努力を報われてしかるべきです。自分の読書体験をよくするために DRM フリーの電子書籍を作り出すことで、彼らをこらしめようとしないでください。長い目で見れば、自分にとって害となります。もし DRM つきと DRM フリーの両方の電子書籍を扱う販売者から DRM つきかどうかわからないまま本を買った場合には、Web サイト上に DRM の状態についてコメントやレビューを書くことで後で他の購入者たちが情報を得られるようにしてあげてください。販売者の多くは、本に DRM がかかっているかどうか個別に示すことが購入者にとって重要だと考えていません。DRM フリーなライフを送るためのガイドは こちら [https://www.defectivebydesign.org/guide/ebooks] です。

© Copyright Kovid Goyal.
最終更新 5月 03, 2024

ナビゲーション

- モジュール
- 前へ |
- 開始 »
- 用語

用語

RSS

RSS (Really Simple Syndication) は、Web フィード形式で、ニュース記事、ブログ記事など、頻繁に更新されるコンテンツを公開するために使用されます。これは特にコンピューターで読むのに適した形式であるため、Webから電子書籍へコンテンツを取り込むのに好ましい方法です。インターネット上では他にも多くのフィード形式が使用されており、calibre はそのほとんどを読むことができます。中でもブログで一般的に使用されている ATOM 形式はしっかりサポートしています。

レシピ

レシピは、calibre に対するまとまった指示のことで、雑誌やブログなどのオンラインニュースソースを電子書籍に変換する方法を calibre に詳細に教えるために使用します。基本的にレシピは Python [https://www.python.org] コードです。そのため複雑なニュースソースであっても自動処理により電子書籍に変換することが可能です。最も簡単なものは、例えば URL などの単なる変数のまとまりで、それだけの情報があれば calibre はインターネットからニュースをダウンロードすることができます。

HTML

HTML (Hyper Text Mark-Up Language) は、電子出版のための標準一般化マークアップ言語 (SGML)のサブセットで、ワールドワイドウェブに特化した標準です。

CSS

CSS (Cascading Style Sheets) は言語の一種で、HTML ドキュメントのレンダリング方法 (見た目のスタイル) を記述するために使用されます。

API

API (Application Programming Interface) はソースコードインタフェースで、コンピュータプログラムによって行われるサービスの要求をサポートするためにライブラリが提供します。

LRF

LRF SONY製の電子書籍リーダーが読む電子書籍形式です。

URL

URL (Uniform Resource Locator) 例: ''http://example.com''

正規表現

正規表現 は、文字列を識別するための柔軟な手段で、特定の文字、単語、文字のパターンなどを簡潔に特定することができます。正規表現の基本については、チュートリアル を参照してください。

© Copyright 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`

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

© Copyright 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')
# }}}

© Copyright 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)

© Copyright 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
# }}}

© Copyright 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()

© Copyright 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)

© Copyright 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)

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

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

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

# }}}

© Copyright 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()

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

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

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

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

© Copyright 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)

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

© Copyright 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)

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

© Copyright 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()

© Copyright 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()
# }}}

© Copyright 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)# }}}

© Copyright 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]

© Copyright 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)

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

© Copyright 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. Searching the full text of all books
8. 仮想ライブラリ
9. 一時的に本をマーク
10. ファイル名から書誌を推測
11. 本の詳細
12. タグブラウザ
13. 表紙グリッド
14. 表紙ブラウザ
15. Adding notes for authors, series, etc.
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. Searching the text
6. キーボード操作だけでリンクを開く
7. 本の外観と操作感をカスタマイズ
8. 辞書検索
9. テキストや画像をコピー
10. 画像を拡大
11. キーボード ショートカット
12. 非リフロー型のコンテンツ
13. calibre ビューア用に本をデザイン

電子書籍変換

1. はじめに

2. 外観

1. フォント
2. テキスト
3. レイアウト
4. スタイル
5. スタイルの変形
6. HTML の変形

3. ページ設定
4. ヒューリスティック処理
5. 検索と置換

6. 構造検出

1. 章と改ページ

2. その他

7. 目次

8. HTML入力ドキュメントを変換するときに章のタイトルとして画像を使用

9. 目次項目のテキストとしてタグの属性を使用

10. 変換のためにオプションを設定/保存する方法

11. 形式固有のヒント

1. Microsoft Word ドキュメントの変換

1. 古い .doc ファイル

2. テキストドキュメントの変換
3. PDF ドキュメントの変換
4. コミック本のコレクション
5. EPUB の優れた書式のデモ
6. ODT ドキュメントの変換

7. PDF への変換

1. ヘッダとフッタ
2. 印刷可能な目次
3. HTML ファイルごとのカスタムページ余白

電子書籍の編集
基本的なワークフロー

ファイルブラウザ

1. ファイルの名前を変更
2. ファイルのマージ
3. テキストファイルの並び順を変更
4. 表紙をマーク
5. ファイルの削除
6. ファイルのエクスポート

7. 新しい画像/フォントなどを追加、または新しい空のライルを作成

8. ファイルの置換

9. スタイルシートを HTML ファイルに効率的にリンク

検索と置換

1. 保存済み検索条件
2. 関数モード
3. HTML タグを無視して検索

自動化ツール

1. 目次の編集
2. 本のチェック
3. 表紙の追加
4. 参照されているフォントの埋め込み
5. 埋め込みフォントのサブセット化
6. 句読点のスマート化
7. CSS プロパティの変換
8. 使用されていない CSS 規則の削除
9. HTML の修正
10. ファイルの整形
11. インライン目次の挿入
12. セマンティクスの設定
13. スタイル情報の絞り込み
14. 本の内部のアップグレード

チェックポイント

ライブプレビューパネル

1. HTML ファイルの分割

ライブ CSS パネル

その他のツール

1. 目次ビュー

2. 本の中身のスペルチェック

1. 新規辞書の追加

3. 特殊文字の挿入
4. コードインスペクタビュー
5. 外部リンクのチェック
6. 外部リソースのダウンロード
7. ファイルをタイプ別にフォルダに配置
8. その他の電子書籍形式から EPUB にインポート
9. レポートツール

コードエディタの特殊な機能

1. 構文の強調表示
2. 状況依存のヘルプ
3. 自動補完
4. スニペット

calibre コンテンツサーバー

1. 別のデバイスからコンテンツサーバにアクセス

1. ホームネットワーク上のデバイスからサーバにアクセス

1. ホームネットワーク接続のトラブルシュート

2. インターネット上のどこかからサーバにアクセス

2. サーバのインタフェース

1. 本のリスト

2. 書籍リーダ

3. ブラウザのサポート

4. オフラインサポートの有効化

5. ユーザアカウントの管理をコマンドラインでのみ行う

6. calibre コンテンツサーバを他のサーバと統合

1. 仮想ホスト全体の利用

2. URL プレフィックスを使用

7. 最新の Linux システムで calibre サーバ用のサービスを作成

電子書籍を比較

1. 比較ビューを解読

2. 比較ツールを起動

1. 2 つの電子書籍ファイルを比較

2. ORIGINAL_FMT と FMT を比較

3. 編集中のチェックポイントと本の現在状態を比較

電子書籍の書誌の編集

1. 書誌を一冊ずつ個別に編集

1. 書誌のダウンロード
2. 本の形式を管理
3. カバーに関するいろいろ

2. 書誌を複数冊まとめて編集

1. 検索と置換

2. 書誌の一括ダウンロード

3. Adding extra data files to a book

よくある質問
電子書籍のフォーマット変換

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. How do I use calibre with my Android phone/tablet or Kindle Fire?

1. USB ケーブルを使用

2. 無線接続

7. Kindle や他の読書デバイス上の Web ブラウザから calibre の本にアクセスできますか？

8. calibre からメールが送れません

9. デバイスが Linux で読み取り専用でマウントされていて、calibre に接続できません

10. どうして calibre は Kindle のコレクションや Nook の本棚をサポートしないのですか？

11. calibre を Kobo Touch/Glo などと一緒に使おうとするとエラーが出ます

12. e-ink Kindle に送信した本の表紙が一瞬表示されてすぐに一般の表紙に置き換わってしまいます

13. MOBI ファイルの表紙が Kindle for PC や Kindle for Android/iPad などで表示されなくなりました

14. calibre で Kindle に本を送信したのに表示されません

ライブラリ管理

1. 本のファイルはどこに保存されていますか？

2. calibre は著者名や著者名の読みをどのように管理しますか？

3. どうして calibre は本を好きなフォルダに保存してくれないのですか？

4. どうして calire には×××の欄がないのですか？

5. フォーマットや ISBN を列に表示できますか？

6. どうすれば calibre のデータを別のコンピュータに移動できますか？

7. calibre の本リストが空っぽです

8. ネットワークドライブ/NAS 上の calibre ライブラリでエラーが出ます

その他

1. Amazon is stopping email delivery of MOBI files?

2. お気に入りのニュースサイトから calibre にニュースをダウンロードしたい

3. どうして calibre という名前なんですか？

4. calibre はどうして macOS ではフォントの一部しか表示しないのですか？

5. calibre が Windows 上で起動しません

6. calibre がときどきフリーズ/クラッシュします

7. calibre の電子書籍ビューアと本の編集ツールが Windows 上で動きません

8. ビューアを使用したり変換を行ったりすると Windows でアクセスが拒否されたとエラーが出ます

9. macOS 上で calibre が開始しない、またはクラッシュします

10. I get only a black or white screen when running the calibre E-book viewer?

11. インストーラをダウンロードしましたが、動きません

12. ウィルス対策プログラムが calibre がウィルス/トロイの木馬だと主張しています

13. calibre のバックアップをとるにはどうすればいいですか？

14. どうすれば calibre を使って EPUB 書籍を購入できますか？ (あるいは、.acsm ファイルで何をすればいいですか？)

15. 「許可がありません」というエラーが出ます

16. リーダ上にコメント書誌を表示させることはできますか？

17. どうすれば calibre に HTTPプロキシを使わせることができますか？

18. calibre に機能追加したいのですが、どうすればよいですか？

19. どうして calibre には自動更新機能がないのですか？

20. calibre のライセンスはどうなっていますか？

21. どうすれば calibre を USB メモリから実行できますか？

22. どうすれば自前の Linux サーバ上でニュースダウンロードのような calibre のパーツとコンテンツサーバを実行できますか？

チュートリアル
お気に入りのニュースサイトを追加

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. テンプレートを使用してカスタム列を定義する
4. テンプレートとプラグボード
5. テンプレートでの関数の使用 - 単関数モード
6. 一般プログラムモード

7. より複雑なテンプレート表現のプログラム - テンプレートプログラムモード

8. Python Template Mode
9. 保存済みテンプレート
10. テンプレートに追加情報を与える
11. モード間の違いに関する注意事項
12. ユーザ定義の Python テンプレート関数
13. テンプレートの保存/送信に関する特記事項
14. ヒント

15. 関数リファレンス

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. 追加の制限の使用

The calibre:// URL スキーム

1. 指定したライブラリに切替え

2. 指定した本を calibre で開く

3. 指定した本を電子書籍ビューアで指定した位置で開く

4. 本を検索

5. Open a book details window on a book in some library

6. Open the notes associated with an author/series/etc.

7. URL パラメータの 16 進エンコード

calibreをカスタマイズ

1. 環境変数
2. Tweaks
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. デバッグのコツ

1. print 文を使用

2. インタラクティブな Python インタプリタの利用

3. Python デバッガをリモートデバッガとして使用

4. 好みの Python IDE でデバッガを使用

5. calibre の Python 環境で任意のスクリプトを実行

8. プロジェクトで calibre を使用

1. calibre のバイナリインストール

2. Linux 上のソースインストール

9. calibre のパーツの API 説明書

1. レシピの API 説明書

1. BasicNewsRecipe

2. プラグインの API 説明書

1. プラグイン
2. FileTypePlugin
3. 書誌プラグイン
4. カタログプラグイン
5. 書誌ダウンロードプラグイン
6. 変換プラグイン
7. デバイスドライバ
8. ユーザインタフェースアクション
9. 設定プラグイン

3. データベースの API 説明書

1. Cache

4. 電子書籍編集ツール API 説明書

1. Container オブジェクト
2. コンテナ内のコンポーネントファイルの管理
3. きれいな印刷と解析エラーの自動修正
4. 本のジャケットの管理
5. ファイルの分割とマージ
6. 表紙の管理
7. CSS の処理
8. 目次の処理
9. 書籍編集ツール
10. エディタのユーザインタフェースを制御

デジタル著作権管理 (Digital Rights Management, DRM)

1. DRM は個人にとってどのような意味がありますか？

2. DRMは著者のために何をしますか？

3. DRMと自由

4. calibre が DRM をサポートしないのはなぜですか？

5. コンテンツプロバイダに対する calibre の見解は？

6. DRMと戦うにはどうすればいいですか？

用語