feat(zotero): add Zotero connector#61
Conversation
Extracts text from PDF attachments in a Zotero collection (and its subcollections) and exposes them as .txt files for sync. Collection hierarchy maps to KB directories. Read-only with respect to Zotero. Checksum mode is configurable via ZOTERO_CHECKSUM: 'version' (cheap, default) or 'content' (hashes extracted text). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Personal, untracked scripts and configs (e.g. the Zotero sync cheatsheet) live in perso/ and should not be tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The diff's mkdir list had no guaranteed order, so a nested directory could be processed before its parent. Since the parent's id is looked up from directory_map (populated as dirs are created), an out-of-order child would get parent_id=None and be created at the wrong level. Sorting lexicographically puts "a" before "a/b", guaranteeing parent-first creation, and also makes the dry-run "Dirs to create" output stable and readable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a Zotero attachment's bytes aren't in zotero.org storage (file sync off, storage quota exceeded, WebDAV-only, or a web link), the /file endpoint returns 404. In 'version' checksum mode this already surfaced per-file at upload time and the rest of the sync completed. But in 'content' mode the download happens during build_manifest, so a single 404 propagated out of run_sync and aborted the entire run with nothing uploaded. Make _checksum degrade to the version checksum when text can't be retrieved, so the manifest is always built and the failure is reported through the normal per-file upload error path (sync everything we can, list what failed at the end). Also wrap the file-download failure in a clear message explaining the bytes aren't in Zotero storage instead of dumping a raw HTTP 404. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A file the source advertises in its manifest but cannot actually provide content for (e.g. a Zotero attachment whose bytes aren't in storage: a web link, linked file, or WebDAV-only item) is a source-side data gap, not an oikb failure. Previously any such case landed in result.errors and made the whole sync exit 1, so a library with a few file-less attachments could never report success (a recurring systemd/daemon run would always look "failed"). Introduce a dedicated SourceFileUnavailable exception in the connectors base. The Zotero connector raises it (instead of a bare RuntimeError) for the no-downloadable-file case. run_sync catches it specifically (no retry) and routes the file to a new result.warnings list instead of result.errors; the CLI and daemon surface warnings but only result.errors affects the exit code and success/partial status. Any other read_file() exception still fails the run exactly as before, so this narrows to that one error class only. Tests cover all three paths: the Zotero missing-file mapping, warning routing in run_sync, and that a generic read failure is still an error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Zotero connector port has been opened as a draft PR against the official open-webui/oikb project (open-webui/oikb#61). Update the deprecation notice to point at it; this repo will be sunset once it lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Nice work on this! One thing that'd make it even more useful: an option to also pull in notes and highlights/annotations, not just the PDF body text. A lot of the value in Zotero libraries lives in child notes and PDF annotations, and right now those get skipped since _collect_items only keeps itemType == "attachment" children. Maybe something like a ZOTERO_INCLUDE_NOTES / ZOTERO_INCLUDE_ANNOTATIONS flag that appends note content and annotation text/comments to (or alongside) the extracted PDF text? |
|
Hey, got the connector working with a Docker setup. Works great for items in collections but I noticed items sitting directly in "My Library" (unfiled, not in any collection) never get picked up regardless of whether you use Makes sense given |
Optionally append child-note text and PDF highlights/comments to each attachment's extracted .txt, since much of a Zotero library's value lives in notes and annotations that were previously skipped. - Notes come back with the item's children (no extra API calls); items that have notes but no PDF now surface as their own notes-only .txt so enabling the flag never silently drops content. - Annotations are children of the attachment, not the item, so they cost one extra children() call per attachment (in version mode; content mode already downloads everything). - Change detection stays correct: content mode hashes the assembled text, and version mode folds in note/annotation versions (editing a note or annotation bumps its own version, not the parent item's). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fake-pyzotero tests for both new flags: notes and annotations appended to the PDF .txt, note-only items surfacing as their own file, both flags off reproducing PDF-only output, and version-mode checksums reacting to note and annotation version bumps (each edit bumps its own version, not the parent's). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the two flags to the README env table, explain the append layout and the per-attachment API cost of annotations in version mode, and add an Unreleased CHANGELOG entry. Documentation updated with Claude Code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Items sitting directly in the library (in no collection) were invisible: the manifest only walked the collection tree. A whole-library sync (bare "zotero:") now also collects them and lands them in a virtual directory, ZOTERO_UNFILED_DIR (default "_unfiled"). A named hierarchy is unchanged and never includes them. The Zotero API has no "unfiled" query, so we fetch top-level items via top() and keep the ones whose data.collections is empty. Per-item processing (attachments, notes, skip rules) is refactored out of _collect_items into a shared _process_item so the collection walk and the unfiled sweep can't drift; the exclude check becomes _is_excluded, letting ZOTERO_EXCLUDE=<unfiled dir> drop it. The existing notes/annotations tests gain a top() on their fake client and mark their items as filed, so the new sweep correctly finds nothing unfiled there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bare zotero: surfaces loose items under _unfiled/ (note-only ones gated on ZOTERO_INCLUDE_NOTES), a named hierarchy never does, filed items are not duplicated into _unfiled/, ZOTERO_UNFILED_DIR renames the directory, and ZOTERO_EXCLUDE can drop it entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_DIR Note that bare zotero: now also syncs items in no collection into ZOTERO_UNFILED_DIR (default _unfiled/), explain the top() fetch and that a named hierarchy never sweeps unfiled items, add the env-table row and an Unreleased CHANGELOG entry. Docs updated with Claude Code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @psuckrow-routine , thanks for giving it a try and thanks for the nice words. I pushes a few things. Can you try and see if it solves the issues you raised? |
|
Hey, thanks so much for the work. Yes I will try it out and let you know ! |
|
When Steps to reproduce:
Running Expected: per-file error messages — when a file fails, show the filename and the actual error (HTTP status / exception), e.g.: Actual: |
|
WebDAV storage support When Zotero is configured to use WebDAV storage instead of Zotero's native cloud storage, This is expected — Zotero's API doesn't proxy WebDAV files. But oikb currently has no way to reach the WebDAV server directly. Feature request: add In the meantime, notes and annotations ( |
|
Daemon interval sync not firing When running Tested with Expected: the daemon should run the first sync shortly after startup and then repeat on the configured interval. |
Summary
Adds a Zotero connector to oikb. It syncs the text of the PDF attachments of items in a Zotero collection (and its subcollections) into an Open WebUI Knowledge Base, mapping the Zotero collection hierarchy onto KB directories. It is read-only with respect to Zotero: it never modifies, deletes, or adds anything to the library.
This is a Claude Code port of an earlier standalone tool I wrote, https://github.com/thiswillbeyourgithub/openwebui-knowledge-zotero-sync, reworked to fit oikb's connector interface.
Zotero connector
zotero:<hierarchy>source scheme.%%separates collection names(e.g.
zotero:Research%%Machine Learning); a barezotero:syncs every top-level collection.ZOTERO_CHECKSUM:version(cheap, hashes the Zotero item version, no download, default) orcontent(hashes the extracted text).ZOTERO_EXCLUDEskips subcollections, relative to the synced root.ZOTERO_LIBRARY_ID,ZOTERO_API_KEY,ZOTERO_LIBRARY_TYPE,ZOTERO_CHECKSUM,ZOTERO_EXCLUDE.pip install oikb[zotero](pyzotero + pymupdf).Sync change: unavailable source files are warnings, not fatal errors
A file the source advertises in its manifest but cannot provide content for (e.g. a Zotero attachment whose bytes aren't in storage: a web link, a linked file, or a WebDAV-only item) is a source-side data gap, not an oikb failure. Previously any such case landed in
result.errorsand exited the whole sync with code 1.This PR adds a
SourceFileUnavailableexception to the connector base.run_synccatches it specifically (no retry) and routes the file to a newresult.warningslist instead of
result.errors, so a sync whose only problems are unavailable source files still succeeds (exit 0). Any otherread_file()exception still fails the run exactly as before. The CLI and daemon surface warnings separately from errors; onlyresult.errorsaffects the exit code / success status.Tests
tests/test_sync_warnings.pycovers all three paths: the Zotero missing-file mapping toSourceFileUnavailable, warning routing inrun_sync, and that a generic read failure is still an error.Notes