From bfe54e395342de18998dd7f678128b63489cd986 Mon Sep 17 00:00:00 2001 From: Utku Bilen Demir <84389167+UtkuBilenDemir@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:44:31 +0200 Subject: [PATCH] 0.6.4: incremental id-based import, Mappings/Integrated, black iA Duo, resizable tables - id-based incremental (new_ids - prev_integrated_ids) with --full flag, kindle-id tag, deletions handling - Mappings tab with delete to return to Conflicts, sorted by updated_at newest first - Integrated tab with Added On / Integrated dates and Citekey - black over white iA Writer Duo theme, candidate-list per-row Use buttons, resizable/sortable tables - Settings editable + save, Artifacts Open/Reveal, Re-import bar with last file reuse - Fix override single-field validation and ignore cleanup --- .gitignore | 6 + README.md | 14 + docs/mismatch-review.md | 1001 ++++++--------- match-overrides.json | 181 +-- plugin/README.md | 25 + plugin/bootstrap.js | 1275 +++++++++++++++++++ plugin/manager.html | 1199 +++++++++++++++++ plugin/manifest.json | 16 + plugin/prefs.js | 4 + plugin_runner.py | 17 + scripts/build_canary_plugin.py | 62 + scripts/build_plugin.py | 74 ++ src/kindle_zotero_importer/cli.py | 270 ++++ src/kindle_zotero_importer/epub_position.py | 23 +- src/kindle_zotero_importer/final_plan.py | 22 +- src/kindle_zotero_importer/pdf_position.py | 27 +- 16 files changed, 3501 insertions(+), 715 deletions(-) create mode 100644 plugin/README.md create mode 100644 plugin/bootstrap.js create mode 100644 plugin/manager.html create mode 100644 plugin/manifest.json create mode 100644 plugin/prefs.js create mode 100644 plugin_runner.py create mode 100644 scripts/build_canary_plugin.py create mode 100644 scripts/build_plugin.py diff --git a/.gitignore b/.gitignore index 1f733a9..06dfa7b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ __pycache__/ *.py[cod] .venv/ .pytest_cache/ +.maschine/ +dist/ +plugin-summary.json +plugin-progress.json +plugin-config.json +kindle-import-manager.html diff --git a/README.md b/README.md index af8e82a..3d43919 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,17 @@ PDF positioning uses Poppler tools (`pdftotext`, `pdftohtml`, `pdfinfo`). If a P ## Safety Rule Do not write directly to `zotero.sqlite`. Use read-only SQLite access for indexing and Zotero's JavaScript API for writes. + +## Zotero Plugin Wrapper + +The `plugin/` directory contains a hybrid Zotero plugin with one user-facing Tools action. It lets the user choose `My Clippings.txt`, runs the importer with visible progress, and writes positioned annotations through Zotero's native APIs. Unresolved cases are reported after the import; intermediate JSON artifacts are implementation details. + +Build the plugin package: + +```sh +python scripts/build_plugin.py +``` + +Install `dist/kindle-zotero-importer.xpi` in Zotero via `Tools > Plugins`. Open `Tools > Kindle Zotero Importer...`, choose `My Clippings.txt`, and keep the manager open to see the current stage, percentage, elapsed time, and final annotation counts. + +The XPI manifest targets Zotero `6.999` through `10.0.*`. diff --git a/docs/mismatch-review.md b/docs/mismatch-review.md index 310b1eb..dc306e9 100644 --- a/docs/mismatch-review.md +++ b/docs/mismatch-review.md @@ -5,8 +5,8 @@ Persistent rule: confirmed title matches go in `match-overrides.json`, not `matc ## Matched Zotero Item But No Attachment Status: `matched-title-no-attachment` -Clippings: 103 -Unique titles: 1 +Clippings: 104 +Unique titles: 2 Action: Attach/link a PDF or EPUB to the listed Zotero item, then re-index Zotero. ### Why AI Undermines Democracy and What To Do About It (Coeckelbergh, Mark) @@ -38,230 +38,38 @@ Candidates: - Example: highlight page `29` loc `678-680`: “organized as processes of communication where humans inform themselves, debate, and take collectively binding decisions” (2023: 12). This requires a public sphere: a social sphere - Example: highlight page `30` loc `686-689`: Dewey and Habermas stressed not only participation but also communication. In Democracy and Education (1944), Dewey sees communication as a process in which participants learn to s -## Unmatched Kindle Titles - -Status: `unmatched-title` -Clippings: 534 -Unique titles: 40 -Action: Add one confirmed mapping to match-overrides.json using citation_key, zotero_key, or zotero_item_id. - -### The Satanic Verses_ A Novel (Salman Rushdie) +### daily-left-news-2026-08-23 (Kindle Left News) ```yaml -clipping_title: "The Satanic Verses_ A Novel (Salman Rushdie)" -status: unmatched-title -clipping_count: 54 -kinds: {highlight: 54} -citation_key: "rushdie2011" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "epub-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `None` loc `51-53`: What characteristics which? Slow down; you think Creation happens in a rush? So then, neither does revelation . . . take a look at the pair of them. Notice anything unusual? Just t -- Example: highlight page `None` loc `88-89`: Now that I am dead I have forgotten how to forgive. I curse you, my Gibreel, may your life be hell. Hell, because that's where you sent me, damn you, where you came from, devil, wh -- Example: highlight page `None` loc `103-106`: what had taken him over was the will to live, unadulterated, irresistible, pure, and the first thing it did was to inform him that it wanted nothing to do with his pathetic persona - -### 1984 (AmazonClassics Edition) (Orwell, George) - -```yaml -clipping_title: "1984 (AmazonClassics Edition) (Orwell, George)" -status: unmatched-title -clipping_count: 46 -kinds: {highlight: 46} -citation_key: "foucault1997" -zotero_item_id: 137 -zotero_key: "GHG9JTKY" -zotero_title: "The essential works of Foucault, 1954-1984" -attachment_count: 1 +clipping_title: "daily-left-news-2026-08-23 (Kindle Left News)" +status: matched-title-no-attachment +clipping_count: 1 +kinds: {highlight: 1} +citation_key: "News2020" +zotero_item_id: 1713 +zotero_key: "5TYCKTFZ" +zotero_title: "News" +attachment_count: 0 expected_attachment_type: "pdf-or-epub" -match_score: 0.88 -match_reason: "clipping-title-contained-in-title" +match_score: 0.92 +match_reason: "title-contained-in-clipping-title" override_entry: - clipping_title: "1984 (AmazonClassics Edition) (Orwell, George)" + clipping_title: "daily-left-news-2026-08-23 (Kindle Left News)" resolution: - citation_key: "foucault1997" + citation_key: "News2020" ``` Candidates: -- citation_key: `foucault1997`, zotero_item_id: `137`, zotero_key: `GHG9JTKY`, attachments: `1`, score: `0.88`, title: The essential works of Foucault, 1954-1984 -- citation_key: `bade2018a`, zotero_item_id: `7612`, zotero_key: `V37MBFR3`, attachments: `0`, score: `0.88`, title: Vom Export der Sozialen Frage zur importierten Sozialen Frage: Deutschland im transnationalen Wanderungsgeschehen seit der Mitte des 19. Jahrhunderts [1984/1985] +- citation_key: `News2020`, zotero_item_id: `1713`, zotero_key: `5TYCKTFZ`, attachments: `0`, score: `0.92`, title: News -- Example: highlight page `9` loc `113-114`: Either the future would resemble the present, in which case it would not listen to him, or it would be different from it, and his predicament would be meaningless. -- Example: highlight page `9` loc `118-118`: At this moment, however, even the monologue had dried up. -- Example: highlight page `9` loc `116-118`: For weeks past he had been making ready for this moment, and it had never crossed his mind that anything would be needed except courage. The actual writing would be easy. All he ha +- Example: highlight page `3` loc `27-27`: Marx a proletarian thinker. -### mew_band03 +## Unmatched Kindle Titles -```yaml -clipping_title: "mew_band03" -status: unmatched-title -clipping_count: 40 -kinds: {highlight: 40} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `19-19` loc `None`: Diese Abhängigkeit von Hegel ist der Grund, warum keiner dieser neueren Kritiker eine umfassende Kritik des Hegeischen Systems auch nur versuchte, sosehr Jeder von ihnen behauptet, -- Example: highlight page `19-19` loc `None`: Nach und nach wurde jedes herrschende Verhältnis für ein Verhältnis der Religion erklärt und in Kultus verwandelt, Kultus des Rechts, Kultus des Staats pp. Überall hatte man es nur -- Example: highlight page `19-19` loc `None`: Die Althegelianer hatten Alles begriffen, sobald es auf eine Hegeische logische Kategorie zurückgeführt war. Die Junghegelianer kritisierten Alles, indem sie ihm religiöse Vorstell - -### MiloradPaviç_-_ hazar_sözlügü - -```yaml -clipping_title: "MiloradPaviç_-_ hazar_sözlügü" -status: unmatched-title -clipping_count: 37 -kinds: {highlight: 36, note: 1} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `15-15` loc `None`: Tersine çevrilebilir' sanatlar olan mimarlık, heykel ve resimde böyledir durum. Ama başka sanatlar da vardır... Tek yönlü sokaklara, her şeyin başlangıcından sonuna, doğumundan ölü -- Example: highlight page `23-23` loc `None`: Okuyucu dokuzuncu sayfadaki şu sözcükleri okuyunca da ölüyordu: Verbum caro/actum est (Kelam beden oldu). Denetim nüshası zehirli yapıtla birlikte okunduğu takdirde, ölümün yaklaşt -- Example: highlight page `31-31` loc `None`: İki insan düşünün, her biri bir ipin iki ucundan çekiyor ve ipin ortasında da bir puma tutuyorlar böylece. Aynı anda birbirlerine yaklaşmak isteseler, puma saldıracak onlara çünkü - -### DRdZ 01 - Die Suche nach dem Auge der Welt (Robert Jordan) - -```yaml -clipping_title: "DRdZ 01 - Die Suche nach dem Auge der Welt (Robert Jordan)" -status: unmatched-title -clipping_count: 33 -kinds: {highlight: 33} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "epub-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `None` loc `6909-6909`: Egwene zeigte nicht ihre sonst so einwandfreien Manieren; -- Example: highlight page `None` loc `7257-7257`: gebaut in Zeitalter von Legenden, man sagt. Da sein eine -- Example: highlight page `None` loc `7257-7258`: gebaut in Zeitalter von Legenden, man sagt. Da sein eine Wand mit Fries, der zeigt Tiere, die kein lebender - -### Marcus Aurelius (Emperor of Rome), Martin Hammond, Diskin Clay - Meditations (2006, Penguin Books) - libgen.li - -```yaml -clipping_title: "Marcus Aurelius (Emperor of Rome), Martin Hammond, Diskin Clay - Meditations (2006, Penguin Books) - libgen.li" -status: unmatched-title -clipping_count: 33 -kinds: {highlight: 33} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `xv-xv` loc `None`: which show the emperor addressing his troops in a formal adlocutio -- Example: highlight page `xxv-xxv` loc `None`: Throughout the history of the Stoa the emulation of Socrates generated the moral impetus to the way of thought and action distinctive of the Stoa. We find it in Epictetus (c. AD 55 -- Example: highlight page `xxxiv-xxxiv` loc `None`: There are other therapies in Marcus' repertoire of spiritual exercises that are not as difficult to accept as his radical dissecting and 'denuding' of human experience - -### On Populist Reason (Laclau, Ernesto) - -```yaml -clipping_title: "On Populist Reason (Laclau, Ernesto)" -status: unmatched-title -clipping_count: 33 -kinds: {highlight: 33} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `135` loc `2285-2285`: As Kazin points out, until 1940 the notion of a conservative populism was an oxymoron. -- Example: highlight page `135` loc `2285-2287`: As Kazin points out, until 1940 the notion of a conservative populism was an oxymoron. There was no connection between populism and the discourse of the traditional Right, which wa -- Example: highlight page `135` loc `2285-2291`: As Kazin points out, until 1940 the notion of a conservative populism was an oxymoron. There was no connection between populism and the discourse of the traditional Right, which wa - -### Martyr! -- Akbar, Kaveh -- null, null, 2024 -- RANDOM HOUSE US -- 9780593802359 -- 930ede856e2a2efb28505e3c4e99e222 -- Anna’s Archive (Kaveh Akbar) - -```yaml -clipping_title: "Martyr! -- Akbar, Kaveh -- null, null, 2024 -- RANDOM HOUSE US -- 9780593802359 -- 930ede856e2a2efb28505e3c4e99e222 -- Anna’s Archive (Kaveh Akbar)" -status: unmatched-title -clipping_count: 31 -kinds: {highlight: 31} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `21` loc `260-260`: maturation of the masses, these treatments would likely be palliative. -- Example: highlight page `35` loc `428-430`: “I’ve read your poems, Cyrus. I get that you’re Persian. Born there, raised here. I know that’s a part of you. But you’ve probably spent more time looking at your phone today, just -- Example: highlight page `37` loc `459-461`: “That certainty is what put worms in their brains, Cyrus. The only people who speak in certainties are zealots and tyrants.” “Sure, sure. But there’s no tiny secret part of you tha - -### (Kapitalismus und Schizophrenie 2) Guattari, Deleuze - Tausend Plateaus, Mille Plateaux-Les Editions de Minuit (1980) - -```yaml -clipping_title: "(Kapitalismus und Schizophrenie 2) Guattari, Deleuze - Tausend Plateaus, Mille Plateaux-Les Editions de Minuit (1980)" -status: unmatched-title -clipping_count: 27 -kinds: {highlight: 27} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `156-156` loc `None`: Deshalb kann man äußerstenfalls sogar auf den Begriff des Zeichens verzichten, da man prinzipiell nicht die Beziehung zwischen einem Zeichen und einem Sachverhalt, den es kennzeich -- Example: highlight page `156-156` loc `None`: Deshalb kann man äußerstenfalls sogar auf den Begriff des Zeichens verzichten, da man prinzipiell nicht die Beziehung zwischen einem Zeichen und einem Sachverhalt, den es kennzeich -- Example: highlight page `156-156` loc `None`: Das Zeichen hat also schon einen hohen Grad relativer Deterritorialisierung erreicht - -### Arendt_2017_ - -```yaml -clipping_title: "Arendt_2017_" -status: unmatched-title -clipping_count: 20 -kinds: {highlight: 20} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `1-1` loc `None`: This transformation never occurred in the totalitarian, the Bolshevik and the Nazi movements. At the time it seized power the danger to the movement lay in the fact that, on one ha -- Example: highlight page `2-2` loc `None`: The point is that both Hitler and Stalin held out promises of stability in order to hide their intention of creating a state of permanent instability -- Example: highlight page `2-2` loc `None`: For the totalitarian ruler is confronted with a dual task which at first appears contradictory to the point of absurdity: he must establish the fictitious world of the movement as +Status: `unmatched-title` +Clippings: 64 +Unique titles: 9 +Action: Add one confirmed mapping to match-overrides.json using citation_key, zotero_key, or zotero_item_id. ### Dune (Frank Herbert) @@ -292,90 +100,6 @@ Candidates: - Example: highlight page `None` loc `243-243`: “Hope clouds observation.” - Example: highlight page `None` loc `243-243`: She extinguished the excitement, reminding herself: “Hope clouds observation.” -### paul-lafargue-the-right-to-be-lazy (Paul Lafargue) - -```yaml -clipping_title: "paul-lafargue-the-right-to-be-lazy (Paul Lafargue)" -status: unmatched-title -clipping_count: 17 -kinds: {highlight: 17} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `5` loc `42-43`: For the Spaniard, in whom the primitive animal has not been atrophied, work is the worst sort of slavery. -- Example: highlight page `9` loc `91-92`: Modern factories have become ideal houses of correction in which the toiling masses are imprisoned, -- Example: highlight page `13` loc `156-159`: requires too much violence and makes too much noise. Hunger, on the contrary, is not only a pressure which is peaceful, silent and incessant, but as it is the most natural motive f - -### Fire & Blood (A Song of Ice and Fire) (George R. R. Martin) - -```yaml -clipping_title: "Fire & Blood (A Song of Ice and Fire) (George R. R. Martin)" -status: unmatched-title -clipping_count: 15 -kinds: {highlight: 15} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "epub-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `None` loc `2164-2167`: Regent, in a rivalry that would continue into Jaehaerys’s own reign and threaten to plunge the Seven Kingdoms back into war.*1 The immediate cause of the tension was the king’s sud -- Example: highlight page `None` loc `2236-2237`: of you.” For her, these days on Dragonstone were an idyll. -- Example: highlight page `None` loc `5746-5747`: from Westeros soon after the wedding, crossing the narrow - -### Hardt_Negri_2017_ - -```yaml -clipping_title: "Hardt_Negri_2017_" -status: unmatched-title -clipping_count: 14 -kinds: {highlight: 14} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `xiii-xiii` loc `None`: Save few exceptions, these movements either have abandoned their radical aspirations and become players in the existing systems or have been defeated by ferocious repression -- Example: highlight page `xiii-xiii` loc `None`: Everything turns,” as Hegel says, “on grasping and expressing the True not only as Substance, but equally as Subject.”1 We must train our eyes to recognize how the movements have t -- Example: highlight page `xiii-xiii` loc `None`: Everything turns,” as Hegel says, “on grasping and expressing the True not only as Substance, but equally as Subject.”1 We must train our eyes to recognize how the movements have t - -### Exhalation (Ted Chiang) - -```yaml -clipping_title: "Exhalation (Ted Chiang)" -status: unmatched-title -clipping_count: 13 -kinds: {highlight: 13} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `111` loc `1601-1605`: The researchers conclude that there’s something missing in the Origami genome, but as far as Derek’s concerned, the fault lies with them. They’re blind to a simple truth: complex m -- Example: highlight page `64` loc `902-903`: mutism was very rare, a result of damage to the anterior cingulate region of the brain. Now it spreads like a cognitive plague. -- Example: highlight page `64` loc `901-903`: Before people started playing with Predictors, akinetic mutism was very rare, a result of damage to the anterior cingulate region of the brain. Now it spreads like a cognitive plag - ### Brave New World (Aldous Huxley) ```yaml @@ -416,23 +140,26 @@ citation_key: "" zotero_item_id: null zotero_key: "" zotero_title: "" -attachment_count: null +attachment_count: 0 expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" +match_score: 0.0 +match_reason: "override-unresolved:Galloway_2004_Protocol" ``` +Candidates: +- citation_key: `None`, zotero_item_id: `0`, zotero_key: ``, attachments: `0`, score: `0.0`, title: None + - Example: highlight page `13-13` loc `None`: This book—Protocol—points to one possible path for doing this. Throughout the discussions on power, control, and decentralization, Protocol consistently makes a case for a material - Example: highlight page `14-14` loc `None`: Further, code is not necessarily language, and certainly not a sign. A code, in the sense that Protocol defnes it, is process-based: It is parsed, compiled, procedural or object-or - Example: highlight page `14-14` loc `None`: Further, code is not necessarily language, and certainly not a sign. A code, in the sense that Protocol defnes it, is process-based: It is parsed, compiled, procedural or object-or -### Chantal Mouffe - Hegemonie und radikale Demokratie.-Passagen Verlag Ges.M.B.H (2000) +### somers-hall-binding-and-axiomatics ```yaml -clipping_title: "Chantal Mouffe - Hegemonie und radikale Demokratie.-Passagen Verlag Ges.M.B.H (2000)" +clipping_title: "somers-hall-binding-and-axiomatics" status: unmatched-title -clipping_count: 8 -kinds: {highlight: 8} +clipping_count: 10 +kinds: {highlight: 9, note: 1} citation_key: "" zotero_item_id: null zotero_key: "" @@ -443,17 +170,17 @@ match_score: null match_reason: "" ``` -- Example: highlight page `22-22` loc `None`: ln der analytischen Tradition ist dies die Bewegung, die mit der Philosophie des späten Wittgenstein beginnt und in den verschiedenen Formen post-wittgensieinschen Denkens beibehal -- Example: highlight page `184-184` loc `None`: politische Frage sich auf den Gegensatz Proletariat oder Kapitalisten zuspitzte... Es war schon eine ganz besondere taktische Geschicklichkeit der demokratischen Partei erforderlic -- Example: highlight page `184-184` loc `None`: die Auflösung dieses amorphen „Volkes“ und sein 1 .i set/en durch <|j(. de soziale Basis der Arbeiterklasse hätten populäre Bewegungen die erlangt, die ihnen einen langf ristigen K +- Example: note page `4` loc `None`: Then again, this might be the axiomatic nature of the machinic expansion . +- Example: highlight page `4-4` loc `None`: In this sense, then, we have a series of indeterminate primitives which can be expressed in a variety of models. Deleuze and Guattari will argue that this abstraction of the axioma +- Example: highlight page `5-5` loc `None`: This claim amounts to noting that without being able to see all of my experiences as my own, experience would be fragmentary. Now, this analytic unity for Kant presupposes a moment -### The City & the City - China Mieville +### Nemo Ramjet - All Tomorrows (2018) - libgen.li ```yaml -clipping_title: "The City & the City - China Mieville" +clipping_title: "Nemo Ramjet - All Tomorrows (2018) - libgen.li" status: unmatched-title -clipping_count: 8 -kinds: {highlight: 8} +clipping_count: 5 +kinds: {highlight: 5} citation_key: "" zotero_item_id: null zotero_key: "" @@ -464,135 +191,9 @@ match_score: null match_reason: "" ``` -- Example: highlight page `12-12` loc `None`: We would never call inexplicable little insights “hunches,” for fear of drawing the universe’s attention. But they happened, and you knew you had been in the proximity of one that -- Example: highlight page `23-23` loc `None`: groove by the raised spill-stopper, and it had come back clean. The stains -- Example: highlight page `142-142` loc `None`: Not very long ago the equivalent tests would have involved being asked about the different national character of Ul Qomans, and judging who from various pictures with stereotyped p - -### Latzer Just (2020) - -```yaml -clipping_title: "Latzer Just (2020)" -status: unmatched-title -clipping_count: 7 -kinds: {highlight: 7} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `2-2` loc `None`: provided by Gillespie: “encoded procedures for transforming input data into a desired output, based on specified calculations” (Gillespie, 2014, p.167) -- Example: highlight page `5-5` loc `None`: communications, discussions about the governance by algorithms often focus on the influence on opinion formation and consequently the potential for its manipulation during polls an -- Example: highlight page `5-5` loc `None`: which is executed via biased filter bubbles (decreasing variety of content), microtargeting (e.g. , political microtargeting, see Zuiderveen Borgesius et al. , 2018; or online beha - -### Deleuze and Guattaris - -```yaml -clipping_title: "Deleuze and Guattaris" -status: unmatched-title -clipping_count: 6 -kinds: {highlight: 6} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `38-38` loc `None`: productive unconscious, built around the concept of desiring-machines -- Example: highlight page `41-41` loc `None`: What the schizophrenic lives, both as an individual and as a member of the human species, is not at all any one specific aspect of nature, but nature as & process of production' (A -- Example: highlight page `41-41` loc `None`: validity" to mitigate the despair, terror, and boredom of existence'.2 It is not simply that as Lenz sees them, the earth, the wind, the water and so on are infused with a life of - -### Ernesto Laclau - On Populist Reason-Verso (2005) - -```yaml -clipping_title: "Ernesto Laclau - On Populist Reason-Verso (2005)" -status: unmatched-title -clipping_count: 6 -kinds: {highlight: 6} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `127-127` loc `None`: This, as we have seen, leads to one of the demands stepping in and becoming the signifier of the whole chain — a tendentially empty signifier -- Example: highlight page `127-127` loc `None`: This, as we have seen, leads to one of the demands stepping in and becoming the signifier of the whole chain — a tendentially empty signifier. But the whole model depends on the pr -- Example: highlight page `127-127` loc `None`: This, as we have seen, leads to one of the demands stepping in and becoming the signifier of the whole chain — a tendentially empty signifier - -### Lem, Stanislaw - Solaris (Stanisław Lem) - -```yaml -clipping_title: "Lem, Stanislaw - Solaris (Stanisław Lem)" -status: unmatched-title -clipping_count: 6 -kinds: {highlight: 6} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `7` loc `52-53`: extended vast and flat; from the size of the streaks on its surface I could tell I was still a long way off. Or rather, a long way up, since I’d already passed that intangible boun -- Example: highlight page `15` loc `298-300`: In this way it was physicists, not biologists, who proposed the paradoxical formulation “plasmic machine” to refer to a formation that in our sense might be devoid of life, but was -- Example: highlight page `48` loc `1262-1269`: We’re humanitarian and noble, we’ve no intention of subjugating other races, we only want to impart our values to them and in return, to appropriate their heritage. We see ourselve - -### Deleuze, Guattari- A Thousand Plateaus - Bobo - -```yaml -clipping_title: "Deleuze, Guattari- A Thousand Plateaus - Bobo" -status: unmatched-title -clipping_count: 4 -kinds: {highlight: 4} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `172-172` loc `None`: BwO is what remains when you take everything away -- Example: highlight page `172-172` loc `None`: BwO is what remains when you take everything away. What you take away is precisely the phantasy, and signifiances and subjectifications as a whole. Psychoanalysis does the opposite -- Example: highlight page `172-172` loc `None`: BwO is what remains when you take everything away. What you take away is precisely the phantasy, and signifiances and subjectifications as a whole. Psychoanalysis does the opposite - -### Howaldt_Schwarz_2016_SI-DRIVE - -```yaml -clipping_title: "Howaldt_Schwarz_2016_SI-DRIVE" -status: unmatched-title -clipping_count: 4 -kinds: {highlight: 4} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `40-40` loc `None`: transformation possibilities and creative potentials” (Antoniolo 2010, p.13) in society and search “for the conditions of the genesis and production of the new in all areas of life -- Example: highlight page `40-40` loc `None`: Thus the concept of micro-politics focuses on the continuous “differentiations of social practices”, on “the large number of differentially determined, interacting currents which r -- Example: highlight page `41-41` loc `None`: (ibid. ) Micropolitical analysis focuses on these structures and the property of being entangled in structures, or in other words on the relational structures at the micro-level. F +- Example: highlight page `10` loc `None`: Heyday +- Example: highlight page `36-36` loc `None`: structure of cerebellum so that certain features associated with heuristic +- Example: highlight page `59-59` loc `None`: world. On one hand there were several races of almost Australopithecine ### (Grundstudium Mathematik) Herbert Amann, Joachim Escher - Analysis 1-Birkhauser Basel (2006) @@ -623,13 +224,41 @@ Candidates: - Example: highlight page `21-21` loc `None`: , es gibt genau eine leere Menge; sie wird mit ∅ bezeichnet und ist Teilmenge jeder Menge - Example: highlight page `21-21` loc `None`: Die leere Menge besitzt jede Eigenschaft -### Deleuze, Bartleby; or, the Formula +### Rethinking Algorithmic Regulation Kybern - Unknown ```yaml -clipping_title: "Deleuze, Bartleby; or, the Formula" +clipping_title: "Rethinking Algorithmic Regulation Kybern - Unknown" status: unmatched-title -clipping_count: 3 -kinds: {highlight: 3} +clipping_count: 2 +kinds: {highlight: 2} +citation_key: "medina2015" +zotero_item_id: 350 +zotero_key: "SGZMZ9X7" +zotero_title: "Rethinking algorithmic regulation" +attachment_count: 1 +expected_attachment_type: "pdf-preferred" +match_score: 0.92 +match_reason: "title-contained-in-clipping-title" +override_entry: + clipping_title: "Rethinking Algorithmic Regulation Kybern - Unknown" + resolution: + citation_key: "medina2015" +``` + +Candidates: +- citation_key: `medina2015`, zotero_item_id: `350`, zotero_key: `SGZMZ9X7`, attachments: `1`, score: `0.92`, title: Rethinking algorithmic regulation +- citation_key: `goffey2008`, zotero_item_id: `208`, zotero_key: `I4C9Y46T`, attachments: `0`, score: `0.92`, title: Algorithm. + +- Example: highlight page `5-5` loc `None`: increase worker participation in the economy and preserve the autonomy of factory managers, even with the expansion of state influence. +- Example: highlight page `5-5` loc `None`: synergy,” the idea that the whole of the system was more than the sum of its technological parts. + +### parrhesia01_sinnerbrink + +```yaml +clipping_title: "parrhesia01_sinnerbrink" +status: unmatched-title +clipping_count: 1 +kinds: {highlight: 1} citation_key: "" zotero_item_id: null zotero_key: "" @@ -640,74 +269,26 @@ match_score: null match_reason: "" ``` -- Example: highlight page `1-1` loc `None`: This is an agrammatical formula that stands as the limit of a series of correct expressions: J'en ai de trop, Je n'en ai pas assez, II m'en manque un ..“I have too many,” “I don't -- Example: highlight page `3-3` loc `None`: If need be, other languages will be summoned into English in order to make it echo this divine language of storm and thunder. Melville invents a foreign language that runs beneath -- Example: highlight page `3-3` loc `None`: If need be, other languages will be summoned into English in order to make it echo this divine language of storm and thunder. Melville invents a foreign language that runs beneath +- Example: highlight page `1-1` loc `None`: And Zizek’s Organs without Bodies certainly remains true to this Deleuzian insight. In this traumatic philosophical encounter, Zizek misunderstands Deleuze, who misunderstands Hege -### The Republic (Plato) +### Raymond M. Smullyan - The Chess Mysteries of Sherlock Holmes-Hutchinson (1980) ```yaml -clipping_title: "The Republic (Plato)" +clipping_title: "Raymond M. Smullyan - The Chess Mysteries of Sherlock Holmes-Hutchinson (1980)" status: unmatched-title -clipping_count: 3 -kinds: {highlight: 3} +clipping_count: 1 +kinds: {highlight: 1} citation_key: "" zotero_item_id: null zotero_key: "" zotero_title: "" attachment_count: null -expected_attachment_type: "epub-preferred" +expected_attachment_type: "pdf-preferred" match_score: null match_reason: "" ``` -- Example: highlight page `None` loc `465-467`: as the government must be supposed to have power, the only reasonable conclusion is, that everywhere there is one principle of justice, which is the interest of the stronger. -- Example: highlight page `None` loc `532-533`: Then, I said, no science or art considers or enjoins the interest of the stronger or superior, but only the interest of the subject and weaker? -- Example: highlight page `None` loc `550-552`: First of all, in private contracts: wherever the unjust is the partner of the just you will find that, when the partnership is dissolved, the unjust man has always more and the jus - -### The Way of Kings (Sanderson, Brandon) - -```yaml -clipping_title: "The Way of Kings (Sanderson, Brandon)" -status: unmatched-title -clipping_count: 3 -kinds: {highlight: 3} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "epub-preferred" -match_score: null -match_reason: "" -``` - -- Example: highlight page `None` loc `369-370`: For if you were going to assassinate a man, he was entitled to see you coming. -- Example: highlight page `None` loc `366-370`: Today, that included wearing white. Loose white trousers tied at the waist with a rope, and over them a filmy shirt with long sleeves, open at the front. White clothing for a kille -- Example: highlight page `None` loc `407-408`: As Truthless, there was only one life he was forbidden to take. And that was his own. - -### Your P2K Articles (2024-12-17) (P2K) - -```yaml -clipping_title: "Your P2K Articles (2024-12-17) (P2K)" -status: unmatched-title -clipping_count: 3 -kinds: {highlight: 3} -citation_key: "" -zotero_item_id: null -zotero_key: "" -zotero_title: "" -attachment_count: null -expected_attachment_type: "pdf-or-epub" -match_score: null -match_reason: "" -``` - -- Example: highlight page `11` loc `85-85`: Captagon, -- Example: highlight page `12` loc `90-92`: particular importance here is Hezbollah, whose elite units played a key part in fighting the jihadi opposition in Syria. But most of these have been transferred to Lebanon to fight -- Example: highlight page `15` loc `114-117`: Nobody knows if Iran and the regime would have been weakened without the recent Israeli attacks in Syria, which have allowed us to return and free the lands and the country,’ a man - -_Showing first 30 of 40 unique titles._ +- Example: highlight page `23` loc `None`: retrograde ## Matched Attachment But Missing File Path @@ -792,8 +373,8 @@ Candidates: ## Attachment Found But Text Position Failed Status: `epub-text-not-found` -Clippings: 183 -Unique titles: 12 +Clippings: 269 +Unique titles: 15 Action: Needs looser EPUB text matching or manual review. ### Dostoyevsky and Taluy_2020_Karamazov kardeşler @@ -812,16 +393,16 @@ expected_attachment_type: "pdf-preferred" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 40547 - attachment_key: "HN83F6ZU" + - attachment_item_id: 73511 + attachment_key: "XSMFAN6T" attachment_title: "Dostoyevsky and Taluy_2020_Karamazov kardeşler 1.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Dostoyevsky and Taluy_2020_Karamazov kardeşler 1.epub" - - attachment_item_id: 40543 - attachment_key: "V42288YE" + path: "/Users/ubd/Zotero/storage/XSMFAN6T/Dostoyevsky and Taluy_2020_Karamazov kardeşler 1.epub" + - attachment_item_id: 73512 + attachment_key: "DP9W5GQQ" attachment_title: "Dostoyevsky and Taluy_2020_Karamazov kardeşler.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Dostoyevsky and Taluy_2020_Karamazov kardeşler.pdf" + path: "/Users/ubd/Zotero/storage/DP9W5GQQ/Dostoyevsky and Taluy_2020_Karamazov kardeşler.pdf" override_entry: clipping_title: "Dostoyevsky and Taluy_2020_Karamazov kardeşler" resolution: @@ -837,6 +418,42 @@ Candidates: - Example: highlight page `VII-VII` loc `None`: Kahramamı Aleksey Fyodoroviç Karamazov'un hayat hikayesine başlarken duraksıyorum biraz. Nedeni de şu: Aleksey Fyodoroviç'i kahraman olarak alıken, onun hiç de büyük bir adam olmad - Example: highlight page `VIII-VIII` loc `None`: istemezlerse? Üstelik, bunun böyle olacağını şimdiden, üzü­ lerek kestirebiliyorum. Bence bu adamda bazı özellikler var, ama bunu okurlara kanıtiayıp kanıtlayamayacağımdan pek emin +### Coogan_2018_The New Oxford Annotated Bible New Revised Standard Version (Michael Coogan) + +```yaml +clipping_title: "Coogan_2018_The New Oxford Annotated Bible New Revised Standard Version (Michael Coogan)" +status: epub-text-not-found +clipping_count: 51 +kinds: {highlight: 51} +citation_key: "coogan2018" +zotero_item_id: 75553 +zotero_key: "8YG5SPS6" +zotero_title: "The New Oxford Annotated Bible: New Revised Standard Version" +attachment_count: 1 +expected_attachment_type: "epub" +match_score: 1.0 +match_reason: "citationKey" +attachment_choices: + - attachment_item_id: 75558 + attachment_key: "2L98LUYN" + attachment_title: "Coogan_2018_The New Oxford Annotated Bible New Revised Standard Version.epub" + content_type: "application/epub+zip" + path: "/Users/ubd/Zotero/storage/2L98LUYN/Coogan_2018_The New Oxford Annotated Bible New Revised Standard Version.epub" +override_entry: + clipping_title: "Coogan_2018_The New Oxford Annotated Bible New Revised Standard Version (Michael Coogan)" + resolution: + citation_key: "coogan2018" + attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" + attachment_item_id: null +``` + +Candidates: +- citation_key: `coogan2018`, zotero_item_id: `75553`, zotero_key: `8YG5SPS6`, attachments: `1`, score: `1.0`, title: The New Oxford Annotated Bible: New Revised Standard Version + +- Example: highlight page `1384` loc `80321-80322`: The scribes were not a distinct religious group, but a loose assemblage of lawyers, teachers, and interpreters of the Torah (Sir 39.1–11). +- Example: highlight page `1390` loc `80530-80534`: You are the light of the world. A city built on a hill cannot be hid. 15No one after lighting a lamp puts it under the bushel basket, but on the lampstand, and it gives light to al +- Example: highlight page `1391` loc `80539-80541`: 20For I tell you, unless your righteousness exceeds that of the scribes and Pharisees, you will never enter the kingdom of heaven. + ### Gilles Deleuze, Felix Guattari - Anti-Oedipus_ Capitalism and Schizophrenia-Univ Of Minnesota Press (1983) ```yaml @@ -853,21 +470,21 @@ expected_attachment_type: "pdf-preferred" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 42874 - attachment_key: "5PMKEQHR" + - attachment_item_id: 73477 + attachment_key: "WM3I2QU5" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" - - attachment_item_id: 42876 - attachment_key: "HUBBR88Z" + path: "/Users/ubd/Zotero/storage/WM3I2QU5/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" + - attachment_item_id: 73478 + attachment_key: "QCT92KP4" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" - - attachment_item_id: 42873 - attachment_key: "WRX7QDU5" + path: "/Users/ubd/Zotero/storage/QCT92KP4/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" + - attachment_item_id: 73479 + attachment_key: "YQT2MIE6" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" + path: "/Users/ubd/Zotero/storage/YQT2MIE6/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" override_entry: clipping_title: "Gilles Deleuze, Felix Guattari - Anti-Oedipus_ Capitalism and Schizophrenia-Univ Of Minnesota Press (1983)" resolution: @@ -899,16 +516,16 @@ expected_attachment_type: "pdf-preferred" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 73104 - attachment_key: "QKYEIMI6" + - attachment_item_id: 73434 + attachment_key: "MYA5AUIH" attachment_title: "Burroughs_2012_The soft machine.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Burroughs_2012_The soft machine.epub" - - attachment_item_id: 73106 - attachment_key: "29NYGBJ9" + path: "/Users/ubd/Zotero/storage/MYA5AUIH/Burroughs_2012_The soft machine.epub" + - attachment_item_id: 73435 + attachment_key: "BTJ4A2BJ" attachment_title: "Burroughs_2012_The soft machine.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Burroughs_2012_The soft machine.pdf" + path: "/Users/ubd/Zotero/storage/BTJ4A2BJ/Burroughs_2012_The soft machine.pdf" override_entry: clipping_title: "William S. Burroughs - The Soft Machine-Grove Press (1992)" resolution: @@ -924,54 +541,51 @@ Candidates: - Example: highlight page `30-30` loc `None`: Switch Artist me. Oh, there goes my frequency. I'm on - Example: highlight page `37-37` loc `None`: More routine work—I studied Mayan and listened to it on the tape recorder and mixed Mayan in with English—I made innumerable photomontages of Mayan codices and artifacts—the next s -### The Communist Hypothesis (Alain Badiou) +### 1984 (AmazonClassics Edition) (Orwell, George) ```yaml -clipping_title: "The Communist Hypothesis (Alain Badiou)" +clipping_title: "1984 (AmazonClassics Edition) (Orwell, George)" status: epub-text-not-found -clipping_count: 15 -kinds: {highlight: 15} -citation_key: "badiou2015" -zotero_item_id: 472 -zotero_key: "GEH63EJT" -zotero_title: "The communist hypothesis" -attachment_count: 2 -expected_attachment_type: "epub-preferred" +clipping_count: 23 +kinds: {highlight: 23} +citation_key: "orwell2017" +zotero_item_id: 73326 +zotero_key: "SV7WPQKQ" +zotero_title: "1984" +attachment_count: 1 +expected_attachment_type: "epub" match_score: 0.98 match_reason: "title-exact" attachment_choices: - - attachment_item_id: 73096 - attachment_key: "WVAYQNEG" - attachment_title: "Badiou_2015_The communist hypothesis.epub" + - attachment_item_id: 73769 + attachment_key: "GBUWLWRY" + attachment_title: "Orwell and Fromm_2017_1984.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Badiou_2015_The communist hypothesis.epub" - - attachment_item_id: 73098 - attachment_key: "WR4TFLUZ" - attachment_title: "Badiou_2015_The communist hypothesis.pdf" - content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Badiou_2015_The communist hypothesis.pdf" + path: "/Users/ubd/Zotero/storage/GBUWLWRY/Orwell and Fromm_2017_1984.epub" override_entry: - clipping_title: "The Communist Hypothesis (Alain Badiou)" + clipping_title: "1984 (AmazonClassics Edition) (Orwell, George)" resolution: - citation_key: "badiou2015" + citation_key: "orwell2017" attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" attachment_item_id: null ``` Candidates: -- citation_key: `badiou2015`, zotero_item_id: `472`, zotero_key: `GEH63EJT`, attachments: `2`, score: `0.98`, title: The communist hypothesis +- citation_key: `orwell2017`, zotero_item_id: `73326`, zotero_key: `SV7WPQKQ`, attachments: `1`, score: `0.98`, title: 1984 +- citation_key: `foucault1997`, zotero_item_id: `137`, zotero_key: `GHG9JTKY`, attachments: `1`, score: `0.88`, title: The essential works of Foucault, 1954-1984 +- citation_key: `bade2018a`, zotero_item_id: `7612`, zotero_key: `V37MBFR3`, attachments: `0`, score: `0.88`, title: Vom Export der Sozialen Frage zur importierten Sozialen Frage: Deutschland im transnationalen Wanderungsgeschehen seit der Mitte des 19. Jahrhunderts [1984/1985] -- Example: highlight page `None` loc `80-87`: for example, that ‘Fermat’s theorem’ is a hypothesis if we formulate it as: ‘For >n, I assume that the equation xn + yn = zn has no whole solutions (solutions in which x, y and z a -- Example: highlight page `None` loc `619-623`: The bourgeoisie had indeed found a convenient hiding place inside the party, and the means to forge its new power, as we can see from China today, now that it has embarked upon a n -- Example: highlight page `None` loc `905-907`: I use the term ‘saturation’ in the sense given to it by Sylvain Lazarus;2 I will attempt to show that the Cultural Revolution is the +- Example: highlight page `9` loc `113-114`: Either the future would resemble the present, in which case it would not listen to him, or it would be different from it, and his predicament would be meaningless. +- Example: highlight page `11` loc `148-149`: It was always the women, and above all the young ones, who were the most bigoted adherents of the Party, the swallowers of slogans, the amateur spies and nosers-out of unorthodoxy +- Example: highlight page `15` loc `209-209`: current, turning one even against one’s will into a grimacing, ### Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide (Holland, Eugene W.) ```yaml clipping_title: "Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide (Holland, Eugene W.)" status: epub-text-not-found -clipping_count: 13 -kinds: {highlight: 13} +clipping_count: 20 +kinds: {highlight: 20} citation_key: "holland2013" zotero_item_id: 45058 zotero_key: "MR7YD8FW" @@ -981,11 +595,11 @@ expected_attachment_type: "epub" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 45062 - attachment_key: "DVSTKU2W" + - attachment_item_id: 73612 + attachment_key: "UQWJT8YQ" attachment_title: "Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide.epub" + path: "/Users/ubd/Zotero/storage/UQWJT8YQ/Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide.epub" override_entry: clipping_title: "Holland_2013_Deleuze and Guattari's A thousand plateaus a reader's guide (Holland, Eugene W.)" resolution: @@ -1001,13 +615,54 @@ Candidates: - Example: highlight page `41` loc `513-516`: The economic instance, in other words, is a Problematic virtual structure (how to produce means of life to assure survival) to which various societies (or a given society at variou - Example: highlight page `41` loc `520-523`: debt; whereas Marx considered production to be primary, Deleuze & Guattari draw here on Nietzsche more than Marx, and instead consider debt to be the primary organizing element in +### The Communist Hypothesis (Alain Badiou) + +```yaml +clipping_title: "The Communist Hypothesis (Alain Badiou)" +status: epub-text-not-found +clipping_count: 15 +kinds: {highlight: 15} +citation_key: "badiou2015" +zotero_item_id: 472 +zotero_key: "GEH63EJT" +zotero_title: "The communist hypothesis" +attachment_count: 2 +expected_attachment_type: "epub-preferred" +match_score: 1.0 +match_reason: "override-citation-key:badiou2015" +attachment_choices: + - attachment_item_id: 73375 + attachment_key: "RKTNKNWN" + attachment_title: "Badiou_2015_The communist hypothesis.epub" + content_type: "application/epub+zip" + path: "/Users/ubd/Zotero/storage/RKTNKNWN/Badiou_2015_The communist hypothesis.epub" + - attachment_item_id: 73376 + attachment_key: "QYX3T3JH" + attachment_title: "Badiou_2015_The communist hypothesis.pdf" + content_type: "application/pdf" + path: "/Users/ubd/Zotero/storage/QYX3T3JH/Badiou_2015_The communist hypothesis.pdf" +override_entry: + clipping_title: "The Communist Hypothesis (Alain Badiou)" + resolution: + citation_key: "badiou2015" + attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" + attachment_item_id: null +``` + +Candidates: +- citation_key: `badiou2015`, zotero_item_id: `472`, zotero_key: `GEH63EJT`, attachments: `2`, score: `1.0`, title: The communist hypothesis + +- Example: highlight page `None` loc `80-87`: for example, that ‘Fermat’s theorem’ is a hypothesis if we formulate it as: ‘For >n, I assume that the equation xn + yn = zn has no whole solutions (solutions in which x, y and z a +- Example: highlight page `None` loc `619-623`: The bourgeoisie had indeed found a convenient hiding place inside the party, and the means to forge its new power, as we can see from China today, now that it has embarked upon a n +- Example: highlight page `None` loc `905-907`: I use the term ‘saturation’ in the sense given to it by Sylvain Lazarus;2 I will attempt to show that the Cultural Revolution is the + ### (fixed) Negarestani_2008_Cyclonopedia complicity with anonymous materials (Reza Negarestani) ```yaml clipping_title: "(fixed) Negarestani_2008_Cyclonopedia complicity with anonymous materials (Reza Negarestani)" status: epub-text-not-found -clipping_count: 5 -kinds: {highlight: 5} +clipping_count: 9 +kinds: {highlight: 9} citation_key: "negarestani2008" zotero_item_id: 51395 zotero_key: "JTDWDKRH" @@ -1017,11 +672,11 @@ expected_attachment_type: "epub" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 51402 - attachment_key: "P6XQUQXF" + - attachment_item_id: 73750 + attachment_key: "67J7SL6P" attachment_title: "Negarestani_2008_Cyclonopedia complicity with anonymous materials 1.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Negarestani_2008_Cyclonopedia complicity with anonymous materials.epub" + path: "/Users/ubd/Zotero/storage/67J7SL6P/Negarestani_2008_Cyclonopedia complicity with anonymous materials.epub" override_entry: clipping_title: "(fixed) Negarestani_2008_Cyclonopedia complicity with anonymous materials (Reza Negarestani)" resolution: @@ -1053,11 +708,11 @@ expected_attachment_type: "epub" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 42882 - attachment_key: "SK7YDIFX" + - attachment_item_id: 73389 + attachment_key: "5IY8GXVD" attachment_title: "Beckett_2009_Three novels.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Beckett_2009_Three novels.epub" + path: "/Users/ubd/Zotero/storage/5IY8GXVD/Beckett_2009_Three novels.epub" override_entry: clipping_title: "Beckett_2009_Three novels (Samuel Beckett)" resolution: @@ -1088,21 +743,21 @@ expected_attachment_type: "pdf-or-epub" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 42874 - attachment_key: "5PMKEQHR" + - attachment_item_id: 73477 + attachment_key: "WM3I2QU5" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" - - attachment_item_id: 42876 - attachment_key: "HUBBR88Z" + path: "/Users/ubd/Zotero/storage/WM3I2QU5/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia 1.pdf" + - attachment_item_id: 73478 + attachment_key: "QCT92KP4" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" - - attachment_item_id: 42873 - attachment_key: "WRX7QDU5" + path: "/Users/ubd/Zotero/storage/QCT92KP4/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.epub" + - attachment_item_id: 73479 + attachment_key: "YQT2MIE6" attachment_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" + path: "/Users/ubd/Zotero/storage/YQT2MIE6/Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia.pdf" override_entry: clipping_title: "Deleuze and Guattari_1983_Anti-Oedipus capitalism and schizophrenia (Gilles Deleuze, Felix Guattari)" resolution: @@ -1133,16 +788,16 @@ expected_attachment_type: "pdf-or-epub" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 46712 - attachment_key: "QPJNPMKP" + - attachment_item_id: 73694 + attachment_key: "VQP7RDYW" attachment_title: "Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.epub" - - attachment_item_id: 46714 - attachment_key: "KE7XRV35" + path: "/Users/ubd/Zotero/storage/VQP7RDYW/Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.epub" + - attachment_item_id: 73695 + attachment_key: "Y49LILX8" attachment_title: "Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.pdf" + path: "/Users/ubd/Zotero/storage/Y49LILX8/Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou.pdf" override_entry: clipping_title: "Mackenzie_2018_Resistance and the politics of truth Foucault, Deleuze, Badiou (Iain MacKenzie)" resolution: @@ -1174,11 +829,11 @@ expected_attachment_type: "epub" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 42872 - attachment_key: "8ZMLY422" + - attachment_item_id: 73429 + attachment_key: "JFZSQXP2" attachment_title: "Buchner_2004_Lenz.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Buchner_2004_Lenz.epub" + path: "/Users/ubd/Zotero/storage/JFZSQXP2/Buchner_2004_Lenz.epub" override_entry: clipping_title: "Buchner_2004_Lenz (Georg Buchner)" resolution: @@ -1208,11 +863,11 @@ expected_attachment_type: "epub" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 49241 - attachment_key: "BWN8LXZP" + - attachment_item_id: 73606 + attachment_key: "JUMLGYQP" attachment_title: "Herbert and Herbert_2020_Dune messiah.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Herbert and Herbert_2020_Dune messiah.epub" + path: "/Users/ubd/Zotero/storage/JUMLGYQP/Herbert and Herbert_2020_Dune messiah.epub" override_entry: clipping_title: "Herbert and Herbert_2020_Dune messiah (Frank Herbert)" resolution: @@ -1242,16 +897,16 @@ expected_attachment_type: "epub-preferred" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 43906 - attachment_key: "QW6E2MJL" + - attachment_item_id: 73859 + attachment_key: "96FZIZ5L" attachment_title: "Sterren et al._2011_Fundamental chess openings.epub" content_type: "application/epub+zip" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Sterren et al._2011_Fundamental chess openings.epub" - - attachment_item_id: 43908 - attachment_key: "IJ68KXNQ" + path: "/Users/ubd/Zotero/storage/96FZIZ5L/Sterren et al._2011_Fundamental chess openings.epub" + - attachment_item_id: 73860 + attachment_key: "78C9NDN7" attachment_title: "Sterren et al._2011_Fundamental chess openings.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Sterren et al._2011_Fundamental chess openings.pdf" + path: "/Users/ubd/Zotero/storage/78C9NDN7/Sterren et al._2011_Fundamental chess openings.pdf" override_entry: clipping_title: "Sterren et al._2011_Fundamental chess openings (Paul van der Sterren)" resolution: @@ -1265,13 +920,83 @@ Candidates: - Example: highlight page `None` loc `11774-11776`: The Taimanov, the Paulsen and the Scheveningen are the great main lines in this position. There are also a few smaller variations, among which one is especially notable because it +### The philosophy of Simondon_ between technology and individuation (Kirkpatrick, Graeme) + +```yaml +clipping_title: "The philosophy of Simondon_ between technology and individuation (Kirkpatrick, Graeme)" +status: epub-text-not-found +clipping_count: 1 +kinds: {highlight: 1} +citation_key: "chabot2013" +zotero_item_id: 76048 +zotero_key: "FKI3T3XT" +zotero_title: "The philosophy of Simondon: between technology and individuation" +attachment_count: 1 +expected_attachment_type: "epub" +match_score: 1.0 +match_reason: "override-citation-key:chabot2013" +attachment_choices: + - attachment_item_id: 76050 + attachment_key: "47IP4ZXW" + attachment_title: "Ebook" + content_type: "application/epub+zip" + path: "/Users/ubd/Zotero/storage/47IP4ZXW/Chabot et al. - 2013 - The philosophy of Simondon between technology and individuation.epub" +override_entry: + clipping_title: "The philosophy of Simondon_ between technology and individuation (Kirkpatrick, Graeme)" + resolution: + citation_key: "chabot2013" + attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" + attachment_item_id: null +``` + +Candidates: +- citation_key: `chabot2013`, zotero_item_id: `76048`, zotero_key: `FKI3T3XT`, attachments: `1`, score: `1.0`, title: The philosophy of Simondon: between technology and individuation + +- Example: highlight page `12` loc `130-133`: Jacques Garelli dedicated considerable space to Simondon in his book Rhythms and Worlds. In 1992, an English translation of some twenty pages from the introduction to his thesis on + ## PDF Text Not Found Status: `pdf-text-not-found` -Clippings: 26 -Unique titles: 5 +Clippings: 38 +Unique titles: 6 Action: Needs looser PDF text matching/OCR/page-offset handling. +### gilbert-simondon-on-the-mode-of-existence-of-technical-objects + +```yaml +clipping_title: "gilbert-simondon-on-the-mode-of-existence-of-technical-objects" +status: pdf-text-not-found +clipping_count: 12 +kinds: {highlight: 12} +citation_key: "simondon2017a" +zotero_item_id: 76043 +zotero_key: "A3A9WMNE" +zotero_title: "On the mode of existence of technical objects" +attachment_count: 1 +expected_attachment_type: "pdf" +match_score: 1.0 +match_reason: "override-citation-key:simondon2017a" +attachment_choices: + - attachment_item_id: 76127 + attachment_key: "R7JSW39C" + attachment_title: "PDF" + content_type: "application/pdf" + path: "/Users/ubd/Zotero/storage/R7JSW39C/gilbert-simondon-on-the-mode-of-existence-of-technical-objects.pdf" +override_entry: + clipping_title: "gilbert-simondon-on-the-mode-of-existence-of-technical-objects" + resolution: + citation_key: "simondon2017a" + attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" + attachment_item_id: null +``` + +Candidates: +- citation_key: `simondon2017a`, zotero_item_id: `76043`, zotero_key: `A3A9WMNE`, attachments: `1`, score: `1.0`, title: On the mode of existence of technical objects + +- Example: highlight page `30-30` loc `None`: by granting the technical object the only status currently valued besides that of the aesthetic object, namely that of the sacred object. This, then, gives rise to an intemperate t +- Example: highlight page `30-30` loc `None`: by granting the technical object the only status currently valued besides that of the aesthetic object, namely that of the sacred object. This, then, gives rise to an intemperate t +- Example: highlight page `30-30` loc `None`: by granting the technical object the only status currently valued besides that of the aesthetic object, namely that of the sacred object. This, then, gives rise to an intemperate t + ### (Undergraduate texts in mathematics) Sheldon Axler - Linear Algebra Done Right-Springer (1997) ```yaml @@ -1288,11 +1013,11 @@ expected_attachment_type: "pdf" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 49385 - attachment_key: "BY36XN8X" + - attachment_item_id: 73373 + attachment_key: "LP7YL6GG" attachment_title: "Axler_2024_Linear Algebra Done Right.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Axler_2024_Linear Algebra Done Right.pdf" + path: "/Users/ubd/Zotero/storage/LP7YL6GG/Axler_2024_Linear Algebra Done Right.pdf" override_entry: clipping_title: "(Undergraduate texts in mathematics) Sheldon Axler - Linear Algebra Done Right-Springer (1997)" resolution: @@ -1324,11 +1049,11 @@ expected_attachment_type: "pdf" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 40764 - attachment_key: "VXSNTSKV" + - attachment_item_id: 73424 + attachment_key: "R7QLWI74" attachment_title: "Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" + path: "/Users/ubd/Zotero/storage/R7QLWI74/Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" override_entry: clipping_title: "Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide" resolution: @@ -1360,11 +1085,11 @@ expected_attachment_type: "pdf" match_score: 0.98 match_reason: "title-exact" attachment_choices: - - attachment_item_id: 41144 - attachment_key: "5WIFUMAC" + - attachment_item_id: 73724 + attachment_key: "D4UFFYUP" attachment_title: "Melville_2006_Bartleby the scrivener.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Melville_2006_Bartleby the scrivener.pdf" + path: "/Users/ubd/Zotero/storage/D4UFFYUP/Melville_2006_Bartleby the scrivener.pdf" override_entry: clipping_title: "Bartleby, the Scrivener (Melville, Herman)" resolution: @@ -1396,11 +1121,11 @@ expected_attachment_type: "pdf" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 47609 - attachment_key: "WZCI29B3" + - attachment_item_id: 73360 + attachment_key: "SXDEZWXF" attachment_title: "Tiamat - İhsan Oktay Anar.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Tiamat - İhsan Oktay Anar.pdf" + path: "/Users/ubd/Zotero/storage/SXDEZWXF/Tiamat - İhsan Oktay Anar.pdf" override_entry: clipping_title: "Tiamat - İhsan Oktay Anar-cdeKey_8F425D6B854B4614AD13E11A3106C4D6" resolution: @@ -1430,11 +1155,11 @@ expected_attachment_type: "pdf" match_score: 1.0 match_reason: "override-citation-key:burroughs1992" attachment_choices: - - attachment_item_id: 39118 - attachment_key: "TN5ANVDD" + - attachment_item_id: 73433 + attachment_key: "J6SWSFK3" attachment_title: "Burroughs_1992_.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Burroughs_1992_.pdf" + path: "/Users/ubd/Zotero/storage/J6SWSFK3/Burroughs_1992_.pdf" override_entry: clipping_title: "WilliamS.Burroughs-Nakedlunch-GrovePress(2001) (Utku)" resolution: @@ -1451,8 +1176,8 @@ Candidates: ## PDF Rectangles Not Found Status: `pdf-rects-not-found` -Clippings: 157 -Unique titles: 4 +Clippings: 162 +Unique titles: 5 Action: Needs improved PDF rectangle recovery after text/page match. ### Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide @@ -1471,11 +1196,11 @@ expected_attachment_type: "pdf" match_score: 1.0 match_reason: "citationKey" attachment_choices: - - attachment_item_id: 40764 - attachment_key: "VXSNTSKV" + - attachment_item_id: 73424 + attachment_key: "R7QLWI74" attachment_title: "Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" + path: "/Users/ubd/Zotero/storage/R7QLWI74/Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide.pdf" override_entry: clipping_title: "Buchanan_2008_Deleuze and Guattari's Anti-Oedipus a reader's guide" resolution: @@ -1507,11 +1232,11 @@ expected_attachment_type: "pdf" match_score: 0.92 match_reason: "title-contained-in-clipping-title" attachment_choices: - - attachment_item_id: 47609 - attachment_key: "WZCI29B3" + - attachment_item_id: 73360 + attachment_key: "SXDEZWXF" attachment_title: "Tiamat - İhsan Oktay Anar.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Tiamat - İhsan Oktay Anar.pdf" + path: "/Users/ubd/Zotero/storage/SXDEZWXF/Tiamat - İhsan Oktay Anar.pdf" override_entry: clipping_title: "Tiamat - İhsan Oktay Anar-cdeKey_8F425D6B854B4614AD13E11A3106C4D6" resolution: @@ -1543,11 +1268,11 @@ expected_attachment_type: "pdf" match_score: 1.0 match_reason: "override-citation-key:burroughs1992" attachment_choices: - - attachment_item_id: 39118 - attachment_key: "TN5ANVDD" + - attachment_item_id: 73433 + attachment_key: "J6SWSFK3" attachment_title: "Burroughs_1992_.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Burroughs_1992_.pdf" + path: "/Users/ubd/Zotero/storage/J6SWSFK3/Burroughs_1992_.pdf" override_entry: clipping_title: "WilliamS.Burroughs-Nakedlunch-GrovePress(2001) (Utku)" resolution: @@ -1563,6 +1288,42 @@ Candidates: - Example: highlight page `49` loc `539-541`: A group of P.R.'s -Partially Reconditioned -- have surrounded some homosexual tourists with horrible knowing smiles showing the Nordic skull beneath in double exposure. "What do yo - Example: highlight page `67` loc `760-762`: The Guard is a sharp dresser, since he has nothing to do and saves all his pay to buy fine clothes and changes three times a day in front of an enormous magnifying mirror. He has a +### gilbert-simondon-on-the-mode-of-existence-of-technical-objects + +```yaml +clipping_title: "gilbert-simondon-on-the-mode-of-existence-of-technical-objects" +status: pdf-rects-not-found +clipping_count: 5 +kinds: {highlight: 5} +citation_key: "simondon2017a" +zotero_item_id: 76043 +zotero_key: "A3A9WMNE" +zotero_title: "On the mode of existence of technical objects" +attachment_count: 1 +expected_attachment_type: "pdf" +match_score: 1.0 +match_reason: "override-citation-key:simondon2017a" +attachment_choices: + - attachment_item_id: 76127 + attachment_key: "R7JSW39C" + attachment_title: "PDF" + content_type: "application/pdf" + path: "/Users/ubd/Zotero/storage/R7JSW39C/gilbert-simondon-on-the-mode-of-existence-of-technical-objects.pdf" +override_entry: + clipping_title: "gilbert-simondon-on-the-mode-of-existence-of-technical-objects" + resolution: + citation_key: "simondon2017a" + attachment_key: "PASTE_SELECTED_ATTACHMENT_KEY" + attachment_item_id: null +``` + +Candidates: +- citation_key: `simondon2017a`, zotero_item_id: `76043`, zotero_key: `A3A9WMNE`, attachments: `1`, score: `1.0`, title: On the mode of existence of technical objects + +- Example: highlight page `31-31` loc `None`: Automatism, and its utilization in the form of industrial organization, which one calls autom ation, possesses an economic or social signification more than a technical one +- Example: highlight page `31-31` loc `None`: Automatism, and its utilization in the form of industrial organization, which one calls autom ation, possesses an economic or social signification more than a technical one. The tr +- Example: highlight page `31-31` loc `None`: Automatism, and its utilization in the form of industrial organization, which one calls autom ation, possesses an economic or social signification more than a technical one. The tr + ### Bartleby, the Scrivener (Melville, Herman) ```yaml @@ -1579,11 +1340,11 @@ expected_attachment_type: "pdf" match_score: 0.98 match_reason: "title-exact" attachment_choices: - - attachment_item_id: 41144 - attachment_key: "5WIFUMAC" + - attachment_item_id: 73724 + attachment_key: "D4UFFYUP" attachment_title: "Melville_2006_Bartleby the scrivener.pdf" content_type: "application/pdf" - path: "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/03_Academia/ZoteroFiles/Melville_2006_Bartleby the scrivener.pdf" + path: "/Users/ubd/Zotero/storage/D4UFFYUP/Melville_2006_Bartleby the scrivener.pdf" override_entry: clipping_title: "Bartleby, the Scrivener (Melville, Herman)" resolution: diff --git a/match-overrides.json b/match-overrides.json index fcfdaa4..80f301a 100644 --- a/match-overrides.json +++ b/match-overrides.json @@ -1,6 +1,42 @@ { "format": "kindle-zotero-importer.match-overrides.v1", "overrides": [ + { + "clipping_title": "(Springer Series in Statistics) Trevor Hastie, Robert Tibshirani, Jerome Friedman - The elements of statistical learning Data mining, inference & predi", + "resolution": { + "ignore": true + }, + "review": { + "status": "ambiguous", + "clipping_count": 2 + }, + "created_at": "2026-09-16T12:39:19.713Z", + "updated_at": "2026-09-16T12:39:19.713Z" + }, + { + "clipping_title": "(Grundstudium Mathematik) Herbert Amann, Joachim Escher - Analysis 1-Birkhauser Basel (2006)", + "resolution": { + "ignore": true + }, + "review": { + "status": "ambiguous", + "clipping_count": 3 + }, + "created_at": "2026-09-16T12:39:05.426Z", + "updated_at": "2026-09-16T12:39:08.496Z" + }, + { + "clipping_title": "The Communist Hypothesis (Alain Badiou)", + "resolution": { + "citation_key": "badiou2015" + }, + "review": { + "status": "epub-text-not-found", + "clipping_count": 1 + }, + "created_at": "2026-09-16T12:20:45.512Z", + "updated_at": "2026-09-16T12:20:45.512Z" + }, { "clipping_title": "WilliamS.Burroughs-Nakedlunch-GrovePress(2001) (Utku)", "resolution": { @@ -9,13 +45,26 @@ "review": { "status": "matched", "clipping_count": 74 - } + }, + "created_at": "2026-09-16T12:19:40.753Z", + "updated_at": "2026-09-16T12:19:40.753Z" + }, + { + "clipping_title": "On the Mode of Existence of Technical Objects (Univocal)", + "resolution": { + "ignore": true + }, + "review": { + "status": "ambiguous", + "clipping_count": 16 + }, + "created_at": "2026-09-16T12:19:16.038Z", + "updated_at": "2026-09-16T12:19:16.038Z" }, { "clipping_title": "(Kapitalismus und Schizophrenie 2) Guattari, Deleuze - Tausend Plateaus, Mille Plateaux-Les Editions de Minuit (1980)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -25,8 +74,7 @@ { "clipping_title": "[Penguin Classics] Marcus Aurelius, Martin Hammond, Diskin Clay - Meditations (2006, Penguin Classics) - libgen.li", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -36,8 +84,7 @@ { "clipping_title": "APuZ_Postdemokratie.2011", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -47,8 +94,7 @@ { "clipping_title": "Arendt_2017_", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -58,8 +104,7 @@ { "clipping_title": "Chantal Mouffe - Hegemonie und radikale Demokratie.-Passagen Verlag Ges.M.B.H (2000)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -69,8 +114,7 @@ { "clipping_title": "Deleuze and Guattaris", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -80,8 +124,7 @@ { "clipping_title": "Deleuze, Bartleby; or, the Formula", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -91,8 +134,7 @@ { "clipping_title": "Deleuze, Guattari- A Thousand Plateaus - Bobo", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -102,8 +144,7 @@ { "clipping_title": "Dewey_2008_", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -113,8 +154,7 @@ { "clipping_title": "DRdZ 01 - Die Suche nach dem Auge der Welt (Robert Jordan)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -124,8 +164,7 @@ { "clipping_title": "Ernesto Laclau - On Populist Reason-Verso (2005)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -135,8 +174,7 @@ { "clipping_title": "Exhalation (Ted Chiang)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -146,8 +184,7 @@ { "clipping_title": "Fire & Blood (A Song of Ice and Fire) (George R. R. Martin)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -157,7 +194,6 @@ { "clipping_title": "Galloway_2004_Protocol", "resolution": { - "ignore": true, "citation_key": "galloway_2004" }, "review": { @@ -165,11 +201,20 @@ "clipping_count": 14 } }, + { + "clipping_title": "gilbert-simondon-on-the-mode-of-existence-of-technical-objects", + "resolution": { + "citation_key": "simondon2017a" + }, + "review": { + "status": "matched", + "clipping_count": 28 + } + }, { "clipping_title": "Gregs Tagebuch 13 - Eiskalt erwischt by Kinney Jeff, Schmidt Dietmar (z-lib.org)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -179,8 +224,7 @@ { "clipping_title": "Hardt_Negri_2017_", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -190,8 +234,7 @@ { "clipping_title": "Howaldt_Schwarz_2016_SI-DRIVE", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -201,8 +244,7 @@ { "clipping_title": "Latzer Just (2020)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -212,8 +254,7 @@ { "clipping_title": "Lem, Stanislaw - Solaris (Stanisław Lem)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -223,8 +264,7 @@ { "clipping_title": "luhmann1981", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -234,8 +274,7 @@ { "clipping_title": "manifestly_haraway_----_a_cyborg_manifesto_science_technology_and_socialist-feminism_in_the_...", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -245,8 +284,7 @@ { "clipping_title": "Marcus Aurelius (Emperor of Rome), Martin Hammond, Diskin Clay - Meditations (2006, Penguin Books) - libgen.li", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -256,8 +294,7 @@ { "clipping_title": "Martyr! -- Akbar, Kaveh -- null, null, 2024 -- RANDOM HOUSE US -- 9780593802359 -- 930ede856e2a2efb28505e3c4e99e222 -- Anna’s Archive (Kaveh Akbar)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -267,8 +304,7 @@ { "clipping_title": "mew_band03", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -278,8 +314,7 @@ { "clipping_title": "MiloradPaviç_-_ hazar_sözlügü", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -289,8 +324,7 @@ { "clipping_title": "On Populist Reason (Laclau, Ernesto)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -300,8 +334,7 @@ { "clipping_title": "paul-lafargue-the-right-to-be-lazy (Paul Lafargue)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -311,8 +344,7 @@ { "clipping_title": "Quantitative Marxism - Paul Dunne Ed_", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -322,8 +354,7 @@ { "clipping_title": "radikale demokratie comtesse, nonhoff etc", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -333,8 +364,7 @@ { "clipping_title": "Rupert präsentiert: Ein echt wildes Abenteuer (Ruperts Tagebuch) (German Edition) (Kinney, Jeff)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -344,19 +374,27 @@ { "clipping_title": "The City & the City - China Mieville", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", "clipping_count": 8 } }, + { + "clipping_title": "The philosophy of Simondon_ between technology and individuation (Kirkpatrick, Graeme)", + "resolution": { + "citation_key": "chabot2013" + }, + "review": { + "status": "epub-text-not-found", + "clipping_count": 1 + } + }, { "clipping_title": "The Republic (Plato)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -366,8 +404,7 @@ { "clipping_title": "The Satanic Verses_ A Novel (Salman Rushdie)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -377,8 +414,7 @@ { "clipping_title": "The Way of Kings (Sanderson, Brandon)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -388,8 +424,7 @@ { "clipping_title": "Your Clippings", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -399,8 +434,7 @@ { "clipping_title": "Your P2K Articles (2024-12-17) (P2K)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", @@ -410,8 +444,7 @@ { "clipping_title": "Your P2K Articles (2024-12-31) (P2K)", "resolution": { - "ignore": true, - "citation_key": "" + "ignore": true }, "review": { "status": "unmatched", diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..9b0210f --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,25 @@ +# Kindle Zotero Importer Zotero Plugin + +This is a hybrid Zotero plugin wrapper around the current Python importer. Zotero owns the user interface and writes native annotations through Zotero APIs; the Python project still performs parsing, matching, planning, EPUB positioning, and PDF positioning. + +## Use + +1. Install `kindle-zotero-importer.xpi` in Zotero through `Tools > Plugins`. +2. Open `Tools > Kindle Zotero Importer...`. +3. Select the updated Kindle `My Clippings.txt` in the manager. +4. Follow the current stage, percentage, and elapsed time in the manager. +5. Review created, existing, updated, failed, and unresolved counts when it completes. + +The plugin uses `match-overrides.json` from the project directory as the persistent source of title mappings and ignored titles. Generated JSON artifacts are still written to the project directory for audit and debugging. + +## Build XPI + +From the repository root: + +```sh +python scripts/build_plugin.py +``` + +The output is `dist/kindle-zotero-importer.xpi`. + +The plugin manifest targets Zotero `6.999` through `10.0.*`. diff --git a/plugin/bootstrap.js b/plugin/bootstrap.js new file mode 100644 index 0000000..9aa3412 --- /dev/null +++ b/plugin/bootstrap.js @@ -0,0 +1,1275 @@ +var KindleZoteroImporter = { + id: "kindle-zotero-importer@utkubilen.de", + menuItems: [], + defaultProjectDir: + "/Users/ubd/Library/Mobile Documents/iCloud~md~obsidian/Documents/rhizome/06_projects/UTI/kindle-zotero-importer", + defaultClippingsPath: + "/Users/ubd/Library/Mobile Documents/com~apple~CloudDocs/Projects/test/My Clippings.txt", + prefBranch: "extensions.kindleZoteroImporter.", + rootURI: null, + managerWindow: null, + chromeHandle: null, + + async startup(data) { + this.rootURI = data.rootURI; + const addonManagerStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService( + Ci.amIAddonManagerStartup + ); + this.chromeHandle = addonManagerStartup.registerChrome( + Services.io.newURI(this.rootURI + "manifest.json"), + [["content", "kindle-zotero-importer", ""]] + ); + await Zotero.initializationPromise; + this.addToAllWindows(); + }, + + shutdown() { + this.removeMenuItems(); + if (this.managerWindow && !this.managerWindow.closed) { + this.managerWindow.close(); + } + this.managerWindow = null; + if (this.chromeHandle) { + this.chromeHandle.destruct(); + this.chromeHandle = null; + } + }, + + onMainWindowLoad(win) { + this.addMenuItems(win); + }, + + onMainWindowUnload(_win) { + this.menuItems = []; + }, + + addMenuItems(win) { + win = win || Zotero.getMainWindow(); + if (!win || !win.document) { + return; + } + + const doc = win.document; + if (doc.getElementById("kindle-zotero-importer-manager")) { + return; + } + + const popup = doc.getElementById("menu_ToolsPopup"); + if (!popup) { + Zotero.debug("Kindle Zotero Importer: Tools menu not found"); + return; + } + + const managerItem = doc.createXULElement("menuitem"); + managerItem.setAttribute("id", "kindle-zotero-importer-manager"); + managerItem.setAttribute("label", "Kindle Zotero Importer..."); + managerItem.addEventListener("command", () => this.openManager()); + popup.appendChild(managerItem); + this.menuItems.push(managerItem); + + }, + + addToAllWindows() { + const windows = Zotero.getMainWindows ? Zotero.getMainWindows() : [Zotero.getMainWindow()]; + for (const win of windows) { + if (win && win.ZoteroPane) { + this.addMenuItems(win); + } + } + }, + + removeMenuItems() { + for (const item of this.menuItems) { + if (item && item.parentNode) { + item.parentNode.removeChild(item); + } + } + this.menuItems = []; + }, + + getPref(name, fallback) { + try { + return Services.prefs.getCharPref(this.prefBranch + name); + } catch (_error) { + return fallback; + } + }, + + setPref(name, value) { + Services.prefs.setCharPref(this.prefBranch + name, String(value)); + }, + + getProjectDir() { + return this.getPref("projectDir", this.defaultProjectDir); + }, + + getPythonPath() { + return this.getPref("pythonPath", "/opt/homebrew/bin/python3"); + }, + + getZoteroDbPath() { + return this.getPref("zoteroDbPath", "/Users/ubd/Zotero/zotero.sqlite"); + }, + + getZoteroStorageRoot() { + return this.getPref("zoteroStorageRoot", "/Users/ubd/Zotero/storage"); + }, + + getConfigPath() { + return this.getProjectDir() + "/plugin-config.json"; + }, + + artifactPath(relativePath) { + return this.getProjectDir() + "/" + relativePath; + }, + + async openArtifact(relativePath) { + const path = this.artifactPath(relativePath); + try { + await Zotero.File.getContentsAsync(path); + this.revealFile(path); + } catch (_error) { + this.alert( + "Kindle Zotero Importer", + `File not found yet:\n${path}\n\nRun Import Kindle Clippings first to generate review artifacts.` + ); + } + }, + + async getRuntimeSettings() { + const settings = { + projectDir: this.getProjectDir(), + pythonPath: this.getPythonPath(), + zoteroDbPath: this.getZoteroDbPath(), + zoteroStorageRoot: this.getZoteroStorageRoot(), + }; + try { + const text = await Zotero.File.getContentsAsync(this.getConfigPath()); + const parsed = JSON.parse(text); + for (const key of Object.keys(settings)) { + if (parsed[key]) { + settings[key] = parsed[key]; + } + } + } catch (_error) { + // The config file is optional; defaults/preferences are enough for first run. + } + return settings; + }, + + getMainWindow() { + return Zotero.getMainWindow(); + }, + + getPickerWindow() { + return Services.wm.getMostRecentWindow("navigator:browser") || this.getMainWindow(); + }, + + alert(title, message) { + Services.prompt.alert(this.getMainWindow(), title, message); + }, + + notify(message, success) { + try { + const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true }); + progressWindow.changeHeadline("Kindle Zotero Importer"); + const icon = success ? "chrome://zotero/skin/tick.png" : "chrome://zotero/skin/cross.png"; + const item = new progressWindow.ItemProgress(icon, message); + item.setProgress(100); + progressWindow.show(); + progressWindow.startCloseTimer(success ? 5000 : 10000); + return progressWindow; + } catch (_error) { + return null; + } + }, + + createProgress(message) { + try { + const progressWindow = new Zotero.ProgressWindow({ closeOnClick: false }); + progressWindow.changeHeadline("Kindle Zotero Importer"); + progressWindow.progress = new progressWindow.ItemProgress( + "chrome://zotero/skin/treesource-collection.png", + message + ); + progressWindow.progress.setProgress(0); + progressWindow.show(); + return progressWindow; + } catch (_error) { + return null; + } + }, + + promptForPath(title, message, fallback) { + const value = { value: fallback || "" }; + const ok = Services.prompt.prompt(this.getMainWindow(), title, message, value, null, {}); + return ok && value.value ? value.value : null; + }, + + async configure() { + const current = { + projectDir: this.getProjectDir(), + pythonPath: this.getPythonPath(), + zoteroDbPath: this.getZoteroDbPath(), + zoteroStorageRoot: this.getZoteroStorageRoot(), + }; + try { + const configPath = this.getConfigPath(); + await Zotero.File.putContentsAsync(configPath, JSON.stringify(current, null, 2) + "\n"); + this.alert( + "Kindle Zotero Importer Configuration", + `Configuration file written to:\n${configPath}\n\nEdit this file to change paths. The import dialog will show the active settings before running.` + ); + this.revealFile(configPath); + } catch (error) { + Zotero.logError(error); + this.alert("Kindle Zotero Importer Failed", String(error && error.stack ? error.stack : error)); + } + }, + + async openManager() { + if (this.managerWindow && !this.managerWindow.closed) { + this.managerWindow.focus(); + return; + } + const settings = await this.getRuntimeSettings(); + const data = await this.loadManagerData(); + const io = { plugin: this, settings, data }; + this.managerWindow = this.getMainWindow().openDialog( + "chrome://kindle-zotero-importer/content/manager.html", + "kindle-zotero-importer-manager-window", + "chrome,centerscreen,resizable", + io + ); + }, + + async loadManagerData() { + return { + matches: await this.readJsonArtifact("matches.json"), + generatedOverrides: await this.readJsonArtifact("match-overrides.generated.json"), + positionedPlan: await this.readJsonArtifact("import-plan.positioned.json"), + finalPlan: await this.readJsonArtifact("import-plan.final.json"), + summary: await this.readJsonArtifact("plugin-summary.json"), + persistentOverrides: await this.readJsonArtifact("match-overrides.json"), + }; + }, + + async openStaticDashboard() { + const settings = await this.getRuntimeSettings(); + const data = { + matches: await this.readJsonArtifact("matches.json"), + generatedOverrides: await this.readJsonArtifact("match-overrides.generated.json"), + positionedPlan: await this.readJsonArtifact("import-plan.positioned.json"), + finalPlan: await this.readJsonArtifact("import-plan.final.json"), + summary: await this.readJsonArtifact("plugin-summary.json"), + persistentOverrides: await this.readJsonArtifact("match-overrides.json"), + }; + const outputPath = this.artifactPath("kindle-import-manager.html"); + await Zotero.File.putContentsAsync(outputPath, this.buildManagerHtml(settings, data)); + this.openFile(outputPath); + }, + + async loadOverridesFile() { + const path = this.artifactPath("match-overrides.json"); + try { + return JSON.parse(await Zotero.File.getContentsAsync(path)); + } catch (_error) { + return { format: "kindle-zotero-importer.match-overrides.v1", overrides: [] }; + } + }, + + async saveOverride(resolution) { + if (!resolution || !resolution.clipping_title) { + throw new Error("Missing clipping title for override"); + } + const payload = await this.loadOverridesFile(); + if (!Array.isArray(payload.overrides)) { + payload.overrides = []; + } + const index = payload.overrides.findIndex( + (entry) => entry.clipping_title === resolution.clipping_title + ); + const nowIso = new Date().toISOString(); + const entry = { + clipping_title: resolution.clipping_title, + resolution: resolution.resolution, + review: resolution.review || { status: "manager", clipping_count: resolution.clipping_count || 0 }, + created_at: nowIso, + updated_at: nowIso, + }; + if (index >= 0) { + const prev = payload.overrides[index]; + entry.created_at = prev.created_at || prev.updated_at || nowIso; + entry.updated_at = nowIso; + payload.overrides[index] = entry; + } else { + payload.overrides.push(entry); + } + payload.overrides.sort((a, b) => (b.updated_at || b.created_at || "").localeCompare(a.updated_at || a.created_at || "") || a.clipping_title.localeCompare(b.clipping_title)); + await Zotero.File.putContentsAsync( + this.artifactPath("match-overrides.json"), + JSON.stringify(payload, null, 2) + "\n" + ); + return entry; + }, + + async deleteOverride(clippingTitle) { + if (!clippingTitle) { + throw new Error("Missing clipping title for delete"); + } + const payload = await this.loadOverridesFile(); + const before = payload.overrides.length; + payload.overrides = payload.overrides.filter((e) => e.clipping_title !== clippingTitle); + if (payload.overrides.length === before) { + throw new Error(`No override found for: ${clippingTitle}`); + } + await Zotero.File.putContentsAsync( + this.artifactPath("match-overrides.json"), + JSON.stringify(payload, null, 2) + "\n" + ); + return true; + }, + + async saveSettingsFromManager(newSettings) { + const toSave = { + projectDir: newSettings.projectDir || this.getProjectDir(), + pythonPath: newSettings.pythonPath || this.getPythonPath(), + zoteroDbPath: newSettings.zoteroDbPath || this.getZoteroDbPath(), + zoteroStorageRoot: newSettings.zoteroStorageRoot || this.getZoteroStorageRoot(), + }; + for (const [key, value] of Object.entries(toSave)) { + this.setPref(key, value); + } + await Zotero.File.putContentsAsync( + this.getConfigPath(), + JSON.stringify(toSave, null, 2) + "\n" + ); + return toSave; + }, + + async readJsonArtifact(relativePath) { + try { + return JSON.parse(await Zotero.File.getContentsAsync(this.artifactPath(relativePath))); + } catch (_error) { + return null; + } + }, + + escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + }, + + fileHref(path) { + return "file://" + encodeURI(path).replaceAll("#", "%23"); + }, + + artifactLink(label, relativePath) { + const path = this.artifactPath(relativePath); + return `${this.escapeHtml(label)}`; + }, + + countStatuses(items, key = "status") { + const counts = {}; + for (const item of items || []) { + const status = item[key] || "unknown"; + counts[status] = (counts[status] || 0) + 1; + } + return counts; + }, + + statusCards(counts) { + return Object.entries(counts || {}) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([status, count]) => `
${this.escapeHtml(count)}${this.escapeHtml(status)}
`) + .join("\n"); + }, + + conflictRows(data) { + const rows = []; + for (const match of data.matches?.matches || []) { + if (["matched", "ignored"].includes(match.status)) { + continue; + } + const candidates = (match.candidates || []) + .slice(0, 3) + .map((candidate) => `${candidate.citation_key || candidate.key || candidate.item_id}: ${candidate.title || ""}`) + .join("; "); + rows.push({ + type: "title-match", + status: match.status, + title: match.clipping_title, + count: match.clipping_count, + detail: candidates || "No candidates", + snippet: this.overrideSnippet(match.clipping_title, match.clipping_count, match.status, match.candidates?.[0]), + }); + } + + for (const override of data.generatedOverrides?.overrides || []) { + const candidateText = (override.candidates || []) + .slice(0, 3) + .map((candidate) => `${candidate.citation_key || candidate.key || candidate.item_id}: ${candidate.title || ""}`) + .join("; "); + rows.push({ + type: "override-suggestion", + status: override.review?.status || "review", + title: override.clipping_title, + count: override.review?.clipping_count || "", + detail: candidateText || override.notes || "Needs override decision", + snippet: JSON.stringify({ + clipping_title: override.clipping_title, + resolution: override.resolution || { ignore: false, citation_key: "" }, + review: override.review, + }, null, 2), + }); + } + + for (const item of data.positionedPlan?.items || []) { + const status = item.status || "unknown"; + if (["positioned", "ignored-title"].includes(status)) { + continue; + } + rows.push({ + type: "positioning/import-plan", + status, + title: item.clipping?.title || "(missing clipping title)", + count: 1, + detail: (item.problems || []).join("; ") || item.annotation?.text || "Needs review", + snippet: "Fix source data, attachment availability, or matching override; then rerun Import Kindle Clippings.", + }); + } + + return rows; + }, + + overrideSnippet(title, clippingCount, status, candidate) { + const resolution = candidate?.citation_key + ? { ignore: false, citation_key: candidate.citation_key } + : { ignore: true }; + return JSON.stringify({ + clipping_title: title, + resolution, + review: { + status, + clipping_count: clippingCount, + }, + }, null, 2); + }, + + groupedRows(rows) { + const groups = { + "Title Matching": [], + "Override Suggestions": [], + "Attachment / Positioning": [], + }; + for (const row of rows) { + if (row.type === "title-match") { + groups["Title Matching"].push(row); + } else if (row.type === "override-suggestion") { + groups["Override Suggestions"].push(row); + } else { + groups["Attachment / Positioning"].push(row); + } + } + return groups; + }, + + tableRows(rows, limit = 200) { + if (!rows.length) { + return 'No conflicts found in this group.'; + } + return rows.slice(0, limit).map((row) => ` + + ${this.escapeHtml(row.type)} + ${this.escapeHtml(row.status)} + ${this.escapeHtml(row.count)} + ${this.escapeHtml(row.title)} + ${this.escapeHtml(row.detail)} +
${this.escapeHtml(row.snippet || "")}
+ `).join("\n"); + }, + + conflictTables(rows) { + const groups = this.groupedRows(rows); + return Object.entries(groups).map(([label, groupRows]) => ` +
+

${this.escapeHtml(label)} ${this.escapeHtml(groupRows.length)} rows

+ + + ${this.tableRows(groupRows)} +
TypeStatusCountKindle TitleDetails / Candidate DifferencesOverride / Fix Snippet
+
`).join("\n"); + }, + + buildManagerHtml(settings, data) { + const matchCounts = this.countStatuses(data.matches?.matches || []); + const planCounts = data.positionedPlan?.status_counts || this.countStatuses(data.positionedPlan?.items || []); + const finalCount = data.finalPlan?.annotation_count || 0; + const skippedCounts = data.finalPlan?.skipped_counts || {}; + const conflicts = this.conflictRows(data); + const generatedAt = new Date().toLocaleString(); + return ` + + + + Kindle Zotero Importer Manager + + + + +

Kindle Zotero Importer Manager

+
Generated ${this.escapeHtml(generatedAt)} from ${this.escapeHtml(settings.projectDir)}
+ + + +
+ This dashboard is generated from importer artifacts. Copy override snippets into match-overrides.json for now, then rerun Import Kindle Clippings. Direct in-dashboard editing is the next step. +
+ +

Settings

+ + +

Run Import

+
+ Run from Zotero: Tools > Import Kindle Clippings.... The import now shows preflight settings, progress, final counts, existing-comment updates, existing-sort-order repairs, and any guarded delete/recreate confirmations for kindle-import annotations. +
+ +

Import Summary

+
+
${this.escapeHtml(data.summary?.counts?.clippings || "-")}clippings
+
${this.escapeHtml(data.summary?.counts?.unique_titles || "-")}unique titles
+
${this.escapeHtml(finalCount)}final annotations
+
${this.escapeHtml(conflicts.length)}conflict rows
+
+ +

Match Statuses

+
${this.statusCards(matchCounts) || '
No match data yet
'}
+ +

Plan Statuses

+
${this.statusCards(planCounts) || '
No plan data yet
'}
+ +

Final Plan Skips

+
${this.statusCards(skippedCounts) || '
No skipped final annotations
'}
+ +

Artifacts

+ + +

Conflicts and Exceptions

+

Rows include unresolved title matches, generated override suggestions, attachment problems, and positioning failures. Use search to filter titles, statuses, citekeys, and candidate text. Showing first 200 rows per group.

+ + ${this.conflictTables(conflicts)} + +`; + }, + + async importClippings() { + let progressWindow = null; + try { + const clippingsPath = await this.pickFile("Select Kindle My Clippings.txt", "*.txt"); + if (!clippingsPath) { + return; + } + + const settings = await this.getRuntimeSettings(); + const settingsMessage = [ + "Run Kindle import pipeline with these settings?", + "", + `Clippings: ${clippingsPath}`, + `Project directory: ${settings.projectDir}`, + `Python: ${settings.pythonPath}`, + `Zotero DB: ${settings.zoteroDbPath}`, + `Zotero storage: ${settings.zoteroStorageRoot}`, + `Config file: ${this.getConfigPath()}`, + "", + "This first creates/updates review files. You can choose whether to import annotations after the summary.", + ].join("\n"); + if (!Services.prompt.confirm(this.getMainWindow(), "Kindle Zotero Importer", settingsMessage)) { + return; + } + + progressWindow = this.createProgress("Starting Kindle import..."); + if (progressWindow) { + progressWindow.changeHeadline("Kindle Zotero Importer: reading clippings"); + } + const summary = await this.runPipeline(clippingsPath, settings); + if (progressWindow && progressWindow.progress) { + progressWindow.progress.setProgress(100); + progressWindow.changeHeadline("Kindle Zotero Importer: pipeline complete"); + } + const finalPlanPath = summary.outputs.final_plan; + const finalPlan = JSON.parse(await Zotero.File.getContentsAsync(finalPlanPath)); + this.notify( + `Pipeline completed. ${summary.counts.final_annotations || 0} final annotations ready.`, + true + ); + + const decision = this.confirmImport(summary); + if (decision === "review") { + this.revealFile(summary.outputs.mismatch_review); + return; + } + if (decision !== "import") { + return; + } + + if (progressWindow) { + progressWindow.changeHeadline("Kindle Zotero Importer: writing annotations"); + } + const results = await this.writeAnnotations(finalPlan, false); + this.notify( + `Import finished. Created ${results.created}; skipped ${results.skippedExisting}; updated order ${results.updatedSortIndex}; recreated ${results.recreatedForSortIndex}; failed ${results.failed.length}.`, + results.failed.length === 0 + ); + this.alert("Kindle Zotero Importer", this.formatWriterResults(results)); + } catch (error) { + Zotero.logError(error); + this.notify("Import failed. See the error dialog for details.", false); + this.alert("Kindle Zotero Importer Failed", String(error && error.stack ? error.stack : error)); + } finally { + if (progressWindow && progressWindow.startCloseTimer) { + progressWindow.startCloseTimer(1000); + } + } + }, + + async pickFile(title, filter) { + try { + const pickerModule = ChromeUtils.importESModule( + "chrome://zotero/content/modules/filePicker.mjs" + ); + const FilePicker = pickerModule.FilePicker; + if (typeof FilePicker !== "function") { + throw new Error(`Zotero FilePicker module did not export a constructor: ${Object.keys(pickerModule)}`); + } + const fp = new FilePicker(); + fp.init(this.getPickerWindow(), title, fp.modeOpen); + if (filter) { + fp.appendFilter("Kindle clippings", filter); + } + fp.appendFilters(fp.filterAll); + + const result = await fp.show(); + if (result !== fp.returnOK) { + return null; + } + return fp.file; + } catch (error) { + Zotero.logError(error); + return this.promptForPath( + "Kindle Zotero Importer", + "File picker failed. Enter the full path to My Clippings.txt:", + this.defaultClippingsPath + ); + } + }, + + async pickDirectory(title) { + try { + const pickerModule = ChromeUtils.importESModule( + "chrome://zotero/content/modules/filePicker.mjs" + ); + const FilePicker = pickerModule.FilePicker; + if (typeof FilePicker !== "function") { + throw new Error(`Zotero FilePicker module did not export a constructor: ${Object.keys(pickerModule)}`); + } + const fp = new FilePicker(); + fp.init(this.getPickerWindow(), title, fp.modeGetFolder); + + const result = await fp.show(); + if (result !== fp.returnOK) { + return null; + } + return fp.file; + } catch (error) { + Zotero.logError(error); + return this.promptForPath( + "Kindle Zotero Importer", + "Folder picker failed. Enter the full project directory path:", + this.defaultProjectDir + ); + } + }, + + async runPipeline(clippingsPath, settings, isFull = false) { + const python = settings.pythonPath; + const projectDir = settings.projectDir; + const runner = projectDir + "/plugin_runner.py"; + const summaryOutput = projectDir + "/plugin-summary.json"; + const progressOutput = projectDir + "/plugin-progress.json"; + const args = [ + runner, + "run", + clippingsPath, + "--workdir", + projectDir, + "--db", + settings.zoteroDbPath, + "--storage-root", + settings.zoteroStorageRoot, + "--overrides", + "match-overrides.json", + "--summary-output", + summaryOutput, + "--progress-output", + progressOutput, + "--pretty", + ]; + if (isFull) { + args.push("--full"); + } + + await this.exec(python, args); + return JSON.parse(await Zotero.File.getContentsAsync(summaryOutput)); + }, + + async runManagedImport(manager) { + const clippingsPath = await this.pickFile("Select Kindle My Clippings.txt", "*.txt"); + if (!clippingsPath) { + return; + } + const isFull = manager.getFullReimport ? manager.getFullReimport() : false; + await this.runManagedImportWithPath(manager, clippingsPath, isFull); + }, + + async runManagedImportWithPath(manager, clippingsPath, isFullOverride) { + try { + if (!clippingsPath) { + throw new Error("No clippings file selected"); + } + const settings = await this.getRuntimeSettings(); + const progressPath = settings.projectDir + "/plugin-progress.json"; + // Determine full vs incremental from explicit arg or manager checkbox + const isFull = typeof isFullOverride === "boolean" ? isFullOverride : (manager.getFullReimport ? manager.getFullReimport() : false); + manager.beginRun(clippingsPath); + try { + await Zotero.File.removeIfExists(progressPath); + } catch (_error) {} + + let polling = true; + const poll = async () => { + while (polling) { + try { + const progress = JSON.parse(await Zotero.File.getContentsAsync(progressPath)); + manager.updateProgress(progress.percent, progress.stage, progress.detail); + } catch (_error) {} + await Zotero.Promise.delay(500); + } + }; + const pollPromise = poll(); + let summary; + try { + summary = await this.runPipeline(clippingsPath, settings, isFull); + } finally { + polling = false; + await pollPromise; + } + + const finalPlan = JSON.parse( + await Zotero.File.getContentsAsync(summary.outputs.final_plan) + ); + manager.updateProgress(96, "Writing annotations", "Saving annotations in Zotero"); + const results = await this.writeAnnotations(finalPlan, false, (completed, total) => { + const percent = total ? 96 + Math.floor((completed / total) * 4) : 100; + manager.updateProgress(percent, "Writing annotations", `${completed} of ${total} checked`); + }); + const data = await this.loadManagerData(); + manager.completeRun(summary, results, data); + } catch (error) { + Zotero.logError(error); + manager.failRun(String(error && error.stack ? error.stack : error)); + } + }, + + async exec(command, args) { + if (!Zotero.Utilities || !Zotero.Utilities.Internal || !Zotero.Utilities.Internal.exec) { + throw new Error("Zotero.Utilities.Internal.exec is unavailable in this Zotero build"); + } + return Zotero.Utilities.Internal.exec(command, args); + }, + + confirmImport(summary) { + const counts = summary.counts || {}; + const planStatuses = counts.plan_statuses || {}; + const matchStatuses = counts.match_statuses || {}; + const unresolved = + (matchStatuses.unmatched || 0) + + (matchStatuses.ambiguous || 0) + + (planStatuses["matched-title-no-attachment"] || 0) + + (planStatuses["matched-title-ambiguous-attachment"] || 0); + const positioned = planStatuses.positioned || 0; + + const message = [ + "Pipeline completed.", + "", + `Clippings: ${counts.clippings || 0}`, + `Unique Kindle titles: ${counts.unique_titles || 0}`, + `Positioned annotations ready: ${positioned}`, + `Final annotations to check/import: ${counts.final_annotations || 0}`, + `Unresolved or ambiguous cases: ${unresolved}`, + "", + "Import positioned annotations now, open the mismatch review, or cancel?", + ].join("\n"); + + const choice = Services.prompt.confirmEx( + this.getMainWindow(), + "Kindle Zotero Importer", + message, + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_2 * Services.prompt.BUTTON_TITLE_CANCEL, + "Import", + "Open Review", + null, + null, + {} + ); + + if (choice === 0) { + return "import"; + } + if (choice === 1) { + return "review"; + } + return "cancel"; + }, + + normalizePosition(position) { + if (!position) { + return ""; + } + if (typeof position === "string") { + try { + return JSON.stringify(JSON.parse(position)); + } catch (_error) { + return position; + } + } + return JSON.stringify(position); + }, + + annotationFingerprint(annotation) { + return `${annotation.text || ""}\u0000${this.normalizePosition(annotation.position)}`; + }, + + existingAnnotationFingerprint(annotation) { + return `${annotation.annotationText || ""}\u0000${this.normalizePosition(annotation.annotationPosition)}`; + }, + + mergeComments(existingComment, newComment) { + const seen = new Set(); + const merged = []; + for (const comment of [existingComment, newComment]) { + for (const part of String(comment || "").split(/\n\s*\n/)) { + const cleaned = part.trim(); + if (!cleaned || seen.has(cleaned)) { + continue; + } + seen.add(cleaned); + merged.push(cleaned); + } + } + return merged.join("\n\n"); + }, + + existingSortIndex(annotation) { + return annotation.annotationSortIndex || annotation._annotationSortIndex || annotation.sortIndex || ""; + }, + + existingTags(annotation) { + try { + if (annotation.getTags) { + return annotation.getTags().map((tag) => tag.tag || tag.name || String(tag)); + } + } catch (_error) { + // Fall through to property checks below. + } + const tags = annotation.annotationTags || annotation.tags || []; + if (!Array.isArray(tags)) { + return []; + } + return tags.map((tag) => tag.tag || tag.name || String(tag)); + }, + + hasKindleImportTag(annotation) { + return this.existingTags(annotation).includes("kindle-import"); + }, + + confirmRecreateForSortIndex(entry, existingAnnotation, oldSortIndex, newSortIndex) { + const tags = this.existingTags(existingAnnotation).join(", ") || "(none)"; + const message = [ + "Zotero could not update an existing annotation's sort order in place.", + "", + "The plugin can delete and recreate this annotation to fix ordering, but only because it has the kindle-import tag.", + "", + `Title: ${entry.clipping_title}`, + `Attachment item: ${entry.attachment_item_id}`, + `Current sortIndex: ${oldSortIndex || "(empty)"}`, + `New sortIndex: ${newSortIndex}`, + `Tags: ${tags}`, + "", + "Text, comment, color, tags, and position will be preserved from the import plan.", + "", + "Delete and recreate this kindle-import annotation?", + ].join("\n"); + return Services.prompt.confirm( + this.getMainWindow(), + "Kindle Zotero Importer: Recreate Annotation?", + message + ); + }, + + async saveExistingAnnotation(existingAnnotation) { + if (existingAnnotation.saveTx) { + await existingAnnotation.saveTx(); + return; + } + if (existingAnnotation.save) { + await existingAnnotation.save(); + return; + } + throw new Error("Existing annotation cannot be saved by this Zotero build"); + }, + + async writeAnnotations(plan, dryRun, onProgress) { + if (plan.format !== "kindle-zotero-importer.zotero-writer-plan.v1") { + throw new Error(`Unsupported plan format: ${plan.format}`); + } + + const results = { + dryRun, + total: plan.annotations.length, + created: 0, + skippedExisting: 0, + updatedComments: 0, + updatedSortIndex: 0, + recreatedForSortIndex: 0, + blockedRecreateMissingTag: 0, + recreateDeclined: 0, + removedDuplicates: 0, + deletedForIncremental: 0, + failed: [], + }; + // Incremental deletions: remove annotations whose clipping_id is no longer present + if (!dryRun && plan.deletions && plan.deletions.length) { + const deletionIds = new Set(plan.deletions); + try { + // Find all annotations with kindle-id tag + const allAnnotations = []; + // Collect via attachments to be safe + const allItems = Zotero.Items.getAll ? await Zotero.Items.getAll() : []; + for (const item of allItems) { + if (item.isAnnotation && item.isAnnotation()) { + allAnnotations.push(item); + } else if (item.getAnnotations) { + const anns = item.getAnnotations(); + for (const a of anns) allAnnotations.push(a); + } + } + // Deduplicate + const seen = new Set(); + for (const ann of allAnnotations) { + if (!ann || seen.has(ann.id)) continue; + seen.add(ann.id); + const tags = this.existingTags(ann); + for (const tag of tags) { + if (tag.startsWith("kindle-id:")) { + const id = tag.substring("kindle-id:".length); + if (deletionIds.has(id)) { + await ann.eraseTx(); + results.deletedForIncremental += 1; + break; + } + } + } + } + } catch (e) { + Zotero.logError(e); + } + } + const existingByAttachment = new Map(); + + const getExistingAnnotations = async (attachment) => { + if (!existingByAttachment.has(attachment.id)) { + const annotations = attachment.getAnnotations ? attachment.getAnnotations() : []; + const byFingerprint = new Map(); + + for (const annotation of annotations) { + const fingerprint = this.existingAnnotationFingerprint(annotation); + const kept = byFingerprint.get(fingerprint); + if (!kept) { + byFingerprint.set(fingerprint, annotation); + continue; + } + + const keptHasComment = Boolean(kept.annotationComment); + const currentHasComment = Boolean(annotation.annotationComment); + const duplicate = keptHasComment || !currentHasComment ? annotation : kept; + const replacement = duplicate === annotation ? kept : annotation; + + byFingerprint.set(fingerprint, replacement); + if (!dryRun) { + await duplicate.eraseTx(); + } + results.removedDuplicates += 1; + } + + existingByAttachment.set(attachment.id, byFingerprint); + } + return existingByAttachment.get(attachment.id); + }; + + let completed = 0; + for (const entry of plan.annotations) { + try { + const attachment = Zotero.Items.get(entry.attachment_item_id); + if (!attachment) { + throw new Error(`Attachment not found: ${entry.attachment_item_id}`); + } + + const annotation = { + key: Zotero.DataObjectUtilities.generateKey(), + ...entry.annotation, + }; + if (!annotation.sortIndex) { + delete annotation.sortIndex; + } + + const existingAnnotations = await getExistingAnnotations(attachment); + const fingerprint = this.annotationFingerprint(annotation); + const existingAnnotation = existingAnnotations.get(fingerprint); + if (existingAnnotation) { + const mergedComment = this.mergeComments( + existingAnnotation.annotationComment, + annotation.comment + ); + let changed = false; + if (mergedComment && existingAnnotation.annotationComment !== mergedComment) { + existingAnnotation.annotationComment = mergedComment; + changed = true; + results.updatedComments += 1; + } + + const oldSortIndex = this.existingSortIndex(existingAnnotation); + const newSortIndex = annotation.sortIndex || ""; + const sortIndexChanged = Boolean(newSortIndex) && oldSortIndex !== newSortIndex; + if (sortIndexChanged) { + existingAnnotation.annotationSortIndex = newSortIndex; + changed = true; + } + + if (changed && !dryRun) { + try { + await this.saveExistingAnnotation(existingAnnotation); + if (sortIndexChanged) { + results.updatedSortIndex += 1; + } + } catch (error) { + if (!sortIndexChanged) { + throw error; + } + if (!this.hasKindleImportTag(existingAnnotation)) { + results.blockedRecreateMissingTag += 1; + throw new Error( + `Could not update sortIndex and existing annotation is not tagged kindle-import: ${error}` + ); + } + if (!this.confirmRecreateForSortIndex(entry, existingAnnotation, oldSortIndex, newSortIndex)) { + results.recreateDeclined += 1; + results.skippedExisting += 1; + continue; + } + await existingAnnotation.eraseTx(); + await Zotero.Annotations.saveFromJSON(attachment, annotation); + existingAnnotations.set(fingerprint, { + annotationComment: annotation.comment || "", + annotationPosition: annotation.position, + annotationSortIndex: annotation.sortIndex || "", + annotationTags: annotation.tags || [], + annotationText: annotation.text || "", + }); + results.recreatedForSortIndex += 1; + results.skippedExisting += 1; + continue; + } + } else if (sortIndexChanged && dryRun) { + results.updatedSortIndex += 1; + } + results.skippedExisting += 1; + continue; + } + + if (!dryRun) { + await Zotero.Annotations.saveFromJSON(attachment, annotation); + } + existingAnnotations.set(fingerprint, { + annotationComment: annotation.comment || "", + annotationPosition: annotation.position, + annotationSortIndex: annotation.sortIndex || "", + annotationTags: annotation.tags || [], + annotationText: annotation.text || "", + }); + results.created += 1; + } catch (error) { + results.failed.push({ + clipping_id: entry.clipping_id, + attachment_item_id: entry.attachment_item_id, + message: String(error), + }); + } finally { + completed += 1; + if (onProgress) { + onProgress(completed, plan.annotations.length); + } + } + } + + return results; + }, + + formatWriterResults(results) { + const lines = [ + "Import finished.", + "", + `Annotations in final plan: ${results.total}`, + `Created: ${results.created}`, + `Skipped existing: ${results.skippedExisting}`, + `Updated comments: ${results.updatedComments}`, + `Updated sort order: ${results.updatedSortIndex}`, + `Recreated for sort order: ${results.recreatedForSortIndex}`, + `Recreate blocked (missing kindle-import tag): ${results.blockedRecreateMissingTag}`, + `Recreate declined: ${results.recreateDeclined}`, + `Removed exact duplicates: ${results.removedDuplicates}`, + `Failed: ${results.failed.length}`, + ]; + if (results.failed.length) { + lines.push("", "First failures:"); + for (const failure of results.failed.slice(0, 5)) { + lines.push(`- ${failure.clipping_id}: ${failure.message}`); + } + } + return lines.join("\n"); + }, + + revealFile(path) { + try { + if (Zotero.File && Zotero.File.reveal) { + Zotero.File.reveal(path); + return; + } + const file = Components.classes["@mozilla.org/file/local;1"].createInstance( + Components.interfaces.nsIFile + ); + file.initWithPath(path); + file.reveal(); + } catch (error) { + this.alert("Kindle Zotero Importer", `Review file written to:\n${path}\n\n${error}`); + } + }, + + openFile(path) { + try { + const file = Components.classes["@mozilla.org/file/local;1"].createInstance( + Components.interfaces.nsIFile + ); + file.initWithPath(path); + if (file.exists() && file.isFile()) { + file.launch(); + return; + } + this.revealFile(path); + } catch (error) { + Zotero.logError(error); + this.revealFile(path); + } + }, +}; + +function install(_data, _reason) {} + +function uninstall(_data, _reason) {} + +async function startup(data, _reason) { + try { + await KindleZoteroImporter.startup(data); + } catch (error) { + Zotero.logError(error); + } +} + +function shutdown(_data, _reason) { + try { + KindleZoteroImporter.shutdown(); + } catch (error) { + Zotero.logError(error); + } +} + +function onMainWindowLoad({ window }) { + try { + KindleZoteroImporter.onMainWindowLoad(window); + } catch (error) { + Zotero.logError(error); + } +} + +function onMainWindowUnload({ window }) { + try { + KindleZoteroImporter.onMainWindowUnload(window); + } catch (error) { + Zotero.logError(error); + } +} diff --git a/plugin/manager.html b/plugin/manager.html new file mode 100644 index 0000000..6e767f6 --- /dev/null +++ b/plugin/manager.html @@ -0,0 +1,1199 @@ + + + + + Kindle Zotero Importer + + + + +
Loading Kindle Zotero Importer...
+ + diff --git a/plugin/manifest.json b/plugin/manifest.json new file mode 100644 index 0000000..92ba78d --- /dev/null +++ b/plugin/manifest.json @@ -0,0 +1,16 @@ +{ + "manifest_version": 2, + "name": "Kindle Zotero Importer", + "version": "0.6.4", + "description": "Import Kindle My Clippings.txt highlights and notes into Zotero as native annotations.", + "homepage_url": "https://github.com/UtkuBilenDemir/kindle-zotero_importer", + "author": "Utku Bilen Demir", + "applications": { + "zotero": { + "id": "kindlezoteroimporter@utkubilen.de", + "update_url": "https://github.com/UtkuBilenDemir/kindle-zotero_importer/releases/latest/download/updates.json", + "strict_min_version": "6.999", + "strict_max_version": "10.0.*" + } + } +} diff --git a/plugin/prefs.js b/plugin/prefs.js new file mode 100644 index 0000000..ddad9fd --- /dev/null +++ b/plugin/prefs.js @@ -0,0 +1,4 @@ +pref("extensions.kindleZoteroImporter.projectDir", "/Users/ubd/Library/Mobile Documents/iCloud~md~obsidian/Documents/rhizome/06_projects/UTI/kindle-zotero-importer"); +pref("extensions.kindleZoteroImporter.pythonPath", "/opt/homebrew/bin/python3"); +pref("extensions.kindleZoteroImporter.zoteroDbPath", "/Users/ubd/Zotero/zotero.sqlite"); +pref("extensions.kindleZoteroImporter.zoteroStorageRoot", "/Users/ubd/Zotero/storage"); diff --git a/plugin_runner.py b/plugin_runner.py new file mode 100644 index 0000000..8295962 --- /dev/null +++ b/plugin_runner.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +SRC = ROOT / "src" + +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) + +from kindle_zotero_importer.cli import main # noqa: E402 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_canary_plugin.py b/scripts/build_canary_plugin.py new file mode 100644 index 0000000..c7e4378 --- /dev/null +++ b/scripts/build_canary_plugin.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import json +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DIST_DIR = ROOT / "dist" +OUTPUT = DIST_DIR / "kindle-zotero-importer-canary.xpi" + + +def main() -> int: + DIST_DIR.mkdir(exist_ok=True) + if OUTPUT.exists(): + OUTPUT.unlink() + + manifest = { + "manifest_version": 2, + "name": "Kindle Zotero Importer Canary", + "version": "0.1.0", + "description": "Minimal install test for Kindle Zotero Importer.", + "homepage_url": "https://github.com/UtkuBilenDemir/kindle-zotero_importer", + "author": "Utku Bilen Demir", + "applications": { + "zotero": { + "id": "kindlezoteroimportercanary@utkubilen.de", + "update_url": "https://github.com/UtkuBilenDemir/kindle-zotero_importer/releases/latest/download/updates-canary.json", + "strict_min_version": "6.999", + "strict_max_version": "9.*", + } + }, + } + bootstrap = """ +function install(data, reason) {} +function uninstall(data, reason) {} +async function startup(data, reason) { + await Zotero.initializationPromise; + Zotero.debug('Kindle Zotero Importer Canary started'); +} +function shutdown(data, reason) {} +""".lstrip() + + prefs = 'pref("extensions.kindleZoteroImporterCanary.enabled", true);\n' + + with zipfile.ZipFile(OUTPUT, "w", zipfile.ZIP_DEFLATED) as archive: + for name, content in [ + ("bootstrap.js", bootstrap), + ("manifest.json", json.dumps(manifest, indent=2) + "\n"), + ("prefs.js", prefs), + ]: + info = zipfile.ZipInfo(name) + info.create_system = 3 + info.external_attr = 0o644 << 16 + archive.writestr(info, content, zipfile.ZIP_DEFLATED) + + print(f"Built {OUTPUT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_plugin.py b/scripts/build_plugin.py new file mode 100644 index 0000000..3815701 --- /dev/null +++ b/scripts/build_plugin.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN_DIR = ROOT / "plugin" +DIST_DIR = ROOT / "dist" +OUTPUT = DIST_DIR / "kindle-zotero-importer.xpi" + + +def main() -> int: + DIST_DIR.mkdir(exist_ok=True) + if OUTPUT.exists(): + OUTPUT.unlink() + + manifest_path = PLUGIN_DIR / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + zotero_app = manifest.get("applications", {}).get("zotero", {}) + required_manifest_fields = [ + "manifest_version", + "name", + "version", + "description", + ] + missing_manifest_fields = [ + field for field in required_manifest_fields if not manifest.get(field) + ] + missing_zotero_fields = [ + field + for field in ["id", "strict_min_version", "strict_max_version"] + if not zotero_app.get(field) + ] + missing_files = [ + str(path.relative_to(PLUGIN_DIR)) + for path in [PLUGIN_DIR / "manifest.json", PLUGIN_DIR / "bootstrap.js"] + if not path.exists() + ] + if missing_manifest_fields or missing_zotero_fields or missing_files: + problems = [] + if missing_manifest_fields: + problems.append(f"manifest fields: {', '.join(missing_manifest_fields)}") + if missing_zotero_fields: + problems.append( + f"applications.zotero fields: {', '.join(missing_zotero_fields)}" + ) + if missing_files: + problems.append(f"files: {', '.join(missing_files)}") + raise SystemExit("Invalid plugin package; missing " + "; ".join(problems)) + + with zipfile.ZipFile(OUTPUT, "w", zipfile.ZIP_DEFLATED) as archive: + for path in PLUGIN_DIR.rglob("*"): + if path.is_file(): + archive_name = str(path.relative_to(PLUGIN_DIR)) + info = zipfile.ZipInfo(archive_name) + info.create_system = 3 + info.external_attr = 0o644 << 16 + archive.writestr(info, path.read_bytes(), zipfile.ZIP_DEFLATED) + + with zipfile.ZipFile(OUTPUT) as archive: + names = set(archive.namelist()) + if "manifest.json" not in names or "bootstrap.js" not in names: + raise SystemExit( + "Invalid XPI: manifest.json and bootstrap.js must be at archive root" + ) + + print(f"Built {OUTPUT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/kindle_zotero_importer/cli.py b/src/kindle_zotero_importer/cli.py index 1ad9e8f..01245a7 100644 --- a/src/kindle_zotero_importer/cli.py +++ b/src/kindle_zotero_importer/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import json import sys +from pathlib import Path from .clippings import clippings_to_jsonable, load_clippings from .epub_position import add_epub_positions @@ -141,6 +142,45 @@ def main(argv: list[str] | None = None) -> int: "--output", "-o", help="Write Markdown to this file instead of stdout" ) + run_parser = subparsers.add_parser( + "run", help="Run the full importer pipeline for a Kindle My Clippings.txt file" + ) + run_parser.add_argument("clippings_file", help="Path to Kindle My Clippings.txt") + run_parser.add_argument( + "--workdir", + default=".", + help="Directory for generated importer artifacts", + ) + run_parser.add_argument( + "--db", default="/Users/ubd/Zotero/zotero.sqlite", help="Path to zotero.sqlite" + ) + run_parser.add_argument( + "--storage-root", + default="/Users/ubd/Zotero/storage", + help="Path to Zotero storage directory for storage: attachments", + ) + run_parser.add_argument( + "--overrides", + default="match-overrides.json", + help="Path to reusable match overrides JSON, relative to workdir unless absolute", + ) + run_parser.add_argument( + "--summary-output", + help="Write pipeline summary JSON to this path instead of stdout", + ) + run_parser.add_argument( + "--progress-output", + help="Write machine-readable pipeline progress JSON to this path", + ) + run_parser.add_argument( + "--pretty", action="store_true", help="Pretty-print JSON artifacts" + ) + run_parser.add_argument( + "--full", + action="store_true", + help="Force full re-import from scratch, ignoring incremental state (processes all clippings)", + ) + args = parser.parse_args(argv) if args.command == "parse": @@ -275,6 +315,236 @@ def main(argv: list[str] | None = None) -> int: sys.stdout.write(text) return 0 + if args.command == "run": + workdir = Path(args.workdir).expanduser().resolve() + workdir.mkdir(parents=True, exist_ok=True) + + progress_path = ( + Path(args.progress_output).expanduser().resolve() + if args.progress_output + else None + ) + + def report_progress(percent: int, stage: str, detail: str) -> None: + if not progress_path: + return + temporary_path = progress_path.with_suffix(progress_path.suffix + ".tmp") + temporary_path.write_text( + json.dumps( + {"percent": percent, "stage": stage, "detail": detail}, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + temporary_path.replace(progress_path) + + def artifact(name: str) -> Path: + return workdir / name + + overrides_path = Path(args.overrides).expanduser() + if not overrides_path.is_absolute(): + overrides_path = workdir / overrides_path + + report_progress(2, "Reading clippings", "Parsing My Clippings.txt") + new_clippings_list = load_clippings(args.clippings_file) + new_clippings = clippings_to_jsonable(new_clippings_list) + new_ids = {c["id"] for c in new_clippings.get("clippings", [])} + + # Incremental state: previous clippings and previous integrated ids + is_incremental = not args.full + prev_clippings_ids: set[str] = set() + prev_integrated_ids: set[str] = set() + prev_final_plan = None + if is_incremental: + prev_clippings_path = artifact("clippings.json") + prev_final_path = artifact("import-plan.final.json") + if prev_clippings_path.exists(): + try: + prev_clippings_ids = {c["id"] for c in load_json(str(prev_clippings_path)).get("clippings", [])} + except Exception: + prev_clippings_ids = set() + if prev_final_path.exists(): + try: + prev_final_plan = load_json(str(prev_final_path)) + prev_integrated_ids = {a.get("clipping_id") for a in prev_final_plan.get("annotations", []) if a.get("clipping_id")} + except Exception: + prev_final_plan = None + prev_integrated_ids = set() + # If no previous state, fall back to full + if not prev_clippings_ids and not prev_integrated_ids: + is_incremental = False + + if is_incremental: + # New or previously not integrated (skipped) -> need to (re)process + to_process_ids = set(new_ids - prev_integrated_ids) + # Deletions: previously integrated but now absent (removed or changed text) + deletions_ids = sorted(prev_integrated_ids - new_ids) + # Also re-queue any previously integrated clippings whose title now has an override + # (mapping may have changed, need to move annotation). We need overrides dict for this. + # Load overrides early for this check (already loaded later, but do here for incremental decision) + # We will reload overrides here if not yet loaded + try: + _ov_for_inc = load_overrides(load_json(str(overrides_path))) if overrides_path.exists() else {} + except Exception: + _ov_for_inc = {} + for c in new_clippings_list: + if c.id in prev_integrated_ids and c.title in _ov_for_inc: + to_process_ids.add(c.id) + # Also handle deleted overrides: if a previously integrated title's override was deleted, + # its annotations should be deleted. Detect via previous final plan's titles vs current overrides. + # For incremental, we don't have previous overrides, so we handle deletions via the main deletions list + # (IDs no longer present). For deleted overrides where ID still present but now unmatched, + # the match report will make them not positioned, and they will be in to_process but then skipped; + # we need to ensure they are deleted. We do this by checking if a previously integrated clipping's + # title is now not in overrides and would be unmatched -> treat as to_delete. + # Simplify: any prev_integrated clipping whose title is not in current new match's "matched" will be + # handled as not in to_process_ids? Actually it is still in new_ids, but we re-queued it above only if title in overrides. + # For now, rely on full re-import for deleted-override cleanup. + # Build filtered clippings for this delta run + filtered_list = [c for c in new_clippings_list if c.id in to_process_ids] + # If nothing to do and no deletions, still need to report but skip expensive steps + if not filtered_list and not deletions_ids: + report_progress(15, "Matching titles", "No new highlights — incremental skip") + # Still need to generate artifacts for consistency (empty delta) + zotero_index = build_zotero_index(args.db, args.storage_root) + overrides = None + if overrides_path.exists(): + overrides = load_overrides(load_json(str(overrides_path))) + clippings = new_clippings + matches = build_match_report(clippings, zotero_index, overrides) + override_skeleton = generate_override_skeleton(matches) + plan = {"format": "kindle-zotero-importer.import-plan.v1", "items": []} + positioned_plan = {"format": plan["format"], "items": []} + final_plan = {"format": FINAL_FORMAT, "source_format": positioned_plan.get("format"), "annotation_count": 0, "skipped_counts": {}, "annotations": [], "deletions": [], "is_incremental": True, "incremental_stats": {"new_clippings": len(new_ids), "prev_integrated": len(prev_integrated_ids), "to_process": 0, "deletions": 0}} + mismatch_review = build_mismatch_review(positioned_plan, matches) + # Will write artifacts below with empty delta + else: + report_progress(8, "Indexing Zotero", "Reading library items and attachments (incremental)") + zotero_index = build_zotero_index(args.db, args.storage_root) + overrides = None + if overrides_path.exists(): + overrides = load_overrides(load_json(str(overrides_path))) + # Only the delta clippings go through matching/positioning + delta_clippings = clippings_to_jsonable(filtered_list) + report_progress(15, "Matching titles", f"Matching {len(filtered_list)} new/pending highlights (incremental)") + matches = build_match_report(delta_clippings, zotero_index, overrides) + override_skeleton = generate_override_skeleton(matches) + # For artifact completeness, also build full matches for summary? Use delta matches for now + # But we need full matches for review? Keep delta + report_progress(22, "Building plan", "Selecting attachments (incremental)") + plan = build_import_plan(delta_clippings, zotero_index, matches) + report_progress(30, "Positioning EPUB highlights", "Locating highlights in EPUB files (incremental)") + epub_plan = add_epub_positions(plan) + report_progress(55, "Positioning PDF highlights", "Locating highlights in PDF files (incremental)") + positioned_plan = add_pdf_positions(epub_plan) + report_progress(90, "Finalizing", "Preparing Zotero annotations and mismatch report") + final_plan = build_final_writer_plan(positioned_plan) + # Attach deletions for writer + final_plan["deletions"] = deletions_ids + final_plan["is_incremental"] = True + final_plan["incremental_stats"] = { + "new_clippings": len(new_ids), + "prev_integrated": len(prev_integrated_ids), + "to_process": len(to_process_ids), + "deletions": len(deletions_ids), + } + mismatch_review = build_mismatch_review(positioned_plan, matches) + # For outputs, clippings should be the full new set (for next diff), but plan artifacts are delta + clippings = new_clippings + else: + # Full re-import + report_progress(8, "Indexing Zotero", "Reading library items and attachments") + zotero_index = build_zotero_index(args.db, args.storage_root) + overrides = None + if overrides_path.exists(): + overrides = load_overrides(load_json(str(overrides_path))) + clippings = new_clippings + report_progress(15, "Matching titles", "Matching Kindle titles to Zotero items") + matches = build_match_report(clippings, zotero_index, overrides) + override_skeleton = generate_override_skeleton(matches) + report_progress(22, "Building plan", "Selecting attachments") + plan = build_import_plan(clippings, zotero_index, matches) + report_progress(30, "Positioning EPUB highlights", "Locating highlights in EPUB files") + epub_plan = add_epub_positions(plan) + report_progress(55, "Positioning PDF highlights", "Locating highlights in PDF files; this is usually the longest stage") + positioned_plan = add_pdf_positions(epub_plan) + report_progress(90, "Finalizing", "Preparing Zotero annotations and mismatch report") + final_plan = build_final_writer_plan(positioned_plan) + final_plan["deletions"] = [] + final_plan["is_incremental"] = False + mismatch_review = build_mismatch_review(positioned_plan, matches) + + indent = 2 if args.pretty else None + outputs = { + "clippings": artifact("clippings.json"), + "zotero_index": artifact("zotero-index.json"), + "matches": artifact("matches.json"), + "generated_overrides": artifact("match-overrides.generated.json"), + "import_plan": artifact("import-plan.json"), + "epub_plan": artifact("import-plan.epub.json"), + "positioned_plan": artifact("import-plan.positioned.json"), + "final_plan": artifact("import-plan.final.json"), + "mismatch_review": workdir / "docs" / "mismatch-review.md", + } + outputs["mismatch_review"].parent.mkdir(parents=True, exist_ok=True) + + report_progress(95, "Saving results", "Writing import artifacts") + for key, payload in [ + ("clippings", clippings), + ("zotero_index", zotero_index), + ("matches", matches), + ("generated_overrides", override_skeleton), + ("import_plan", plan), + ("epub_plan", epub_plan), + ("positioned_plan", positioned_plan), + ("final_plan", final_plan), + ]: + with open(outputs[key], "w", encoding="utf-8") as file: + json.dump(payload, file, ensure_ascii=False, indent=indent) + file.write("\n") + + with open(outputs["mismatch_review"], "w", encoding="utf-8") as file: + file.write(mismatch_review) + + status_counts: dict[str, int] = {} + for item in positioned_plan.get("items", []): + status = item.get("status", "unknown") + status_counts[status] = status_counts.get(status, 0) + 1 + + match_counts: dict[str, int] = {} + for match in matches.get("matches", []): + status = match.get("status", "unknown") + match_counts[status] = match_counts.get(status, 0) + 1 + + summary = { + "format": "kindle-zotero-importer.pipeline-summary.v1", + "workdir": str(workdir), + "overrides_path": str(overrides_path), + "outputs": {key: str(path) for key, path in outputs.items()}, + "counts": { + "clippings": len(clippings.get("clippings", [])), + "unique_titles": len(matches.get("matches", [])), + "final_annotations": final_plan.get("annotation_count", 0), + "match_statuses": match_counts, + "plan_statuses": status_counts, + "skipped_final": final_plan.get("skipped_counts", {}), + "is_incremental": final_plan.get("is_incremental", False), + "incremental_stats": final_plan.get("incremental_stats", {}), + "deletions": len(final_plan.get("deletions", [])), + }, + } + json_text = json.dumps(summary, ensure_ascii=False, indent=2) + if args.summary_output: + with open(args.summary_output, "w", encoding="utf-8") as file: + file.write(json_text) + file.write("\n") + else: + sys.stdout.write(json_text) + sys.stdout.write("\n") + report_progress(100, "Pipeline complete", "Ready to import annotations into Zotero") + return 0 + parser.error(f"unknown command: {args.command}") return 2 diff --git a/src/kindle_zotero_importer/epub_position.py b/src/kindle_zotero_importer/epub_position.py index 8639ee3..9c8e2a2 100644 --- a/src/kindle_zotero_importer/epub_position.py +++ b/src/kindle_zotero_importer/epub_position.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from html import unescape from html.parser import HTMLParser import posixpath -import re import zipfile import xml.etree.ElementTree as ET from typing import Any @@ -65,8 +64,8 @@ def _position_item( updated = dict(item) annotation = dict(updated["annotation"]) - annotation["position"] = position - annotation["sortIndex"] = _epub_sort_index(position["value"]) + annotation["position"] = position["selector"] + annotation["sortIndex"] = position["sortIndex"] updated["annotation"] = annotation updated["status"] = "positioned" return updated @@ -97,7 +96,7 @@ def extract_epub_text_nodes(path: str) -> list[TextNode]: return nodes -def find_epub_cfi(text_nodes: list[TextNode], quote: str) -> dict[str, str] | None: +def find_epub_cfi(text_nodes: list[TextNode], quote: str) -> dict[str, Any] | None: haystack = " ".join(node.text for node in text_nodes) normalized_haystack, haystack_map = _normalize_with_map(haystack) normalized_quote, _ = _normalize_with_map(quote) @@ -134,9 +133,12 @@ def find_epub_cfi(text_nodes: list[TextNode], quote: str) -> dict[str, str] | No f"{end_node.cfi_parent_path}/{end_node.text_step}:{end_offset})" ) return { - "type": "FragmentSelector", - "conformsTo": "http://www.idpf.org/epub/linking/cfi/epub-cfi.html", - "value": value, + "selector": { + "type": "FragmentSelector", + "conformsTo": "http://www.idpf.org/epub/linking/cfi/epub-cfi.html", + "value": value, + }, + "sortIndex": f"{spine_step:05d}|{raw_start:08d}", } @@ -257,13 +259,6 @@ def _node_at(nodes: list[TextNode], offset: int) -> tuple[TextNode | None, int]: return None, 0 -def _epub_sort_index(cfi: str) -> str: - numbers = [int(number) for number in re.findall(r"/([0-9]+)", cfi)] - spine = numbers[1] if len(numbers) > 1 else 0 - content = numbers[-1] if numbers else 0 - return f"{spine:05d}|{content:08d}" - - def _with_problem(item: dict[str, Any], problem: str) -> dict[str, Any]: updated = dict(item) updated["status"] = problem diff --git a/src/kindle_zotero_importer/final_plan.py b/src/kindle_zotero_importer/final_plan.py index 682882e..925dcfd 100644 --- a/src/kindle_zotero_importer/final_plan.py +++ b/src/kindle_zotero_importer/final_plan.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime from typing import Any @@ -20,10 +21,14 @@ def build_final_writer_plan(positioned_plan: dict[str, Any]) -> dict[str, Any]: skipped.get("positioned-missing-writer-fields", 0) + 1 ) continue + clipping = item["clipping"] annotations.append( { - "clipping_id": item["clipping"]["id"], - "clipping_title": item["clipping"]["title"], + "clipping_id": clipping["id"], + "clipping_title": clipping["title"], + "clipping_added_on": clipping.get("added_on"), + "clipping_added_on_iso": clipping.get("added_on_iso"), + "integrated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), "attachment_item_id": attachment["item_id"], "attachment_key": attachment["key"], "parent_item_id": item["zotero"]["parent_item_id"], @@ -37,11 +42,22 @@ def build_final_writer_plan(positioned_plan: dict[str, Any]) -> dict[str, Any]: "pageLabel": annotation.get("pageLabel") or "", "sortIndex": annotation.get("sortIndex"), "position": annotation["position"], - "tags": annotation.get("tags") or [{"name": "kindle-import"}], + "tags": [ + {"name": "kindle-import"}, + {"name": f"kindle-id:{clipping['id']}"}, + ], }, } ) + annotations.sort( + key=lambda entry: ( + entry.get("attachment_item_id") or 0, + entry["annotation"].get("sortIndex") or "", + entry.get("clipping_id") or "", + ) + ) + return { "format": FINAL_FORMAT, "source_format": positioned_plan.get("format"), diff --git a/src/kindle_zotero_importer/pdf_position.py b/src/kindle_zotero_importer/pdf_position.py index 5bc52b5..4d264e6 100644 --- a/src/kindle_zotero_importer/pdf_position.py +++ b/src/kindle_zotero_importer/pdf_position.py @@ -4,12 +4,31 @@ from dataclasses import dataclass import hashlib import os import re +import shutil import subprocess import tempfile import xml.etree.ElementTree as ET from typing import Any +_TOOL_PATHS = { + "pdftotext": ("/Users/ubd/bin/pdftotext", "/opt/homebrew/bin/pdftotext"), + "pdftohtml": ("/Users/ubd/bin/pdftohtml", "/opt/homebrew/bin/pdftohtml"), + "pdfinfo": ("/Users/ubd/bin/pdfinfo", "/opt/homebrew/bin/pdfinfo"), + "qpdf": ("/Users/ubd/bin/qpdf", "/opt/homebrew/bin/qpdf"), +} + + +def _tool(name: str) -> str: + resolved = shutil.which(name) + if resolved: + return resolved + for candidate in _TOOL_PATHS.get(name, ()): + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return name + + @dataclass(frozen=True) class PDFWord: text: str @@ -117,7 +136,7 @@ def _position_item( def extract_pdf_text_pages(path: str) -> list[str]: result = subprocess.run( - ["pdftotext", "-enc", "UTF-8", path, "-"], + [_tool("pdftotext"), "-enc", "UTF-8", path, "-"], check=True, capture_output=True, text=True, @@ -157,7 +176,7 @@ def _decrypted_pdf_path( base_name = os.path.splitext(os.path.basename(path))[0] or "document" output_path = os.path.join(temp_dir, f"{base_name}.{digest}.decrypted.pdf") subprocess.run( - ["qpdf", "--decrypt", path, output_path], + [_tool("qpdf"), "--decrypt", path, output_path], check=True, capture_output=True, text=True, @@ -194,7 +213,7 @@ def extract_pdf_page_xml(path: str, page_index: int) -> PDFPageXML: page_number = page_index + 1 result = subprocess.run( [ - "pdftohtml", + _tool("pdftohtml"), "-f", str(page_number), "-l", @@ -302,7 +321,7 @@ def _pdf_page_size( path: str, page_number: int, fallback_width: float, fallback_height: float ) -> tuple[float, float]: result = subprocess.run( - ["pdfinfo", "-f", str(page_number), "-l", str(page_number), path], + [_tool("pdfinfo"), "-f", str(page_number), "-l", str(page_number), path], check=True, capture_output=True, text=True,