Crie seus próprios plugins para aumentar as funcionalidades do Calibre

O calibre possui um design muito modular. Quase todas as funcionalidades do calibre vêm na forma de plugins. Os plug-ins são usados para conversão, para baixar notícias (embora sejam chamadas de receitas), para vários componentes da interface do usuário, para conectar-se a dispositivos diferentes, para processar arquivos ao adicioná-los ao calibre e assim por diante. Você pode obter uma lista completa de todos os plugins integrados no calibre, visitando: guilabel: Preferências-> Avançado-> Plugins.

Aqui vamos mostrar a você como criar seus próprios plugins e adicionar novos atributos para o calibre

Nota

Somente se aplica as versões do calibre >= 0.8.60

Anatomia de um plugin do calibre

Um plugin de calibre é muito simples, é apenas um arquivo ZIP que contém algum código Python e outros recursos, como arquivos de imagem necessários ao plugin. Sem mais delongas, vamos ver um exemplo básico.

Suponha que você tenha uma instalação de calibre que esteja usando para publicar automaticamente vários documentos eletrônicos nos formatos EPUB e MOBI. Você deseja que todos os arquivos gerados pelo calibre tenham o editor definido como “Olá, mundo”, veja como fazê-lo. Crie um arquivo chamado: file: __init __. Py (este é um nome especial e sempre deve ser usado para o arquivo principal do seu plugin) e insira o seguinte código Python nele:


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

Isso é tudo. Para adicionar este código ao Calibre como um plugin, apenas execute o seguinte na pasta na qual foi criado __init__.py:

calibre-customize -b .

Nota

No macOS, as ferramentas de linha de comando estão dentro do pacote do calibre. Por exemplo, se você instalou o calibre em /Applications , as ferramentas de linha de comando estão em /Applications/calibre.app/Contents/MacOS/.

Baixe o plugin Hello World em helloworld_plugin.zip.

Toda vez que você usar o calibre para converter um livro, o método plugin: meth: run será chamado e o livro convertido terá seu editor definido como” Hello World “. Este é um plugin trivial, vamos para um exemplo mais complexo que realmente adiciona um componente à interface do usuário.

Um plugin de Interface de Usuário

Este plugin será espalhado por alguns arquivos (para manter o código limpo). Ele mostrará como obter recursos (imagens ou arquivos de dados) do arquivo ZIP do plug-in, permitir que os usuários configurem seu plug-in, como criar elementos na interface do usuário do calibre e como acessar e consultar o banco de dados de livros no calibre.

You can download this plugin from interface_demo_plugin.zip

A primeira coisa a observar é que esse arquivo ZIP contém muito mais arquivos, explicados abaixo, e preste atenção plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.tx

Um arquivo de texto vazio usado para ativar a mágica do plug-in com vários arquivos. Este arquivo deve estar presente em todos os plugins que usam mais de um arquivo .py. Ele deve estar vazio e seu nome de arquivo deve estar no formato: plugin-import-name-some_name.txt``. A presença desse arquivo permite importar código dos arquivos .py presentes dentro do arquivo ZIP, usando uma instrução como:

from calibre_plugins.some_name.some_module import some_object

The prefix calibre_plugins must always be present. some_name comes from the filename of the empty text file. some_module refers to some_module.py file inside the ZIP file. Note that this importing is just as powerful as regular Python imports. You can create packages and subpackages of .py modules inside the ZIP file, just like you would normally (by defining __init__.py in each sub-folder), and everything should “just work”.

O nome que você usa para `` some_name`` entra em um namespace global compartilhado por todos os plugins, ** para torná-lo o mais exclusivo possível **. Mas lembre-se de que deve ser um identificador Python válido (apenas alfabetos, números e sublinhado)

__init__.py

Como antes, o arquivo que define a classe de plug-in

main.py

Este arquivo contém o código real que faz algo útil

ui.py

Esse arquivo define a parte de interface do plugin

images/icon.png

O ícone deste plugin

about.txt

Um arquivo de texto com informações sobre o plugin

translations

Uma pasta contendo arquivos .mo com as traduções da interface do usuário do seu plugin para diferentes idiomas. Veja abaixo os detalhes.

Agora vamos ver o código

__init__.py

Primeiro, o obrigatório __init__.py para definir os metadados do plugin:

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


O único recurso digno de nota é o campo actual_plugin. Como o Calibre tem linha de comando e interfaces GUI, plug-ins GUI como este não devem carregar nenhuma biblioteca GUI em __init__.py. O campo actual_plugin faz isso para você, informando ao Calibre que o plugin real deve ser encontrado em outro arquivo dentro do seu arquivo ZIP, que só será carregado em um contexto GUI.

Lembre-se de que, para que isso funcione, você deve ter um arquivo plugin-import-name-some_name.txt no arquivo ZIP do plugin, conforme discutido acima.

Também existem alguns métodos para habilitar a configuração do plugin pelo usuário. Eles são discutidos abaixo.

ui.py

Agora vamos dar uma olhada em ui.py que define o plugin GUI real. O código-fonte é muito comentado e deve ser autoexplicativo:

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

A lógica real para implementar a caixa de diálogo das Interface Plugin Demo.


from calibre_plugins.interface_demo.config import prefs


class DemoDialog(QDialog):

    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        # The current database shown in the GUI
        # db is an instance of the class LibraryDatabase from db/legacy.py
        # This class has many, many methods that allow you to do a lot of
        # things. For most purposes you should use db.new_api, which has
        # a much nicer interface from db/cache.py
        self.db = gui.current_db

        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel(prefs['hello_world_msg'])
        self.l.addWidget(self.label)

        self.setWindowTitle('Interface Plugin Demo')
        self.setWindowIcon(icon)

        self.about_button = QPushButton('About', self)
        self.about_button.clicked.connect(self.about)
        self.l.addWidget(self.about_button)

        self.marked_button = QPushButton(
            'Show books with only one format in the calibre GUI', self)
        self.marked_button.clicked.connect(self.marked)
        self.l.addWidget(self.marked_button)

        self.view_button = QPushButton(
            'View the most recently added book', self)
        self.view_button.clicked.connect(self.view)
        self.l.addWidget(self.view_button)

        self.update_metadata_button = QPushButton(
            'Update metadata in a book\'s files', self)
        self.update_metadata_button.clicked.connect(self.update_metadata)
        self.l.addWidget(self.update_metadata_button)

        self.conf_button = QPushButton(
                'Configure this plugin', self)
        self.conf_button.clicked.connect(self.config)
        self.l.addWidget(self.conf_button)

        self.resize(self.sizeHint())

    def about(self):
        # Get the about text from a file inside the plugin zip file
        # The get_resources function is a builtin function defined for all your
        # plugin code. It loads files from the plugin zip file. It returns
        # the bytes from the specified file.
        #
        # Note that if you are loading more than one file, for performance, you
        # should pass a list of names to get_resources. In this case,
        # get_resources will return a dictionary mapping names to bytes. Names that
        # are not found in the zip file will not be in the returned dictionary.
        text = get_resources('about.txt')
        QMessageBox.about(self, 'About the Interface Plugin Demo',
                text.decode('utf-8'))

    def marked(self):
        ''' Show books with only one format '''
        db = self.db.new_api
        matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
        # Mark the records with the matching ids
        # new_api does not know anything about marked books, so we use the full
        # db object
        self.db.set_marked_ids(matched_ids)

        # Tell the GUI to search for all marked records
        self.gui.search.setEditText('marked:true')
        self.gui.search.do_search()

    def view(self):
        ''' View the most recently added book '''
        most_recent = most_recent_id = None
        db = self.db.new_api
        for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
            if most_recent is None or timestamp > most_recent:
                most_recent = timestamp
                most_recent_id = book_id

        if most_recent_id is not None:
            # Get a reference to the View plugin
            view_plugin = self.gui.iactions['View']
            # Ask the view plugin to launch the viewer for row_number
            view_plugin._view_calibre_books([most_recent_id])

    def update_metadata(self):
        '''
        Set the metadata in the files in the selected book's record to
        match the current metadata in the database.
        '''
        from calibre.ebooks.metadata.meta import set_metadata
        from calibre.gui2 import error_dialog, info_dialog

        # Get currently selected books
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, 'Cannot update metadata',
                             'No books selected', show=True)
        # Map the rows to book ids
        ids = list(map(self.gui.library_view.model().id, rows))
        db = self.db.new_api
        for book_id in ids:
            # Get the current metadata for this book from the db
            mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
            fmts = db.formats(book_id)
            if not fmts:
                continue
            for fmt in fmts:
                fmt = fmt.lower()
                # Get a python file object for the format. This will be either
                # an in memory file or a temporary on disk file
                ffile = db.format(book_id, fmt, as_file=True)
                ffile.seek(0)
                # Set metadata in the format
                set_metadata(ffile, mi, fmt)
                ffile.seek(0)
                # Now replace the file in the calibre library with the updated
                # file. We 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'])

Obtendo recursos do arquivo ZIP do plugin

o sistema de carregamento do plugin do calibre define algumas funções built-in que permitem obter convenientemente os arquivos do arquivo ZIP do plugin.

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.

Habilitando configuração de usuário do seu plugin

Para permitir que os usuários configurem seu plug-in, você deve definir três métodos em sua classe base de plug-in, is_customizable(), config_widget() e save_settings() conforme mostrado abaixo:

    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 tem muitas maneiras diferentes de armazenar dados de configuração (um legado de sua longa história). A maneira recomendada é usar a classe JSONConfig, que armazena suas informações de configuração em um arquivo .json.

O código para gerenciar os dados de configuração no plug-in demo está em config.py:


from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you 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()

O objeto prefs agora está disponível em todo o código do plugin por um simples:

from calibre_plugins.interface_demo.config import prefs

É possível ver o objeto prefs sendo usado em main.py:

    def config(self):
        self.do_user_config(parent=self)
        # Apply the changes
        self.label.setText(prefs['hello_world_msg'])

Editar plug-ins de livros

Now let’s change gears for a bit and look at creating a plugin to add tools to the calibre book editor. The plugin is available here: editor_demo_plugin.zip.

The first step, as for all plugins is to create the import name empty txt file, as described above. We shall name the file plugin-import-name-editor_plugin_demo.txt.

Now we create the mandatory __init__.py file that contains metadata about the plugin – its name, author, version, etc.

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)

A single editor plugin can provide multiple tools each tool corresponds to a single button in the toolbar and entry in the Plugins menu in the editor. These can have sub-menus in case the tool has multiple related actions.

The tools must all be defined in the file main.py in your plugin. Every tool is a class that inherits from the calibre.gui2.tweak_book.plugin.Tool class. Let’s look at main.py from the demo plugin, the source code is heavily commented and should be self-explanatory. Read the API documents of the calibre.gui2.tweak_book.plugin.Tool class for more details.

main.py

Here we will see the definition of a single tool that will multiply all font sizes in the book by a number provided by the user. This tool demonstrates various important concepts that you will need in developing your own plugins, so you should read the (heavily commented) source code carefully.

import re

from css_parser.css import CSSRule
from qt.core import QAction, QInputDialog

from calibre import force_unicode
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize
from calibre.gui2 import error_dialog

# The base class that all tools must inherit from
from calibre.gui2.tweak_book.plugin import Tool


class DemoTool(Tool):

    #: Set this to a unique name it will be used as a key
    name = 'demo-tool'

    #: If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True

    #: If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    def create_action(self, for_toolbar=True):
        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui)  # noqa
        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

Let’s break down main.py. We see that it defines a single tool, named Magnify fonts. This tool will ask the user for a number and multiply all font sizes in the book by that number.

The first important thing is the tool name which you must set to some relatively unique string as it will be used as the key for this tool.

The next important entry point is the calibre.gui2.tweak_book.plugin.Tool.create_action(). This method creates the QAction objects that appear in the plugins toolbar and plugin menu. It also, optionally, assigns a keyboard shortcut that the user can customize. The triggered signal from the QAction is connected to the ask_user() method that asks the user for the font size multiplier, and then runs the magnification code.

The magnification code is well commented and fairly simple. The main things to note are that you get a reference to the editor window as self.gui and the editor Boss as self.boss. The Boss is the object that controls the editor user interface. It has many useful methods, that are documented in the calibre.gui2.tweak_book.boss.Boss class.

Finally, there is self.current_container which is a reference to the book being edited as a calibre.ebooks.oeb.polish.container.Container object. This represents the book as a collection of its constituent HTML/CSS/image files and has convenience methods for doing many useful things. The container object and various useful utility functions that can be reused in your plugin code are documented in Documentação da API para ferramentas de edição de e-book.

Adicionando traduções para o seu plugin

You can have all the user interface strings in your plugin translated and displayed in whatever language is set for the main calibre user interface.

The first step is to go through your plugin’s source code and mark all user visible strings as translatable, by surrounding them in _(). For example:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Then use some program to generate .po files from your plugin source code. There should be one .po file for every language you want to translate into. For example: de.po for German, fr.po for French and so on. You can use the Poedit program for this.

Send these .po files to your translators. Once you get them back, compile them into .mo files. You can again use Poedit for that, or just do:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Put the .mo files into the translations folder in your plugin.

The last step is to simply call the function load_translations() at the top of your plugin’s .py files. For performance reasons you should only call this function in those .py files that actually have translatable strings. So in a typical User Interface plugin you would call it at the top of ui.py but not __init__.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

Replace de with the language code of the language you want to test.

For translations with plurals, use the ngettext() function instead of _(). For example:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

A API do plugin

As you may have noticed above, a plugin in calibre is a class. There are different classes for the different types of plugins in calibre. Details on each class, including the base class of all plugins can be found in Documentação de API para plug-ins.

Your plugin is almost certainly going to use code from calibre. To learn how to find various bits of functionality in the calibre code base, read the section on the calibre Layout do código.

Depurando plugins

A primeira e mais importante etapa é executar o calibre no modo de depuração. Você pode fazer isso na linha de comando com:

calibre-debug -g

Or from within calibre by right-clicking the Preferences button or using the Ctrl+Shift+R keyboard shortcut.

Ao executar a partir da linha de comando, a saída de depuração será impressa no console; ao executar a partir do Calibre, a saída irá para um arquivo .txt.

É possível inserir declarações print() em qualquer lugar do código do plugin, elas serão geradas no modo de depuração. Lembre-se, isso é Python, você realmente não precisa de nada além de declarações print() para depurar ;) Desenvolvi todo o Calibre usando apenas esta técnica de depuração.

Você pode testar rapidamente as alterações em seu plug-in usando a seguinte linha de comando:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Isso desligará o calibre em execução, esperará até que o desligamento seja concluído e, em seguida, atualizará o calibre do plug-in e o reiniciará.

Mais exemplos de plugins

You can find a list of many sophisticated calibre plugins here.

Compartilhando seus plugins com os outros

Se você gostaria de compartilhar os plug-ins que criou com outros usuários do calibre, poste seu plug-in em um novo tópico no fórum de plug-ins do calibre.