zotero-index

Query a Zotero library offline, from a Better BibTeX auto-export. No Zotero install, no web API, no API key: just the export file, e.g. synced to your other machines by a cloud drive.

zotero-index path smith_example_2020 --ext pdf     # where is the PDF?
zotero-index show smith_example_2020               # the record
zotero-index doi 10.1234/example.2020              # which citekey has this DOI?
zotero-index search usable security --year 2018-   # one line per hit: citekey  year  first-author  title
zotero-index check                                 # data-quality report

One file, standard library only, run with uv.

Why

Zotero knows where every attachment lives, but only on the machine running Zotero, and only while it runs. Better BibTeX can auto-export the whole library as JSON and keep that file updated; put it in a synced folder and every other machine has the library's metadata, collections and attachment paths too. What it lacks is a way to ask it anything: the export is several MB of nested JSON, and its attachment paths are absolute paths on the exporting host (C:\Users\alice\Sync\Zotero\sm\smith_example_2020.pdf), useless as they stand on a Linux box where the same folder is ~/sync/Zotero.

This tool reads the export, rewrites the paths for the host it runs on, and answers the questions you actually have: where is the file for this citekey, which citekey is this DOI, what's in this collection.

Install

Symlink it onto your PATH; nothing in it depends on where it lives.

ln -s ~/src/tools/zotero-index/zotero-index.py ~/.local/bin/zotero-index

Set up Better BibTeX

In Zotero: right-click My Library → Export Library… → format BetterBibTeX JSON, tick Keep updated, and save it somewhere synced. Include notes if you like; they are skipped. Better BibTeX then rewrites the file as the library changes, whenever Zotero is running.

Configure

$XDG_CONFIG_HOME/zotero-index/config.toml (default ~/.config/zotero-index/config.toml):

export = "~/sync/Zotero/library.json"

[path_map]   # prefix in the export = prefix to emit; longest match wins
'C:\Users\alice\Sync\Zotero' = "~/sync/Zotero"
'C:\Users\alice\Sync\Zotero\Archive' = "/mnt/archive/zotero"

export can also be a list, for a library exported from more than one machine; the most recently modified file is used:

export = ["~/sync/Zotero/library.json", "/var/lib/zotero/library-local.json"]

--config FILE reads a different config; --export FILE overrides the export path or list, and with it the config becomes optional. A missing config or export is a short error naming the path it expected.

Commands

Command Prints Exit 1 when
path CITEKEY [--ext pdf]… attachment paths, one per line no (mapped) attachment matches
show CITEKEY the record: type, title, authors, year, DOI, tags, collections, attachments, abstract no such citekey
doi DOI the citekey(s) with that DOI (any case; a https://doi.org/ or doi: prefix is fine) no match
search [TERMS…] [--collection C] [--tag T]… [--year Y] citekey year first-author title per hit no hits
check citekeys shared by several items, attachment filenames that don't start with their citekey, items without attachments, unmapped paths never

A citekey shared by several items is never resolved silently. path and show print every item with that key, list them on stderr, and exit 2. (Better BibTeX can produce shared keys, e.g. when an item was added twice; check lists them so you can merge the duplicates in Zotero.)

Exit status 3 means the config or the export is missing or unreadable.

The index

Reading a multi-MB export on every call would be slow, especially if it sits on a network or FUSE mount, so the tool keeps a slim index in $XDG_CACHE_HOME/zotero-index/ (default ~/.cache/zotero-index/), one per export.

Every run stats each export and the config once, picks the newest export, and compares its mtime and size with the values its index was built from. If anything changed, it re-reads that export and rebuilds; otherwise the export is not opened at all, and the others never are. The index is written to a temp file and renamed into place, so concurrent runs are safe. There is no file watcher and nothing to schedule: the next query after the export changes picks it up. Delete the cache directory to force a rebuild.

Not in scope

Writing to Zotero, the Zotero web API, full-text search of attachments, watching files.

Tests

uv run tests/test_zotero_index.py

They run against a small synthetic export, tests/export.json, which doubles as an example of the format.

Contents

📁tests/
📄README.md6.3 KB
📄zotero-index.py16.5 KB