| Title: | Access 'Zotero' Libraries |
|---|---|
| Description: | Interface to the 'Zotero' reference manager <https://www.zotero.org> through its public web API and local client database. Provides paginated reads, versioned writes, read-only local queries, batch plans and resumable ledgers, bibliographic comparison, metadata mapping, file attachment, and optional semantic-index queries. |
| Authors: | Alejandro Verri Kozlowski [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-8535-1170>) |
| Maintainer: | Alejandro Verri Kozlowski <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.2 |
| Built: | 2026-09-22 15:08:16 UTC |
| Source: | https://github.com/cran/zot |
Stores a preview digest, optional notes, and a timestamp in an object used
by zotExecute().
zotApprove(preview, notes = "")zotApprove(preview, notes = "")
preview |
A |
notes |
Free-text metadata associated with the record. |
This function does not authenticate a person, evaluate an authorization
policy, display the plan, or perform a write. The calling application
determines how approval records are created. A preview should not be
modified after this record is created because zotExecute() compares stored
digests and does not recalculate the plan digest.
A zotApproval containing the preview digest.
zotPreview() and zotExecute().
Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "ABCD1234", payload = list(list(title = "A reviewed title")) ) Preview <- zotPreview(Plan, describe = "Synthetic documentation batch") Approval <- zotApprove(Preview, notes = "Documentation example") Approval$digest == Preview$digestPlan <- data.table::data.table( action = "patch", library = "user", itemKey = "ABCD1234", payload = list(list(title = "A reviewed title")) ) Preview <- zotPreview(Plan, describe = "Synthetic documentation batch") Approval <- zotApprove(Preview, notes = "Documentation example") Approval$digest == Preview$digest
Creates a configuration object for requests to the Zotero Web API. The object stores the user identifier, API key, and an optional request performer used by tests or alternative transports. Arguments may be passed explicitly or read from environment variables, which supports interactive, scripted, and agent-driven use.
zotConfig( userID = Sys.getenv("ZOTERO_LIBRARY_ID"), key = Sys.getenv("ZOTERO_API_KEY"), performer = NULL )zotConfig( userID = Sys.getenv("ZOTERO_LIBRARY_ID"), key = Sys.getenv("ZOTERO_API_KEY"), performer = NULL )
userID |
Zotero user identifier. The default reads
|
key |
Zotero API key. The default reads |
performer |
Optional function used by |
An object of class zotConfig.
Config <- zotConfig(userID = "42", key = "example-key") ConfigConfig <- zotConfig(userID = "42", key = "example-key") Config
Requires matching approval and preview digests and refuses a ledger written for a different digest. Completed ledger entries are skipped when the same batch resumes. The ledger file is rewritten periodically and at the end.
zotExecute(config, preview, approval, ledgerPath)zotExecute(config, preview, approval, ledgerPath)
config |
A |
preview |
The |
approval |
Its |
ledgerPath |
JSON ledger file; reuse the same path to resume. |
Patch operations read the current item version, use a conditional write,
and compare requested fields with a subsequent API read. Create operations
record the new key after it can be read. Trash operations use the status
returned by zotTrash(). The approval record is application metadata; this
function does not independently determine who authorized the batch.
The ledger as a list (class zotLedger).
zotPreview(), zotApprove(), and zotVerify().
Store <- new.env(parent = emptyenv()) Store$version <- 1L Store$title <- "Before" Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(), body = list(version = Store$version, data = list(title = Store$title)) )) } Store$title <- body$title Store$version <- Store$version + 1L list(status = 204L, headers = list(), body = NULL) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "K1", payload = list(list(title = "After")) ) Preview <- zotPreview(Plan, describe = "Synthetic offline patch") Approval <- zotApprove(Preview, notes = "Documentation example") LedgerPath <- tempfile(fileext = ".json") Ledger <- zotExecute(Config, Preview, Approval, ledgerPath = LedgerPath) Ledger$items$K1$status unlink(LedgerPath)Store <- new.env(parent = emptyenv()) Store$version <- 1L Store$title <- "Before" Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(), body = list(version = Store$version, data = list(title = Store$title)) )) } Store$title <- body$title Store$version <- Store$version + 1L list(status = 204L, headers = list(), body = NULL) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "K1", payload = list(list(title = "After")) ) Preview <- zotPreview(Plan, describe = "Synthetic offline patch") Approval <- zotApprove(Preview, notes = "Documentation example") LedgerPath <- tempfile(fileext = ".json") Ledger <- zotExecute(Config, Preview, Approval, ledgerPath = LedgerPath) Ledger$items$K1$status unlink(LedgerPath)
Follows Total-Results with start/limit pagination and returns the
concatenated item list.
zotGetAll(config, path, query = list(), limit = 100L)zotGetAll(config, path, query = list(), limit = 100L)
config |
A |
path |
API path starting at the host root (see |
query |
Named list of query parameters. |
limit |
Page size. |
A list of parsed items.
zotGet() and zotRequest().
Performer <- function(config, method, path, query, body, version) { Page <- if (query$start == 0L) { list(list(key = "A"), list(key = "B")) } else { list(list(key = "C")) } list(status = 200L, headers = list(`total-results` = "3"), body = Page) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Items <- zotGetAll(Config, path = "/users/42/items", limit = 2L) vapply(Items, function(Item) Item$key, character(1L))Performer <- function(config, method, path, query, body, version) { Page <- if (query$start == 0L) { list(list(key = "A"), list(key = "B")) } else { list(list(key = "C")) } list(status = 200L, headers = list(`total-results` = "3"), body = Page) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Items <- zotGetAll(Config, path = "/users/42/items", limit = 2L) vapply(Items, function(Item) Item$key, character(1L))
Build a Zotero library API path
zotLibraryPath(config, library = "user")zotLibraryPath(config, library = "user")
config |
A |
library |
|
The API path prefix for the selected library.
zotConfig() and zotRequest().
Config <- zotConfig(userID = "42", key = "example-key") zotLibraryPath(Config) zotLibraryPath(Config, library = "5107760")Config <- zotConfig(userID = "42", key = "example-key") zotLibraryPath(Config) zotLibraryPath(Config, library = "5107760")
Returns key, library, type, title, and dates for non-trashed items as a
data.table. See zotLocalQuery() for synchronization and read-only details.
zotLocalItems( library = NULL, path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite") )zotLocalItems( library = NULL, path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite") )
library |
|
path |
Database file. |
A data.table with key, groupID (0 for personal), itemType,
title, dateAdded and dateModified.
zotLocalQuery(), ztCensus(), and ztSemantic().
# A temporary database with the tables used by zotLocalItems(). Path <- tempfile(fileext = ".sqlite") Connection <- DBI::dbConnect(RSQLite::SQLite(), Path) DBI::dbWriteTable(Connection, "libraries", data.frame(libraryID = 1:2)) DBI::dbWriteTable(Connection, "groups", data.frame(libraryID = 2L, groupID = 555L)) DBI::dbWriteTable(Connection, "itemTypes", data.frame(itemTypeID = 1L, typeName = "journalArticle")) DBI::dbWriteTable(Connection, "items", data.frame( itemID = 1:3, key = c("AAA", "BBB", "CCC"), libraryID = c(1L, 2L, 1L), itemTypeID = 1L, dateAdded = "2026-01-01", dateModified = "2026-01-01" )) DBI::dbWriteTable(Connection, "fieldsCombined", data.frame(fieldID = 110L, fieldName = "title")) DBI::dbWriteTable(Connection, "itemData", data.frame(itemID = 1:3, fieldID = 110L, valueID = 1:3)) DBI::dbWriteTable(Connection, "itemDataValues", data.frame( valueID = 1:3, value = c("Personal article", "Group article", "Trashed article") )) DBI::dbWriteTable(Connection, "deletedItems", data.frame(itemID = 3L)) DBI::dbDisconnect(Connection) zotLocalItems(library = "user", path = Path) zotLocalItems(library = 555, path = Path) unlink(Path)# A temporary database with the tables used by zotLocalItems(). Path <- tempfile(fileext = ".sqlite") Connection <- DBI::dbConnect(RSQLite::SQLite(), Path) DBI::dbWriteTable(Connection, "libraries", data.frame(libraryID = 1:2)) DBI::dbWriteTable(Connection, "groups", data.frame(libraryID = 2L, groupID = 555L)) DBI::dbWriteTable(Connection, "itemTypes", data.frame(itemTypeID = 1L, typeName = "journalArticle")) DBI::dbWriteTable(Connection, "items", data.frame( itemID = 1:3, key = c("AAA", "BBB", "CCC"), libraryID = c(1L, 2L, 1L), itemTypeID = 1L, dateAdded = "2026-01-01", dateModified = "2026-01-01" )) DBI::dbWriteTable(Connection, "fieldsCombined", data.frame(fieldID = 110L, fieldName = "title")) DBI::dbWriteTable(Connection, "itemData", data.frame(itemID = 1:3, fieldID = 110L, valueID = 1:3)) DBI::dbWriteTable(Connection, "itemDataValues", data.frame( valueID = 1:3, value = c("Personal article", "Group article", "Trashed article") )) DBI::dbWriteTable(Connection, "deletedItems", data.frame(itemID = 3L)) DBI::dbDisconnect(Connection) zotLocalItems(library = "user", path = Path) zotLocalItems(library = 555, path = Path) unlink(Path)
Opens the local zotero.sqlite database in read-only mode. Local records
can lag the Web API until the desktop client synchronizes, and WAL mode can
make recent local changes temporarily unavailable to another reader.
zotLocalQuery( sql, params = list(), path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite") )zotLocalQuery( sql, params = list(), path = file.path(Sys.getenv("HOME"), "Zotero", "zotero.sqlite") )
sql |
One read-only SQL statement. |
params |
Positional query parameters. |
path |
Database file (default: the standard Zotero location). |
The database connection is opened with SQLite's read-only flag and closed
when the query finishes. zotLocalQuery() does not copy or modify the
desktop database. The caller remains responsible for supplying a read-only
SQL statement. The default path is ~/Zotero/zotero.sqlite; pass path
explicitly for a
portable installation or a custom Zotero data directory.
Query result as a data.table.
zotLocalItems(), zotRequest(), and ztSemantic().
Path <- tempfile(fileext = ".sqlite") Connection <- DBI::dbConnect(RSQLite::SQLite(), Path) DBI::dbWriteTable(Connection, "items", data.frame(key = c("A", "B"))) DBI::dbDisconnect(Connection) zotLocalQuery("SELECT key FROM items ORDER BY key", path = Path) unlink(Path)Path <- tempfile(fileext = ".sqlite") Connection <- DBI::dbConnect(RSQLite::SQLite(), Path) DBI::dbWriteTable(Connection, "items", data.frame(key = c("A", "B"))) DBI::dbDisconnect(Connection) zotLocalQuery("SELECT key FROM items ORDER BY key", path = Path) unlink(Path)
Validates and summarizes a plan for execution by zotExecute(). A row has
action "patch", "create", or "trash"; a user or group library; an
item key for patch and Trash rows; and a payload list.
zotPreview(plan, describe)zotPreview(plan, describe)
plan |
A data.table with columns |
describe |
Text identifying the batch. |
zotPreview() validates the plan, rejects unknown actions and recognized
source labels, copies the rows, and calculates a digest from a canonical JSON
representation. It does not contact Zotero.
A zotPreview: the plan, its canonical SHA-256 digest, counts
and creation time.
zotApprove(), zotExecute(), and zotVerify().
Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "ABCD1234", payload = list(list(title = "A reviewed title")) ) Preview <- zotPreview(Plan, describe = "Correct one title") PreviewPlan <- data.table::data.table( action = "patch", library = "user", itemKey = "ABCD1234", payload = list(list(title = "A reviewed title")) ) Preview <- zotPreview(Plan, describe = "Correct one title") Preview
Core transport for every zot API call: bounded retries, Backoff and
Retry-After honoring, and conditional writes through
If-Unmodified-Since-Version. Returns for every HTTP status — callers
interpret status (a 404 is a verdict, not a transport failure); only an
exhausted transport raises a zotHttpError.
zotRequest(config, method, path, query = list(), body = NULL, version = NULL) zotGet(config, path, query = list()) zotPost(config, path, body) zotPatch(config, path, body, version)zotRequest(config, method, path, query = list(), body = NULL, version = NULL) zotGet(config, path, query = list()) zotPost(config, path, body) zotPatch(config, path, body, version)
config |
A |
method |
HTTP method: |
path |
API path starting at the host root (see |
query |
Named list of query parameters. |
body |
Request body, serialized as JSON when not |
version |
Value for |
A performer receives config, method, path, query, body, and
version, and returns a list with status, headers, and body. This
makes the complete HTTP boundary replaceable in examples and tests without
changing the public request contract. Responses eligible for retry are
bounded by five attempts; other HTTP statuses are returned unchanged.
A list with status, headers (named list) and body (parsed
JSON, or NULL when empty).
zotGetAll(), zotTrash(), and zotPreview().
Performer <- function(config, method, path, query, body, version) { list( status = if (method == "POST") 201L else 200L, headers = list(), body = list(method = method, path = path, body = body, version = version) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) zotGet(Config, path = "/users/42/items/ABCD1234")$status zotPost(Config, path = "/users/42/items", body = list(list(title = "New")))$status zotPatch(Config, path = "/users/42/items/ABCD1234", body = list(title = "Revised"), version = 7L)$body$versionPerformer <- function(config, method, path, query, body, version) { list( status = if (method == "POST") 201L else 200L, headers = list(), body = list(method = method, path = path, body = body, version = version) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) zotGet(Config, path = "/users/42/items/ABCD1234")$status zotPost(Config, path = "/users/42/items", body = list(list(title = "New")))$status zotPatch(Config, path = "/users/42/items/ABCD1234", body = list(title = "Revised"), version = 7L)$body$version
A duplicate is only what strong identity proves: an exact DOI, an exact attachment MD5, or normalized title plus year plus first-author family name. A bare title match proves nothing — generic titles collide across distinct documents — and reuse is refused when the first record carries a file the second lacks, whatever the metadata says.
zotSameItem(a, b)zotSameItem(a, b)
a, b
|
Lists with any of |
A list: match (logical) and basis ("doi", "md5",
"title-year-author" or "none").
ztTwins() for pairwise candidate detection.
A <- list(doi = "10.1000/Example", hasFile = TRUE) B <- list(doi = "10.1000/example", hasFile = TRUE) zotSameItem(A, B) # A title alone never establishes identity. zotSameItem(list(title = "Introduction"), list(title = "Introduction"))A <- list(doi = "10.1000/Example", hasFile = TRUE) B <- list(doi = "10.1000/example", hasFile = TRUE) zotSameItem(A, B) # A title alone never establishes identity. zotSameItem(list(title = "Introduction"), list(title = "Introduction"))
The only removal zot implements: deleted = 1 through a minimal
conditional PATCH. Permanent deletion is not part of this package;
emptying Trash is not provided by this package.
zotTrash(config, keys, library = "user")zotTrash(config, keys, library = "user")
config |
A |
keys |
Character vector of item keys. |
library |
|
A data.table with one row per key: itemKey, status
("trashed", "already", "missing" or "failed").
zotPatch(), zotPreview(), and zotExecute().
Store <- new.env(parent = emptyenv()) Store$version <- 1L Store$deleted <- 0L Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(), body = list(version = Store$version, data = list(deleted = Store$deleted)) )) } Store$deleted <- body$deleted Store$version <- Store$version + 1L list(status = 204L, headers = list(), body = NULL) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) zotTrash(Config, keys = "ABCD1234")Store <- new.env(parent = emptyenv()) Store$version <- 1L Store$deleted <- 0L Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(), body = list(version = Store$version, data = list(deleted = Store$deleted)) )) } Store$deleted <- body$deleted Store$version <- Store$version + 1L list(status = 204L, headers = list(), body = NULL) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) zotTrash(Config, keys = "ABCD1234")
Summarizes item statuses and re-reads a bounded sample of completed patch rows.
zotVerify(config, preview, ledger)zotVerify(config, preview, ledger)
config |
A |
preview |
The executed |
ledger |
The |
At most ten completed patch rows are sampled. Create and Trash rows are not included. The result does not replace inspection of failed or missing ledger entries.
A list: counts by status, sampleChecked, sampleConfirmed.
zotExecute() and ztCensus().
Performer <- function(config, method, path, query, body, version) { list( status = 200L, headers = list(), body = list(version = 2L, data = list(title = "After")) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "K1", payload = list(list(title = "After")) ) Preview <- zotPreview(Plan, describe = "Synthetic verified patch") Ledger <- structure( list(items = list(K1 = list(status = "done"))), class = "zotLedger" ) zotVerify(Config, Preview, Ledger)Performer <- function(config, method, path, query, body, version) { list( status = 200L, headers = list(), body = list(version = 2L, data = list(title = "After")) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Plan <- data.table::data.table( action = "patch", library = "user", itemKey = "K1", payload = list(list(title = "After")) ) Preview <- zotPreview(Plan, describe = "Synthetic verified patch") Ledger <- structure( list(items = list(K1 = list(status = "done"))), class = "zotLedger" ) zotVerify(Config, Preview, Ledger)
Implements Zotero's full upload protocol: create the attachment item,
request upload authorization (an identical file already in storage
short-circuits as exists), upload to the returned storage URL, register
the upload, and verify the attachment's MD5 server-side. Uploads may consume
Zotero storage quota and have immediate external effects.
ztAttach( config, parentKey, filePath, library = "user", filename = basename(filePath), contentType = .zotContentType(filePath), fileTransport = .zotFileTransport() )ztAttach( config, parentKey, filePath, library = "user", filename = basename(filePath), contentType = .zotContentType(filePath), fileTransport = .zotFileTransport() )
config |
A |
parentKey |
Key of the regular item receiving the file, or |
filePath |
Local file to upload. |
library |
|
filename |
Stored filename; defaults to the file's basename. |
contentType |
MIME type; defaults by extension for pdf/epub, else
|
fileTransport |
The three protocol steps as an injectable list
( |
The default file transport performs every external protocol step. Supplying
fileTransport replaces only authorization, byte upload, and registration;
attachment creation and final API verification still use config. A stored
file with the same MD5 returns "exists" before uploading bytes.
A list: status ("uploaded", "exists" or "failed"), the
attachment key, and error when failed.
ztCleanName(), zotPreview(), and zotExecute().
Path <- tempfile(fileext = ".pdf") writeBin(as.raw(1:8), Path) Performer <- function(config, method, path, query, body, version) { list( status = 200L, headers = list(), body = list(successful = list(`0` = list(key = "ATT1"))) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Transport <- list( authorize = function(...) list(exists = 1L), upload = function(...) stop("upload should not run"), register = function(...) stop("registration should not run") ) ztAttach(Config, parentKey = NA_character_, filePath = Path, fileTransport = Transport) unlink(Path)Path <- tempfile(fileext = ".pdf") writeBin(as.raw(1:8), Path) Performer <- function(config, method, path, query, body, version) { list( status = 200L, headers = list(), body = list(successful = list(`0` = list(key = "ATT1"))) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) Transport <- list( authorize = function(...) list(exists = 1L), upload = function(...) stop("upload should not run"), register = function(...) stop("registration should not run") ) ztAttach(Config, parentKey = NA_character_, filePath = Path, fileTransport = Transport) unlink(Path)
Produces create rows in the destination library from the given records. Items carrying imported files are refused: moving files needs the upload protocol, which this plan builder does not perform.
ztBlockMoveCreate(records, toLibrary, collectionKey)ztBlockMoveCreate(records, toLibrary, collectionKey)
records |
A data.table with |
toLibrary |
Destination library. |
collectionKey |
Destination collection key. |
A guarded-flow plan of create rows, one per record, whose order
matches records.
ztBlockMoveTrash(), zotPreview(), and ztAttach().
Records <- data.table::data.table( itemKey = "OLD1", payload = list(list(itemType = "book", title = "Reviewed book")), hasFile = FALSE ) ztBlockMoveCreate(Records, toLibrary = "5107760", collectionKey = "DEST")Records <- data.table::data.table( itemKey = "OLD1", payload = list(list(itemType = "book", title = "Reviewed book")), hasFile = FALSE ) ztBlockMoveCreate(Records, toLibrary = "5107760", collectionKey = "DEST")
Returns Trash rows for originals whose corresponding create-ledger entry is
marked "done" and records a new item key.
ztBlockMoveTrash(records, createLedger, fromLibrary)ztBlockMoveTrash(records, createLedger, fromLibrary)
records |
The |
createLedger |
The |
fromLibrary |
Library holding the originals. |
A guarded-flow plan of trash rows.
ztBlockMoveCreate(), zotExecute(), and zotTrash().
Records <- data.table::data.table( itemKey = "OLD1", payload = list(list(title = "Reviewed book")), hasFile = FALSE ) Ledger <- list(items = list(`create-1` = list(status = "done", new = "NEW1"))) ztBlockMoveTrash(Records, createLedger = Ledger, fromLibrary = "user")Records <- data.table::data.table( itemKey = "OLD1", payload = list(list(title = "Reviewed book")), hasFile = FALSE ) Ledger <- list(items = list(`create-1` = list(status = "done", new = "NEW1"))) ztBlockMoveTrash(Records, createLedger = Ledger, fromLibrary = "user")
Returns top-level and Trash counts for one library, plus per-collection counts when collection keys are supplied.
ztCensus(config, library = "user", collectionKeys = NULL)ztCensus(config, library = "user", collectionKeys = NULL)
config |
A |
library |
|
collectionKeys |
Optional named character vector: name -> key. |
Each scope is counted from the API Total-Results response header while
requesting only one record. Supplying named collection keys adds one row per
requested collection without downloading the library contents.
A data.table with scope and total.
zotGet(), zotLocalItems(), and zotVerify().
Performer <- function(config, method, path, query, body, version) { Total <- if (grepl("/trash", path)) "7" else "100" list(status = 200L, headers = list(`total-results` = Total), body = list()) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztCensus(Config)Performer <- function(config, method, path, query, body, version) { Total <- if (grepl("/trash", path)) "7" else "100" list(status = 200L, headers = list(`total-results` = Total), body = list()) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztCensus(Config)
Removes source labels recognized by the package and copy markers such as
(1) before the extension. The extension and other parenthetical text are
retained.
ztCleanName(name)ztCleanName(name)
name |
A filename. |
The cleaned filename.
ztCleanTitle() and ztCleanPayload().
ztCleanName("Dynamics (1).pdf") ztCleanName("Course Notes (SEG Course).pdf")ztCleanName("Dynamics (1).pdf") ztCleanName("Course Notes (SEG Course).pdf")
Applies the package's text-normalization rules across an item payload. Titles lose trailing extensions and copy markers, filenames retain their extensions, and creator names and tags are included. Non-text values are returned unchanged.
ztCleanPayload(payload)ztCleanPayload(payload)
payload |
An item payload (named list of fields). |
The payload with every text field cleaned.
ztCleanName(), ztCleanTitle(), and zotPreview().
Payload <- list( itemType = "book", title = "Structural Analysis (1).pdf", creators = list(list(creatorType = "author", firstName = "A.", lastName = "Author")), filename = "Structural Analysis (1).pdf" ) ztCleanPayload(Payload)Payload <- list( itemType = "book", title = "Structural Analysis (1).pdf", creators = list(list(creatorType = "author", firstName = "A.", lastName = "Author")), filename = "Structural Analysis (1).pdf" ) ztCleanPayload(Payload)
Like ztCleanName(), and additionally strips a trailing file extension
and trailing copy markers — a title is not a filename.
ztCleanTitle(title)ztCleanTitle(title)
title |
An item title. |
The cleaned title.
ztCleanName(), ztCleanPayload(), and ztRetitle().
ztCleanTitle("Dynamics of Structures (1).pdf") ztCleanTitle("Optimization (2nd Edition)")ztCleanTitle("Dynamics of Structures (1).pdf") ztCleanTitle("Optimization (2nd Edition)")
Find or create each path (vectors of names, root first) and return the full path to key map. Creating a missing collection has an immediate API effect.
ztCollectionTree(config, library, paths)ztCollectionTree(config, library, paths)
config |
A |
library |
|
paths |
A list of character vectors, e.g. |
Existing collections are indexed by parent key and name. Missing nodes are created from the root downward and reused by later paths in the same call. Unlike the plan builders, this function has immediate external effects.
Named character vector: "A/B" -> collection key.
ztRefile() and zotPreview().
Counter <- new.env(parent = emptyenv()) Counter$n <- 0L Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(`total-results` = "0"), body = list() )) } Counter$n <- Counter$n + 1L Key <- paste0("COLL", Counter$n) list( status = 200L, headers = list(), body = list(successful = list(`0` = list(key = Key))) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztCollectionTree(Config, library = "user", paths = list(c("Projects", "2026")))Counter <- new.env(parent = emptyenv()) Counter$n <- 0L Performer <- function(config, method, path, query, body, version) { if (method == "GET") { return(list( status = 200L, headers = list(`total-results` = "0"), body = list() )) } Counter$n <- Counter$n + 1L Key <- paste0("COLL", Counter$n) list( status = 200L, headers = list(), body = list(successful = list(`0` = list(key = Key))) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztCollectionTree(Config, library = "user", paths = list(c("Projects", "2026")))
The returned payload is cleaned with ztCleanPayload() and can be placed in
a create plan. The default fetcher contacts the public Crossref API; inject a
compatible function for reproducible or offline work.
ztCrossref(doi, getJson = .zotCrossrefGet)ztCrossref(doi, getJson = .zotCrossrefGet)
doi |
The DOI to resolve. |
getJson |
Injectable fetcher returning the parsed Crossref message for a DOI; the default performs the real request. |
An item payload for a create row, or a classed error when the DOI does not resolve.
ztCleanPayload() and zotPreview().
Message <- function(doi) list( title = list("A reproducible record"), author = list(list(given = "Ada", family = "Lovelace")), `container-title` = list("Example Journal"), issued = list(`date-parts` = list(list(2026L))) ) ztCrossref("10.1000/example", getJson = Message)Message <- function(doi) list( title = list("A reproducible record"), author = list(list(given = "Ada", family = "Lovelace")), `container-title` = list("Example Journal"), issued = list(`date-parts` = list(list(2026L))) ) ztCrossref("10.1000/example", getJson = Message)
Fetches each item's current memberships at plan time and produces patch
rows whose collections payload is the union with addTo — the Zotero
API replaces the whole membership list, so the merge is mandatory.
ztRefile(config, itemKeys, library, addTo)ztRefile(config, itemKeys, library, addTo)
config |
A |
itemKeys |
Character vector of item keys. |
library |
|
addTo |
Collection key to add. |
Planning performs one current API read per key. It does not write, and it omits missing items and items already in the destination collection. Memberships can change after planning; rebuild or review the plan before execution when concurrent changes are possible.
A guarded-flow plan (items already members are omitted).
ztCollectionTree(), zotPreview(), and zotExecute().
Memberships <- list(K1 = list("OLD"), K2 = list("TARGET")) Performer <- function(config, method, path, query, body, version) { Key <- sub(".*/items/", "", path) list( status = 200L, headers = list(), body = list(version = 1L, data = list(collections = Memberships[[Key]])) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztRefile(Config, itemKeys = c("K1", "K2"), library = "user", addTo = "TARGET")Memberships <- list(K1 = list("OLD"), K2 = list("TARGET")) Performer <- function(config, method, path, query, body, version) { Key <- sub(".*/items/", "", path) list( status = 200L, headers = list(), body = list(version = 1L, data = list(collections = Memberships[[Key]])) ) } Config <- structure( list(userID = "42", key = "example", apiBase = "https://example.invalid", performer = Performer), class = "zotConfig" ) ztRefile(Config, itemKeys = c("K1", "K2"), library = "user", addTo = "TARGET")
Pure plan builder: applies transform to every title and returns patch
rows only where the result differs. Feed the result to zotPreview().
ztRetitle(items, transform)ztRetitle(items, transform)
items |
A data.table with |
transform |
A function from one title to its cleaned form. |
A guarded-flow plan (possibly empty).
ztCleanTitle() and zotPreview().
Items <- data.table::data.table( itemKey = c("A", "B"), library = "user", title = c("Reviewed book (1).pdf", "Existing title") ) ztRetitle(Items, transform = ztCleanTitle)Items <- data.table::data.table( itemKey = c("A", "B"), library = "user", title = c("Reviewed book (1).pdf", "Existing title") ) ztRetitle(Items, transform = ztCleanTitle)
Read-only query over the chroma embedding index that the companion
zotero-mcp tool builds (zotero-mcp update-db). Building or refreshing
that index is a separate operation of the companion tool and is never
triggered by this function. Requires the companion tool's Python
environment; results can lag current Zotero API data.
ztSemantic( query, limit = 10L, dbPath = file.path(Sys.getenv("HOME"), ".config", "zotero-mcp", "chroma_db"), pythonPath = file.path(Sys.getenv("HOME"), ".local", "share", "uv", "tools", "zotero-mcp-server", "bin", "python3") )ztSemantic( query, limit = 10L, dbPath = file.path(Sys.getenv("HOME"), ".config", "zotero-mcp", "chroma_db"), pythonPath = file.path(Sys.getenv("HOME"), ".local", "share", "uv", "tools", "zotero-mcp-server", "bin", "python3") )
query |
Natural-language query text. |
limit |
Maximum results. |
dbPath |
Chroma database directory. |
pythonPath |
Python interpreter of the companion environment. |
The defaults target one common Unix uv tool installation and index layout;
pass pythonPath and dbPath for other environments. The function requires
local access to a compatible Chroma collection named zotero_library,
launches that Python interpreter once, and converts its JSON result to a
data.table. It never creates or refreshes the semantic index. The current
query has no library selector: limit applies across all indexed personal
and group records, whose groupID is returned for inspection.
A data.table with itemKey, groupID, title, distance
(smaller is closer).
zotLocalItems() and zotLocalQuery().
# Requires the companion Python environment and a populated semantic index. # These paths match the default Unix uv installation; adapt them as needed. Python <- file.path(path.expand("~"), ".local", "share", "uv", "tools", "zotero-mcp-server", "bin", "python3") Index <- file.path(path.expand("~"), ".config", "zotero-mcp", "chroma_db") if (file.exists(Python) && file.exists(file.path(Index, "chroma.sqlite3"))) { ztSemantic("conditional mean spectrum", limit = 5L, pythonPath = Python, dbPath = Index) }# Requires the companion Python environment and a populated semantic index. # These paths match the default Unix uv installation; adapt them as needed. Python <- file.path(path.expand("~"), ".local", "share", "uv", "tools", "zotero-mcp-server", "bin", "python3") Index <- file.path(path.expand("~"), ".config", "zotero-mcp", "chroma_db") if (file.exists(Python) && file.exists(file.path(Index, "chroma.sqlite3"))) { ztSemantic("conditional mean spectrum", limit = 5L, pythonPath = Python, dbPath = Index) }
Groups records by normalized title to nominate candidates, then judges
every pair with zotSameItem() — the strong-identity rule decides, never
the title alone.
ztTwins(records)ztTwins(records)
records |
A data.table with |
A data.table of pairs: keyA, keyB, match, basis.
zotSameItem() and zotPreview().
Records <- data.table::data.table( itemKey = c("A", "B"), title = c("Paper", "Paper"), year = c(2026L, 2026L), author = c("Lovelace", "Lovelace"), doi = NA_character_, md5 = NA_character_, hasFile = FALSE ) ztTwins(Records)Records <- data.table::data.table( itemKey = c("A", "B"), title = c("Paper", "Paper"), year = c(2026L, 2026L), author = c("Lovelace", "Lovelace"), doi = NA_character_, md5 = NA_character_, hasFile = FALSE ) ztTwins(Records)