Curation plans

The curation helpers in zot convert candidate records into plan rows or Zotero payloads. The returned objects can be inspected before any API write.

library(data.table)
#> 
#> Attaching package: 'data.table'
#> The following object is masked from 'package:base':
#> 
#>     %notin%
library(zot)

Normalize incoming metadata

ztCleanName() preserves a filename extension while removing recognized source labels and copy suffixes. ztCleanTitle() also removes a trailing file extension. ztCleanPayload() applies the corresponding rules to text fields, creator names, and tags.

ztCleanName("Dynamics (1).pdf")
#> [1] "Dynamics.pdf"
ztCleanTitle("Dynamics (1).pdf")
#> [1] "Dynamics"

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)
#> $itemType
#> [1] "book"
#> 
#> $title
#> [1] "Structural Analysis"
#> 
#> $creators
#> $creators[[1]]
#> $creators[[1]]$creatorType
#> [1] "author"
#> 
#> $creators[[1]]$firstName
#> [1] "A."
#> 
#> $creators[[1]]$lastName
#> [1] "Author"
#> 
#> 
#> 
#> $filename
#> [1] "Structural Analysis.pdf"

Other parenthetical text is retained. zotPreview() rejects a payload that still contains one of the source labels recognized by the package.

Retitle changed records

ztRetitle() applies a transformation and returns a patch row only when the result differs from the original title.

Items <- data.table(
  itemKey = c("A", "B"),
  library = "user",
  title = c("Reviewed book (1).pdf", "Existing title")
)

RetitlePlan <- ztRetitle(Items, transform = ztCleanTitle)
zotPreview(RetitlePlan, describe = "Normalize reviewed titles")
#> <zotPreview> Normalize reviewed titles 
#>   digest: 1737f2bbf3c2706d92827a746c5299a38e518e066d343dd596632c04ae8abb9e 
#>    action library     N
#>    <char>  <char> <int>
#> 1:  patch    user     1

ztRefile() builds collection-membership patches. It reads each item’s current memberships and includes their union with the requested collection, because the Zotero API replaces the complete membership list.

Compare duplicate candidates

zotSameItem() compares exact DOI, exact attachment MD5, or normalized title with year and first-author family name. A title alone does not produce a positive match. A record with a file is not considered equivalent to a candidate without that file.

zotSameItem(
  list(doi = "10.1000/Example", hasFile = TRUE),
  list(doi = "10.1000/example", hasFile = TRUE)
)
#> $match
#> [1] TRUE
#> 
#> $basis
#> [1] "doi"

zotSameItem(
  list(title = "Introduction"),
  list(title = "Introduction")
)
#> $match
#> [1] FALSE
#> 
#> $basis
#> [1] "none"

ztTwins() evaluates pairs within normalized-title groups and returns the matching verdict and its evidence class.

Records <- data.table(
  itemKey = c("A", "B", "C"),
  title = c("Paper", "Paper", "Paper"),
  year = c(2026L, 2026L, NA_integer_),
  author = c("Lovelace", "Lovelace", NA_character_),
  doi = NA_character_,
  md5 = NA_character_,
  hasFile = FALSE
)
ztTwins(Records)
#>      keyA   keyB  match             basis
#>    <char> <char> <lgcl>            <char>
#> 1:      A      B   TRUE title-year-author
#> 2:      A      C  FALSE              none
#> 3:      B      C  FALSE              none

Map Crossref metadata

ztCrossref() normally requests a DOI record from Crossref. Its injectable fetcher also permits offline and reproducible mapping.

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)
#> $itemType
#> [1] "journalArticle"
#> 
#> $title
#> [1] "A reproducible record"
#> 
#> $creators
#> $creators[[1]]
#> $creators[[1]]$creatorType
#> [1] "author"
#> 
#> $creators[[1]]$firstName
#> [1] "Ada"
#> 
#> $creators[[1]]$lastName
#> [1] "Lovelace"
#> 
#> 
#> 
#> $DOI
#> [1] "10.1000/example"
#> 
#> $publicationTitle
#> [1] "Example Journal"
#> 
#> $date
#> [1] "2026"

The result is a payload; the function does not create a Zotero item.

Build a cross-library move

Zotero item keys belong to one library. ztBlockMoveCreate() therefore builds create rows for the destination. It rejects records with imported files, because copying their metadata alone would omit attachments.

MoveRecords <- data.table(
  itemKey = "OLD1",
  payload = list(list(itemType = "book", title = "Reviewed book")),
  hasFile = FALSE
)

CreatePlan <- ztBlockMoveCreate(
  MoveRecords,
  toLibrary = "5107760",
  collectionKey = "DEST"
)
CreatePlan
#>    action library itemKey   payload
#>    <char>  <char>  <char>    <list>
#> 1: create 5107760    <NA> <list[3]>

ztBlockMoveTrash() adds an original item to a Trash plan only when its corresponding create-ledger entry is marked "done".

CreateLedger <- list(items = list(
  `create-1` = list(status = "done", new = "NEW1")
))

TrashPlan <- ztBlockMoveTrash(
  MoveRecords,
  createLedger = CreateLedger,
  fromLibrary = "user"
)
TrashPlan
#>    action library itemKey   payload
#>    <char>  <char>  <char>    <list>
#> 1:  trash    user    OLD1 <list[0]>

The second plan is independent of the first execution. Trash is recoverable; the package does not empty Zotero’s Trash.