Compare commits

..

1 commit

Author SHA1 Message Date
Utku Bilen Demir
0e53fa4ef8 feat: anonymized sharing opt-in + more fuzzy tests, no AI 2026-09-18 18:53:20 +02:00
6 changed files with 4 additions and 104 deletions

View file

@ -1,13 +1,12 @@
> [!CAUTION]
> **This project has been heavily vibecoded.**
# Kindle Zotero Importer
Import Kindle `My Clippings.txt` highlights into Zotero as native annotations; directly inside Zotero, no terminal needed.
> [!CAUTION]
> **Disclaimer: This project has been heavily vibecoded.**
[<img src="https://img.shields.io/badge/please%20give%20me%20money+-red?style=for-the-badge" alt="please give me money+">](https://github.com/sponsors/UtkuBilenDemir)
Import Kindle `My Clippings.txt` highlights into Zotero as native annotations; directly inside Zotero, no terminal needed.
## Install (30 seconds)
1. Download `kindle-zotero-importer.xpi` from [Releases](../../releases) (latest `0.6.4`, or `0.6.4-beta` for preview).

View file

@ -1,17 +0,0 @@
from kindle_zotero_importer.clippings import parse_clippings_text, _stable_id
def test_parse_and_id_stable():
txt = "My Book (Author)\n- Your Highlight at location 10-20 | Added on Monday, 6 October 2025 11:32:29\n\nSome text\n==========\n"
clips = parse_clippings_text(txt)
assert len(clips) == 1
c = clips[0]
assert c.title == "My Book (Author)"
assert c.text == "Some text"
assert c.id == _stable_id(c.title, c.raw_detail, c.text)
# changing text changes id
assert _stable_id(c.title, c.raw_detail, "Other") != c.id
def test_added_on_iso():
txt = "B\n- Your Highlight at location 1 | Added on Tuesday, 18 May 2021 14:18:13\n\nText\n==========\n"
c = parse_clippings_text(txt)[0]
assert c.added_on_iso.startswith("2021-05-18T14:18:13")

View file

@ -1,23 +0,0 @@
from kindle_zotero_importer.final_plan import build_final_writer_plan
def test_final_includes_kindle_id_tag():
positioned = {
"format":"x",
"items":[
{"status":"positioned","clipping":{"id":"abc123","title":"T","added_on":None,"added_on_iso":None},
"zotero":{"attachment":{"item_id":1,"key":"K1"},"parent_item_id":10,"parent_key":"P1","citation_key":"c1"},
"annotation":{"type":"highlight","text":"hi","position":{"type":"FragmentSelector","value":"cfi"}}}
]
}
plan = build_final_writer_plan(positioned)
assert plan["annotation_count"]==1
tags = plan["annotations"][0]["annotation"]["tags"]
assert {"name":"kindle-import"} in tags
assert {"name":"kindle-id:abc123"} in tags
assert plan["annotations"][0]["clipping_id"]=="abc123"
def test_final_skips_non_positioned():
positioned = {"format":"x","items":[{"status":"epub-text-not-found","clipping":{"id":"a"},"zotero":{},"annotation":{}}]}
plan = build_final_writer_plan(positioned)
assert plan["annotation_count"]==0
assert plan["skipped_counts"]["epub-text-not-found"]==1

View file

@ -1,19 +0,0 @@
import json, tempfile, pathlib
from pathlib import Path
def test_incremental_ids(tmp_path: Path):
# simulate previous final with one integrated id
prev_final = {"annotations":[{"clipping_id":"aaa"}]}
(tmp_path/"import-plan.final.json").write_text(json.dumps(prev_final))
prev_clipp = {"clippings":[{"id":"aaa","title":"T"},{"id":"bbb","title":"U"}]}
(tmp_path/"clippings.json").write_text(json.dumps(prev_clipp))
new_ids = {"aaa","bbb","ccc"} # ccc is new
prev_integrated = {"aaa"}
to_process = new_ids - prev_integrated
assert to_process == {"bbb","ccc"}
deletions = prev_integrated - new_ids
assert deletions == set() # none removed
# changed: aaa text changed -> new id ddd, old aaa no longer in new -> deletion
new_ids2 = {"ddd","bbb","ccc"}
assert (prev_integrated - new_ids2) == {"aaa"}

View file

@ -1,14 +0,0 @@
import assert from 'assert';
// test the grouping logic for title variants sharing same candidate
function findOtherRows(rows, rowIndex, targetKey) {
const row = rows[rowIndex];
return rows.filter((r, idx) => idx !== rowIndex && (r.candidates||[]).some(c => (c.citation_key||c.key||String(c.item_id))===targetKey));
}
const rows = [
{title:"gilbert-simondon-on-the-mode", candidates:[{citation_key:"chabot2013", key:"A"}, {citation_key:"simondon2017a"}]},
{title:"On the Mode (Univocal)", candidates:[{citation_key:"chabot2013"}, {citation_key:"simondon2017a"}]},
{title:"Other", candidates:[{citation_key:"foo"}]}
];
assert.equal(findOtherRows(rows, 0, "chabot2013").length, 1);
assert.equal(findOtherRows(rows, 0, "chabot2013")[0].title, "On the Mode (Univocal)");
console.log("manager logic ok");

View file

@ -1,26 +0,0 @@
from kindle_zotero_importer.overrides import load_overrides, OverrideError
def test_ignore_only():
payload = {"format":"kindle-zotero-importer.match-overrides.v1","overrides":[{"clipping_title":"A","resolution":{"ignore":True},"review":{}}]}
assert load_overrides(payload)["A"] == {"ignore": True}
def test_ignore_with_other_fails():
payload = {"format":"kindle-zotero-importer.match-overrides.v1","overrides":[{"clipping_title":"A","resolution":{"ignore":True,"citation_key":"foo"},"review":{}}]}
try:
load_overrides(payload)
assert False, "should have raised"
except OverrideError:
pass
def test_single_field_ok():
for res in [{"citation_key":"foo"},{"zotero_key":"ABC12345"},{"zotero_item_id":12}]:
payload = {"format":"kindle-zotero-importer.match-overrides.v1","overrides":[{"clipping_title":"A","resolution":res,"review":{}}]}
assert "A" in load_overrides(payload)
def test_multi_field_fails():
payload = {"format":"kindle-zotero-importer.match-overrides.v1","overrides":[{"clipping_title":"A","resolution":{"citation_key":"a","zotero_key":"b"},"review":{}}]}
try:
load_overrides(payload)
assert False
except OverrideError:
pass