kindle-zotero_importer/MEMORY.md
2026-05-27 10:49:04 +02:00

283 lines
9.1 KiB
Markdown

---
canvas:
- "[[UTI-roadmap.canvas]]"
UTI-roadmap: []
---
# Kindle Zotero Importer Memory
## Goal
Import Kindle `My Clippings.txt` highlights/notes into the corresponding Zotero library items as native Zotero annotations where possible.
Primary source of Kindle data:
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/My Clippings.txt`
Zotero database discovered at:
- `/Users/ubd/Zotero/zotero.sqlite`
Do not write directly to SQLite. Use Zotero's local JavaScript API/data layer.
Confirmed title-to-Zotero mappings must be stored in `match-overrides.json` so they persist across cumulative Kindle `My Clippings.txt` exports. `match-overrides.generated.json` is disposable and should not be treated as authoritative.
## Confirmed Architecture
Use a two-step importer:
1. Python-side preview/import-plan generator.
- Parse `My Clippings.txt`.
- Match clipping titles to Zotero items/attachments.
- For EPUB, compute EPUB CFI selectors.
- For PDF, compute page rect coordinates.
- Emit JSON import plan.
2. Zotero-side JavaScript writer.
- Run in Zotero via `Tools > Developer > Run JavaScript` or later as a plugin.
- Load/import generated plan.
- Call `Zotero.Annotations.saveFromJSON(attachment, annotation)`.
## Working Zotero API
Native annotations can be created safely with:
```js
const attachment = Zotero.Items.get(ATTACHMENT_ITEM_ID);
const saved = await Zotero.Annotations.saveFromJSON(attachment, annotation);
```
Annotation item type exists internally as Zotero item type `annotation`.
Native annotation table observed:
- `itemAnnotations`
- columns include `itemID`, `parentItemID`, `type`, `text`, `comment`, `color`, `pageLabel`, `sortIndex`, `position`, `isExternal`
## EPUB Test: Cyclonopedia
Zotero parent item:
- itemID: `51395`
- key: `JTDWDKRH`
- type: `book`
- citationKey: `negarestani2008`
- title: `Cyclonopedia: complicity with anonymous materials`
Zotero EPUB attachment:
- itemID: `51402`
- key: `P6XQUQXF`
- contentType: `application/epub+zip`
- path: `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Negarestani_2008_Cyclonopedia complicity with anonymous materials.epub`
Kindle clipping title:
- `(fixed) Negarestani_2008_Cyclonopedia complicity with anonymous materials (Reza Negarestani)`
Text matching against the Zotero-linked EPUB worked:
- Cyclonopedia clipping bodies: `79`
- Highlights: `69`
- Notes: `10`
- Exact text matches: `66`
- Partial text matches: `5`
- Unmatched: `8`, all standalone notes.
EPUB spine inspection showed `EPUB/paleopetrology.html` is spine slot `/6/30`.
Working native EPUB test annotation:
```js
const attachment = Zotero.Items.get(51402);
const annotation = {
key: Zotero.DataObjectUtilities.generateKey(),
type: "highlight",
text: "Oil as a lubricant or Tellurian Lube, upon which everything moves forward,",
comment: "Kindle import test annotation. Delete after verification.",
color: "#ffd400",
pageLabel: "",
sortIndex: "00014|00047607",
position: {
type: "FragmentSelector",
conformsTo: "http://www.idpf.org/epub/linking/cfi/epub-cfi.html",
value: "epubcfi(/6/30!/4/90/2,/1:3,/1:77)"
},
tags: [{ name: "kindle-import-test" }]
};
await Zotero.Annotations.saveFromJSON(attachment, annotation);
```
Important EPUB CFI lesson:
- Bad CFI navigated but did not render highlight: `epubcfi(/6/30!/4/90/2/1:3,/4/90/2/1:77)`
- Good CFI rendered highlight: `epubcfi(/6/30!/4/90/2,/1:3,/1:77)`
- Zotero's EPUB CFI uses a common parent element plus relative text-node offsets.
Manual Zotero highlight over a longer range produced:
```json
{
"value": "epubcfi(/6/30!/4/90/2,/1:3,/1:317)"
}
```
## PDF Test: Rethinking Algorithmic Regulation
Chose this because it is a normal Zotero item, has a real PDF path, directly matches Kindle clipping text, and had zero existing annotations before the test.
Zotero parent item:
- itemID: `350`
- key: `SGZMZ9X7`
- type: `journalArticle`
- citationKey: `medina2015`
- title: `Rethinking algorithmic regulation`
- publicationTitle: `Kybernetes`
Zotero PDF attachment:
- itemID: `673`
- key: `4BPF7ZUR`
- contentType: `application/pdf`
- attachment title/path in DB: `attachments:Medina_2015_Rethinking algorithmic regulation.pdf`
- actual file found at: `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Medina_2015_Rethinking algorithmic regulation.pdf`
- existing annotations before test: `0`
Kindle clipping title:
- `Rethinking Algorithmic Regulation Kybern - Unknown`
Tested clipping text:
```text
increase worker participation in the economy and preserve the autonomy of factory managers, even with the expansion of state influence.
```
Located in PDF:
- PDF page: `5`
- Zotero `pageIndex`: `4`
- PDF page label: `1009`
- `pdftohtml` XML page dimensions: width `739`, height `1020`
- `pdfinfo` PDF page size: width `493.228`, height `680.315` pts
Working native PDF test annotation:
```js
const attachment = Zotero.Items.get(673);
const annotation = {
key: Zotero.DataObjectUtilities.generateKey(),
type: "highlight",
text: "increase worker participation in the economy and preserve the autonomy of factory managers, even with the expansion of state influence.",
comment: "Kindle import PDF test annotation. Delete after verification.",
color: "#ffd400",
pageLabel: "1009",
sortIndex: "00004|000435|00197",
position: {
pageIndex: 4,
rects: [
[197.252, 380.843, 445.841, 390.181],
[99.447, 369.504, 403.125, 378.842]
]
},
tags: [{ name: "kindle-import-pdf-test" }]
};
await Zotero.Annotations.saveFromJSON(attachment, annotation);
```
Important PDF lessons:
- Native PDF annotation writing works.
- PDF positions use `{ pageIndex, rects }`.
- Rect coordinates are in PDF points, origin bottom-left.
- `pdftohtml -xml` coordinates are top-left in its own scaled XML coordinate system.
- Convert from `pdftohtml` XML to PDF points using:
- `scaleX = pdfPageWidth / xmlPageWidth`
- `scaleY = pdfPageHeight / xmlPageHeight`
- `x1 = xmlLeft * scaleX`
- `x2 = (xmlLeft + xmlWidth) * scaleX`
- `y1 = pdfPageHeight - (xmlTop + xmlHeight) * scaleY`
- `y2 = pdfPageHeight - xmlTop * scaleY`
- PDF `sortIndex` needs three numeric parts: `pageIndex|vertical|horizontal`, e.g. `00004|000435|00197`.
- EPUB `sortIndex` accepted two numeric parts, e.g. `00014|00047607`.
## Test Scripts Created During Exploration
These were created outside this repo during probing and may be copied/adapted:
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/create-cyclonopedia-test-annotation.js`
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/list-cyclonopedia-annotations.js`
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/create-latzer-pdf-test-annotation.js`
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/select-medina-pdf-zotero-item.js`
- `/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/select-howaldt-pdf-zotero-item.js`
## Useful Commands/Tools
Query Zotero DB read-only while Zotero is running:
```sh
sqlite3 "file:/Users/ubd/Zotero/zotero.sqlite?mode=ro&immutable=1" "SQL HERE"
```
PDF text extraction available:
```sh
pdftotext -enc UTF-8 -f PAGE -l PAGE file.pdf -
```
PDF coordinate extraction available:
```sh
pdftohtml -f PAGE -l PAGE -xml -stdout file.pdf
```
PDF page size:
```sh
pdfinfo -f PAGE -l PAGE file.pdf
```
Python PDF libraries were not installed at exploration time:
- `fitz` / PyMuPDF: unavailable
- `pypdf`: unavailable
## Repo/Workspace Decision
This project lives in the Obsidian vault project area:
- `/Users/ubd/Library/Mobile Documents/iCloud~md~obsidian/Documents/rhizome/06_projects/UTI/kindle-zotero-importer`
The vault `.gitignore` ignores it:
- `06_projects/UTI/kindle-zotero-importer`
The importer directory has its own independent Git repo:
- `## No commits yet on main`
## Next Roadmap
1. Create project scaffold.
2. Implement Kindle clipping parser.
3. Implement Zotero DB read-only indexer.
4. Implement matching between Kindle titles and Zotero items/attachments.
5. Implement EPUB text matching and CFI generation.
6. Implement PDF text matching and rect generation using `pdftotext`, `pdftohtml`, and `pdfinfo` first.
7. Emit import preview JSON with confidence flags.
8. Build Zotero JavaScript writer that reads preview JSON and calls `Zotero.Annotations.saveFromJSON()`.
9. Add deduplication. Candidate keys should include attachment ID/key, clipping text hash, Kindle location/page, timestamp, and annotation type.
10. Attach standalone Kindle notes to nearest highlight/location as annotation comments where possible; otherwise import as separate text annotations or report unmatched.
11. Generate import report: matched, ambiguous, unmatched, skipped duplicates, created annotations.
## Cautions
- Never write directly to Zotero SQLite.
- Test on one item before bulk writes.
- Keep a dry-run/import-preview mode as default.
- Keep tags on imported annotations, e.g. `kindle-import`, for easy deletion/filtering.
- PDF text matching will be more fragile than EPUB because of hyphenation, line breaks, OCR quality, and coordinate extraction.
- Some Kindle notes are standalone and have no quote text; attach by location/page proximity.