Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Script Runner

Script Runner is a calibre plugin that runs saved Python user scripts when books are added or removed. It also provides named manual actions, a configurable editable workspace, recipes, and a separate toolbar/menu plugin ZIP.

A typical setup keeps the event hook as a small wrapper:

# on_book_added.py
from actions import _on_book_added


def on_book_added(book):
    _on_book_added(book)

The user-owned actions.py contains the workflow. The following is an example:

# actions.py
from script_lib.goodreads import get_goodreads_metadata


def _on_book_added(book):
    book.add_tag("TODO")

    # Normalize tags according to Tag Mapper
    book.apply_toolbar_tag_mapper()

    if not book.has_identifier("isbn"):
        book.log.info("ISBN missing; starting extraction for book", book.book_id)
        isbn = book.extract_isbn(keep_existing=False)
        if isbn:
            book.set_identifier("isbn", isbn, overwrite=False)
            book.log.info("ISBN added for book", book.book_id, isbn)
        else:
            book.log.info("No ISBN found for book", book.book_id)
    else:
        book.log.info("ISBN already present; extraction skipped for book", book.book_id)

    if not book.has_identifier("isbn"):
        book.log.info("Goodreads lookup skipped: ISBN is missing")
        return

    match = get_goodreads_metadata(
        book,
        require_absolute_match=True,
        minimum_title_similarity=None,
    )
    if match is not None:
        book.set_identifier("goodreads", match.goodreads_id, overwrite=False)
        book.set_title(match.title)

The fresh-install hook is intentionally a no-op: installing Script Runner does not change book metadata. Create actions.py and replace the hook with the wrapper above when you are ready to enable an automatic workflow.

Install

  1. Download matching versions of the Script Runner and Script Runner Toolbar ZIPs from the desired GitHub release.
  2. Optional: install and enable the Extract ISBN plugin from kiwidude's calibre plugins if your scripts will call book.extract_isbn().
  3. Open calibre > Preferences > Plugins.
  4. Choose Load plugin from file and select the Script Runner ZIP.
  5. Choose Load plugin from file again and select the Script Runner Toolbar ZIP.
  6. Restart calibre.
  7. Open Preferences > Plugins > Script Runner > Customize plugin to edit the saved Python script.

Add the Script Runner action to a calibre toolbar or menu from Preferences > Toolbars & menus if calibre does not show it automatically.

The editable script workspace defaults to calibre's configuration directory under plugins/script_runner/, outside the plugin ZIP. Its location can be changed at the top of the Customize window. New file, Import file, Rename, and Delete manage custom .py files; the event-hook and helper bootstrap files are protected and can be restored from the plugin defaults.

Default workspace:

plugins/script_runner/
|-- on_book_added.py
|-- on_book_removed.py
`-- script_lib/
    `-- __init__.py

The ZIP contains the stable engine and only seeds this minimal workspace. User actions and helpers remain ordinary files outside the ZIP. In particular, installing or updating the plugin never creates, restores, protects, or changes actions.py. The editor only writes it when the user explicitly saves an edit. Users own that file completely, so adding or editing actions does not require a Script Runner release. Plugin updates are reserved for the editor, calibre integration, job handling, and the stable script API.

Book Removal Hook

The toolbar plugin listens for books removed through the running calibre GUI from the current library and calls on_book_removed(removed) once for each removed database ID. The fresh-install hook is a no-op. You can edit on_book_removed.py to perform work that only needs the removal identifiers:

def on_book_removed(removed):
    removed.log.info(
        "Removed book",
        removed.book_id,
        "from library",
        removed.library_id,
    )

The removed object provides only:

  • script_api_version
  • book_id
  • library_id
  • log

Calibre sends this event after it has removed the book record from the active library database, and the event supplies only the removed IDs. Consequently, this hook cannot access the title, authors, identifiers, comments, formats, or any other book metadata, and it does not receive book.db or book.api. If a removal workflow needs metadata, it must save that information before the book is removed.

The hook runs for both moving a book to calibre's library trash and deleting it permanently. Calibre does not tell the listener which kind of removal occurred.

Recipes

Optional examples live under recipes/ in the repository and are not included in either plugin ZIP. The Goodreads recipe provides a manual action that looks up a book by ISBN, uses exact normalized base-title matching by default, and adds the goodreads:<bookId> identifier. Its optional fuzzy threshold and metadata handling are configured in the user's actions.py. The Count Pages recipe uses that plugin's public API to calculate selected statistics. Each recipe README lists the helper's workspace-relative import path and the small function to merge into the user-owned actions.py.

Toolbar And Menu Actions

The toolbar action opens the script editor. Its menu provides:

  • Configure scripts
  • Open workspace folder
  • Named book actions discovered from workspace scripts
  • View script log

The Script Runner log viewer updates once per second and follows new entries by default. Scrolling upward turns off Follow latest and preserves the current reading position while more lines arrive. Enable Follow latest again to jump to the newest entry. This viewer is separate from calibre's job-details window.

Named book actions create calibre background jobs visible in calibre's Jobs UI. Each action runs against the current selected books. Completion remains in calibre's Jobs UI and the status bar; successful runs do not open a popup. Different named actions use separate job types, so a short action can run while another named action is still working. Repeated runs of the same action remain serialized. Avoid running actions concurrently when they update the same field on the same books, because the last write may replace the earlier one.

The toolbar menu also discovers top-level functions in root-level workspace .py files, excluding script_lib/, that can be called with a single book argument. Create and maintain actions.py yourself as the intended home for actions you want to run manually on existing selected books. Set a literal menu_name string inside the function to control the menu label. If no menu_name is present, the formatted function name becomes the menu label. Actions appear in the same order as their function definitions in each file. The menu refreshes from the workspace when those scripts change:

def add_verified_tag(book):
    menu_name = "Add VERIFIED tag"
    book.add_tag("VERIFIED")

Extract ISBN Integration

book.extract_isbn() is a non-mutating lookup. Scripts explicitly decide whether to store its return value with book.set_identifier(). Scanning is delegated to the separately installed Extract ISBN plugin through its calibre_plugins.extract_isbn.jobs.scan_for_isbn() callable. If that plugin is missing, disabled, or incompatible, extraction logs a clear error and returns None.

Script Runner contains only the dependency adapter. It does not package Extract ISBN's scanner implementation.

What Scripts Can Do with book

Both the on_book_added(book) hook and named toolbar actions receive a BookContext object named book. It provides:

  • script_api_version (currently (2, 2))
  • book_id
  • db and api
  • title, authors, tags, languages, comments, formats, identifiers
  • format_paths
  • get_field(name)
  • set_field(name, value)
  • set_title(title)
  • request_plugin_call(plugin_name, method_name, *, args=None, kwargs=None, merge_list_argument=None) (manual toolbar actions only)
  • clear_field(name)
  • add_tag(name)
  • remove_tag(name)
  • apply_toolbar_tag_mapper()
  • set_comments(html)
  • clear_comments()
  • set_languages(*codes)
  • add_language(code)
  • remove_language(code)
  • clear_languages()
  • has_language(code)
  • has_identifier(name)
  • get_identifier(name, default=None)
  • set_identifier(name, value, overwrite=True)
  • remove_identifier(name)
  • has_format(name)
  • is_format(name)
  • has_any_format(*names)
  • is_any_format(*names)
  • get_format_path(name)
  • clear_rating()
  • extract_isbn(keep_existing=True) (requires the optional Extract ISBN plugin from installation step 2; returns an ISBN without changing metadata)
  • log

get_field(name) reads any calibre metadata field by its lookup name. set_field(name, value) replaces that field's complete value for the current book. For example:

book.set_field("publisher", "Example Press")
book.set_field("languages", ["sv", "en"])

For fields containing multiple values, set_field() does not append or merge: the supplied list becomes the complete new value. The dedicated language helpers make incremental changes clearer:

book.set_languages("sv", "en")
book.add_language("fr")
book.remove_language("en")
book.clear_languages()

book.apply_toolbar_tag_mapper() applies the rules most recently accepted in calibre's toolbar Tag Mapper to the current book only. It returns True when the tags changed and False when there are no toolbar rules or the tags were already normalized. Call it after any script steps that add or edit tags:

def _on_book_added(book):
    book.add_tag("Imported")
    book.apply_toolbar_tag_mapper()

The toolbar configuration is a snapshot of the last rules accepted in its dialog. Editing a named saved ruleset does not update that snapshot until the ruleset is loaded in the toolbar Tag Mapper and the dialog is accepted.

Language helpers accept two- or three-letter ISO 639 codes such as sv, en, and fr. Calibre stores and returns their canonical three-letter forms.

Calibre uses the lookup name comments for the description or back-cover text and allows it to contain HTML. book.comments returns the stored value, set_comments(html) replaces it exactly, and clear_comments() removes it. Script Runner does not sanitize, escape, or convert the content:

description = book.comments
book.set_comments("<p>A revised description.</p>")

The same API tuple is available as SCRIPT_API_VERSION in every user-script namespace. Scripts should prefer these BookContext methods over direct access through book.api; the direct database handles remain available for advanced workflows.

Warning

Scripts run as local Python code. They can import modules and access the local filesystem, so only save scripts you trust.

License

Script Runner is distributed under the MIT License.

Acknowledgements

Thanks to kiwidude for creating and maintaining an excellent collection of calibre plugins, including Extract ISBN.

Releases

Packages

Contributors

Languages