@chronicle.app/zotero 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/connectors/ZoteroExtractor.d.ts +73 -0
- package/dist/connectors/ZoteroExtractor.js +551 -0
- package/dist/connectors/ZoteroTransformer.d.ts +139 -0
- package/dist/connectors/ZoteroTransformer.js +941 -0
- package/dist/connectors/records.d.ts +175 -0
- package/dist/connectors/records.js +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Louis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @chronicle.app/zotero
|
|
2
|
+
|
|
3
|
+
Chronicle plugin for a local [Zotero](https://www.zotero.org/) library: works
|
|
4
|
+
(references), the highlights and notes made on them, and standalone
|
|
5
|
+
attachments filed without a parent work.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @chronicle.app/zotero
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Basic extraction
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
chronicle extract zotero
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
By default the plugin reads `~/Zotero`, Zotero's default data directory. Point
|
|
22
|
+
it elsewhere with `--input`, either the data directory itself or a path
|
|
23
|
+
directly to `zotero.sqlite`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
chronicle extract zotero --input /path/to/Zotero
|
|
27
|
+
chronicle extract zotero --input /path/to/Zotero/zotero.sqlite
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### With date filtering
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
chronicle extract zotero \
|
|
34
|
+
--input /path/to/Zotero \
|
|
35
|
+
--since 2024-01-01 \
|
|
36
|
+
--until 2024-12-31 \
|
|
37
|
+
--limit 1000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`--since`/`--until` filter on each item's own `dateModified`, so a filtered run
|
|
41
|
+
re-reports whatever changed (or was added) in that window, including edits to
|
|
42
|
+
older items.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
The extractor supports the following options:
|
|
47
|
+
|
|
48
|
+
- `--input`: Path to the Zotero data directory, or directly to `zotero.sqlite`.
|
|
49
|
+
Defaults to `~/Zotero`.
|
|
50
|
+
- `--since`: Extract records modified from this date onwards
|
|
51
|
+
- `--until`: Extract records modified up to this date
|
|
52
|
+
- `--limit`: Maximum number of records to extract, across all record types
|
|
53
|
+
|
|
54
|
+
## Source
|
|
55
|
+
|
|
56
|
+
The plugin opens `zotero.sqlite` read-only — it never writes to the library,
|
|
57
|
+
and never needs Zotero to be closed. It reads the **user library only**; group
|
|
58
|
+
libraries are out of scope. Trashed items (and anything whose parent is
|
|
59
|
+
trashed) are excluded throughout.
|
|
60
|
+
|
|
61
|
+
If Zotero is running, its exclusive lock on the database can make that
|
|
62
|
+
readonly open fail with `SQLITE_BUSY`; the extractor falls back to reading a
|
|
63
|
+
disposable filesystem-level clone of the (closed-journal, so consistent)
|
|
64
|
+
database file instead.
|
|
65
|
+
|
|
66
|
+
Every record carries the library's `localUserKey` (`settings.account`), stable
|
|
67
|
+
even for a library that has never synced to zotero.org.
|
|
68
|
+
|
|
69
|
+
## What it extracts
|
|
70
|
+
|
|
71
|
+
- **Works** (`recordType: work`) — top-level reference items (journal
|
|
72
|
+
articles, books, book sections, preprints, …), each with its bibliographic
|
|
73
|
+
fields (title, date, publisher, DOI, ISBN, …), creators, tag list, the
|
|
74
|
+
collections it's filed under (with parent chains resolved), and its child
|
|
75
|
+
attachments (with storage paths resolved to a file on disk where possible).
|
|
76
|
+
- **Annotations** (`recordType: annotation`) — highlights, underlines, notes,
|
|
77
|
+
and other markup made in the Zotero reader (PDF or EPUB), with the
|
|
78
|
+
annotated attachment and its parent work carried as context.
|
|
79
|
+
- **Attachments** (`recordType: attachment`) — standalone files saved
|
|
80
|
+
directly to the library with no parent work, including their own tags and
|
|
81
|
+
collection memberships.
|
|
82
|
+
- **Notes** (`recordType: note`) — Zotero note items, written on a work or
|
|
83
|
+
standing alone, with the parent work carried as context and the HTML body
|
|
84
|
+
passed through verbatim.
|
|
85
|
+
|
|
86
|
+
## Schema
|
|
87
|
+
|
|
88
|
+
The transformer maps these records onto Chronicle's schema: the catalog item
|
|
89
|
+
is the abstract work (`Book`, `Article`, `Post`, `VideoObject`, … by Zotero
|
|
90
|
+
item type, keyed by its item key so a re-typing refines in place), saved via a
|
|
91
|
+
`BookmarkAction` and carrying protocol `sameAs` edges (DOI with its resolver
|
|
92
|
+
url, ISBN, arXiv, PMID/PMCID — a bookSection's ISBN lands on its containing
|
|
93
|
+
book), creators, collections, and tags. Item keys are only per-library
|
|
94
|
+
unique, so every item-keyed identity is scoped by the library as an `inRealm`
|
|
95
|
+
Realm (the zotero.org userID when synced, else the local user key). A child
|
|
96
|
+
attachment is not a separate entity: it merges into its work via `sameAs` as
|
|
97
|
+
a sparse facet (mimeType, contentPath, its filesystem and content
|
|
98
|
+
identities — no name), entering the log through reading — each last-read
|
|
99
|
+
timestamp is a one-shot `ViewAction` (Zotero has no completion signal, so no
|
|
100
|
+
reading run is opened) whose object is the file facet, resolving to the
|
|
101
|
+
work. Annotations anchor to the facet too, so which copy a selector is valid
|
|
102
|
+
in survives beneath the merge. A carrier shared by two different works (one
|
|
103
|
+
anthology PDF under several items) withholds its merge edges so distinct
|
|
104
|
+
works never weld through a shared file; standalone attachments stay their
|
|
105
|
+
own entities. The file also
|
|
106
|
+
carries a filesystem-keyed `sameAs` (path scoped by machine, the timing/shell
|
|
107
|
+
convention), so app-tracked sessions on the same file fold onto it, and
|
|
108
|
+
`snapshotOf` the url hub for webpage snapshots. A highlight is a
|
|
109
|
+
`QuoteAction` → `Quotation` carrying a `PageSelector` (PDF) or
|
|
110
|
+
`EpubCfiSelector` (EPUB), and a note is an `AnnotateAction` → `Comment`
|
|
111
|
+
about the quotation.
|
|
112
|
+
|
|
113
|
+
## Tests
|
|
114
|
+
|
|
115
|
+
The tests build a synthetic Zotero data directory in a temporary folder: a
|
|
116
|
+
`zotero.sqlite` with only the tables the extractor reads, and a few files
|
|
117
|
+
under `storage/`. They stub the machine name, so they never read the host's
|
|
118
|
+
Zotero library, hostname, or accounts.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Record } from '@chronicle.app/etl';
|
|
2
|
+
import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
|
|
3
|
+
import ZoteroTransformer from './ZoteroTransformer.js';
|
|
4
|
+
/**
|
|
5
|
+
* A local Zotero library — works (references), the highlights and notes made
|
|
6
|
+
* on their attachments, standalone attachments filed without a parent work,
|
|
7
|
+
* and note items written on a work or standing alone. Reads `zotero.sqlite`
|
|
8
|
+
* directly; the user library only (group libraries are out of scope),
|
|
9
|
+
* excluding anything in the trash.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ZoteroExtractor extends SqliteExtractor<typeof ZoteroExtractor> {
|
|
12
|
+
static source: string;
|
|
13
|
+
static description: string;
|
|
14
|
+
static delivery: "local";
|
|
15
|
+
static strategy: string;
|
|
16
|
+
static recordTypes: string[];
|
|
17
|
+
static default: boolean;
|
|
18
|
+
static defaultTransformer: typeof ZoteroTransformer;
|
|
19
|
+
static temporality: "snapshot";
|
|
20
|
+
static schema: any;
|
|
21
|
+
private dataDir;
|
|
22
|
+
private localUserKey;
|
|
23
|
+
private library;
|
|
24
|
+
private machine;
|
|
25
|
+
private dbMtime;
|
|
26
|
+
private extractedCount;
|
|
27
|
+
/** Set when Zotero's lock forced a filesystem-level clone; cleaned up in teardown(). */
|
|
28
|
+
private tempDbPath?;
|
|
29
|
+
/** Item keys of child attachments whose carrier (path or content hash) is shared with a different parent work; rebuilt per extract() run. */
|
|
30
|
+
private flaggedAttachmentKeys;
|
|
31
|
+
/**
|
|
32
|
+
* The item's 8-char Zotero key. Every record type (work, annotation,
|
|
33
|
+
* attachment, note) is an `items` row, and the key is unique across all of
|
|
34
|
+
* them within a library — the same field each transformer keyset uses.
|
|
35
|
+
*/
|
|
36
|
+
keyOf(record: Record): string | null;
|
|
37
|
+
setup(): Promise<void>;
|
|
38
|
+
private readLocalUserKey;
|
|
39
|
+
private readUserID;
|
|
40
|
+
teardown(): Promise<void>;
|
|
41
|
+
protected asOfTime(): Date;
|
|
42
|
+
extract(): AsyncGenerator<Record>;
|
|
43
|
+
/** The since/until window over a `dateModified` column, as ` AND …` terms appended to each stream's WHERE. */
|
|
44
|
+
private sinceUntilFilter;
|
|
45
|
+
private iterateWorks;
|
|
46
|
+
private iterateAnnotations;
|
|
47
|
+
private iterateOrphanAttachments;
|
|
48
|
+
private iterateNotes;
|
|
49
|
+
private buildAttachment;
|
|
50
|
+
/**
|
|
51
|
+
* Item keys of child attachments whose carrier — resolved path, or file
|
|
52
|
+
* content hash — also appears under a DIFFERENT parent work. Built from
|
|
53
|
+
* every non-deleted attachment in the user library (children and orphans),
|
|
54
|
+
* but only children contribute to (and can be flagged by) the map: an
|
|
55
|
+
* orphan has no parent to weld two works through.
|
|
56
|
+
*/
|
|
57
|
+
private buildFlaggedAttachmentKeys;
|
|
58
|
+
/** The resolved path, plus a content-hash key when the file is readable — the two ways two attachments can carry the same bytes. */
|
|
59
|
+
private carrierKeys;
|
|
60
|
+
private hashFile;
|
|
61
|
+
/** Storage folders are named by the attachment's own key, not its filename. */
|
|
62
|
+
private resolveAttachmentPath;
|
|
63
|
+
private loadWorkRef;
|
|
64
|
+
private loadWorkAttachments;
|
|
65
|
+
private loadFields;
|
|
66
|
+
private loadCreators;
|
|
67
|
+
private loadTags;
|
|
68
|
+
private loadCollections;
|
|
69
|
+
private buildCollectionChain;
|
|
70
|
+
private getItemCollections;
|
|
71
|
+
determineCount(): Promise<number | null>;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=ZoteroExtractor.d.ts.map
|