-
Notifications
You must be signed in to change notification settings - Fork 4
/
readwise_bulk_import.js
455 lines (372 loc) · 17 KB
/
readwise_bulk_import.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/** Readwise Access Token from https://readwise.io/access_token */
let ACCESS_TOKEN = "XXX"; // if not changed here, script will prompt for it.
const BASE_URL = "https://readwise.io/api/v2/";
let booksRoot = WF.currentItem()
let bookCountImported = 0;
let newBookCount = 0;
let newHighlightCount = 0;
let oldBookCount = 0;
let oldHighlightCount = 0;
let noteTags = []
let bookListUpdated = booksRoot.getNote()
// Check the note section of the root node for details about the last successful sync.
// If these details do not exist, assume this is the first time we're running the sync.
if(bookListUpdated != ""){
bookListUpdated = bookListUpdated.split("Updated: ")[1]
bookListUpdated = bookListUpdated.split("...")[0]
bookListUpdated = new Date(bookListUpdated)
bookListUpdated = bookListUpdated.toISOString()
} else {
bookListUpdated = new Date("1980-01-01")
bookListUpdated = bookListUpdated.toISOString()
}
// If we have run the sync before, let's compile a map of the books for parsing below
let bookArray = []
let booksList = booksRoot.getChildren()
booksList.forEach(function(book){
let bookID = book.data.note.split("Resource ID: ")[1]
let bookUpdated = book.data.note.split("Updated: ")[1]
if (bookUpdated) { // no updates present on initial import
bookUpdated = bookUpdated.split(" | ")[0]
let arr = {
wfID: book.data.id,
wfName: book.data.name,
wfNamePlain: book.data.nameInPlainText,
wfNote: book.data.note,
bookID: bookID,
bookUpdated: bookUpdated
}
bookArray.push(arr)
}
});
// If the user has not updated the placeholder text with their Readwise API, prompt them for it now
if (ACCESS_TOKEN == "XXX") {
ACCESS_TOKEN = prompt("Enter Readwise Access Token from https://readwise.io/access_token");
}
/**
* General purpose readwise request method.
* Make a Readwise request of type 'method' at the specified url with the provided data
*
* @param {String} method http method: GET, POST, PUT, PATCH, DELETE
* @param {String} url request url
* @param {Object} params data parameters
* @return {Promise}
*/
function readwiseRequest(method, url, params) {
return Promise.resolve($.ajax({
type: method,
url: url,
contentType: 'application/json',
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Token ' + ACCESS_TOKEN);
},
data: params
}));
}
/**
* Retrieve all of the available results of type 'resource'.
*
* @param {String} resource [Readwise resource to retrieve. ie. books, highlights]
* @param {Object} params [Data object containing request parameters]
* @return {Array} Collection of results where the type 'resource'
*/
async function getAllResults(resource, params) {
let results = [];
let url = BASE_URL + resource + "/";
let complete = false;
let num_tries = 0;
let max_tries = 10;
while(!complete && num_tries < max_tries) {
num_tries += 1;
try {
let response = await readwiseRequest('GET', url, params);
console.debug('Response:', response);
if (response.results != null) {
results.push(...response.results);
}
if (response.next == null) {
console.debug("All done.");
complete = true;
} else {
console.debug("More to get...");
url = response.next;
params = null; // Params are already in the next url;
}
} catch (error) {
console.log('Error:', error);
}
}
return results;
}
/**
* Build a map of highlights where the key is the book id and the value is a mashup of the book details and the highlights for that book.
*
* @return {Object} The map of books with their highlights
*/
async function getAllHighlightsByBook() {
let books = await getAllResults("books", {"page_size": 1000, "num_highlights__gt": 0, "updated__gt": bookListUpdated});
let highlights = await getAllResults("highlights", {"page_size": 1000, "updated__gt": bookListUpdated});
let highlightsByBook = {};
// build map by book id
books.forEach((book) => { book.highlights = []; highlightsByBook[book.id] = book; });
// inject highlights into the corresponding book (or article, tweet, etc.)
highlights.forEach((highlight) => { highlightsByBook[highlight.book_id].highlights.unshift(highlight) } );
return highlightsByBook;
}
/**
* Update an existing Workflowy node with new details and/or highlights from the related resource delivered via the Readwise API.
*
* @param {String} existingBookID Update this book's node with new details and/or highlights
* @param {Object} book The complete book object delivered by the Readwise API
* @return {void} This function does not return a value.
*/
async function updateBookInWF(existingBookID, book){
let highlightArray = []
let highlightsList = WF.getItemById(existingBookID).getChildren()
// Create an array of the highlights that exist already for this book within WF
// We're going to use this array to check for existing highlights we need to update
highlightsList.forEach(function(highlight){
let highlightID = highlight.data.note.split("Note ID: ")[1];
let highlightUpdated = highlight.data.note.split("Highlighted: ")[1];
highlightUpdated = highlightUpdated.split(" | ")[0];
let highlightLocation = highlight.data.note.split("Location: ")[1];
highlightLocation = parseInt(highlightLocation.split(" | ")[0]);
let arr = {
wfID: highlight.data.id,
wfName: highlight.data.name,
wfNote: highlight.data.note,
highlightID: highlightID,
highlightedDate: highlightUpdated,
location: highlightLocation
}
highlightArray.push(arr)
});
if (book.author) {
book.author = book.author.replaceAll(',', '#');
book.author = book.author.replaceAll(' ', '_')
book.author = book.author.replaceAll('.', '')
book.author = book.author.replaceAll('#', ' #')
}
book.updated = new Date(book.updated)
let wfBook = WF.getItemById(existingBookID)
oldBookCount++
let itemNotes = [];
if (book.author) {
if (book.author.startsWith("@")) { // Leave Twitter authors alone
itemNotes.push(book.author);
} else {
itemNotes.push("#" + book.author);
}
}
itemNotes.push("Notes: " + book.num_highlights);
itemNotes.push("Updated: " + book.updated.toDateString());
itemNotes.push("Resource ID: " + book.id);
WF.setItemNote(wfBook, itemNotes.join(" | "));
// We're going to add any new highlights we find to this array.
// When we're done, we're going to sort them all by "Location",
// And use the wfMove function to rearrange them as necessary.
let highlights = wfBook.getChildren();
var bookHasNotes = false;
book.highlights.forEach(function(highlight){
// Does this highlight exist already in the highlights already listed on this WF node?
let existingHighlight = highlightArray.findIndex(x => x.highlightID == highlight.id)
highlight.highlighted_at = new Date(highlight.highlighted_at)
if(existingHighlight != "-1"){ // This highlight exists already - just update the existing one
existingHighlight = highlightArray.find(x => x.highlightID == highlight.id).wfID
let wfHighlight = WF.getItemById(existingHighlight)
oldHighlightCount++
WF.setItemName(wfHighlight, highlight.text)
itemNotes = [];
if (highlight.location) {
itemNotes.push("Location: " + highlight.location);
} else {
itemNotes.push("Location: 0");
}
itemNotes.push("Highlighted: " + highlight.highlighted_at.toDateString());
itemNotes.push("Note ID: " + highlight.id);
highlight.tags = [];
let noteWords = highlight.note.split(" ");
noteWords.forEach(word => {
if (word.startsWith(".")) {
highlight.tags.push(word.replaceAll(".", "#"));
}
});
if (highlight.tags.length > 0) {
itemNotes.push("Tags: " + highlight.tags.join(" "));
}
WF.setItemNote(wfHighlight, itemNotes.join(" | "));
if (highlight.note != ""){
let allNotes = wfHighlight.getChildren()
allNotes.forEach(function(note){
noteTag = WF.getItemTags(note)
noteTag = noteTag[0]["tag"]
if(noteTag == "#readwise_notes"){
WF.setItemName(note, highlight.note + " #readwise_notes")
bookHasNotes = true;
}
})
}
} else { // This highlight doesn't exist - create a new one
let wfHighlight = WF.createItem(WF.currentItem(), 0)
newHighlightCount++
WF.setItemName(wfHighlight, highlight.text)
itemNotes = [];
if (highlight.location) {
itemNotes.push("Location: " + highlight.location);
} else {
itemNotes.push("Location: 0");
}
itemNotes.push("Highlighted: " + highlight.highlighted_at.toDateString());
itemNotes.push("Note ID: " + highlight.id);
highlight.tags = [];
let noteWords = highlight.note.split(" ");
noteWords.forEach(word => {
if (word.startsWith(".")) {
highlight.tags.push(word.replaceAll(".", "#"));
noteTags.push(word.replaceAll(".", "#"));
}
});
if (highlight.tags.length > 0) {
itemNotes.push("Tags: " + highlight.tags.join(" "));
}
WF.setItemNote(wfHighlight, itemNotes.join(" | "));
highlights.push(wfHighlight)
if (highlight.note != ""){
let newNote = WF.createItem(wfHighlight,highlight.location)
WF.setItemName(newNote, highlight.note + " #readwise_notes")
bookHasNotes = true;
}
}
});
if (bookHasNotes){
WF.setItemName(wfBook, wfBook.getName().split(" #readwise_notes")[0] + " #readwise_notes")
}
// Credit to rawbytz (https://github.com/rawbytz/sort) for the code to sort the bullets
highlights.sort(function(a, b){
a = a.getNote().split("Location: ")[1].split(" | ")[0];
b = b.getNote().split("Location: ")[1].split(" | ")[0];
return a - b;
});
WF.editGroup(() => {
highlights.forEach((highlight, i) => {
if (highlight.getPriority() !== i) WF.moveItems([highlight], wfBook, i);
});
});
}
/**
* Add a new Workflowy node with details and highlights from the related resource delivered via the Readwise API.
*
* @param {Object} book The complete book object delivered by the Readwise API
* @return {void} This function does not return a value.
*/
async function addBookToWF(book) {
if (book.author) {
book.author = book.author.replaceAll(',', '#');
book.author = book.author.replaceAll(' ', '_')
book.author = book.author.replaceAll('.', '')
book.author = book.author.replaceAll('#', ' #')
}
let wfBook = WF.createItem(WF.currentItem(),0);
newBookCount++;
book.updated = new Date(book.updated)
if (book.source_url == null){
WF.setItemName(wfBook, book.title + ' #' + book.category)
} else {
WF.setItemName(wfBook, '<a href="' + book.source_url + '">' + book.title + '</a> #' + book.category)
}
let itemNotes = [];
if (book.author) {
if (book.author.startsWith("@")) { // Leave Twitter authors alone
itemNotes.push(book.author);
} else {
itemNotes.push("#" + book.author);
}
}
itemNotes.push("Notes: " + book.num_highlights);
itemNotes.push("Updated: " + book.updated.toDateString());
itemNotes.push("Resource ID: " + book.id);
WF.setItemNote(wfBook, itemNotes.join(" | "));
let wfHighlights = []
var bookHasNotes = false;
book.highlights.forEach((highlight) => {
highlight.highlighted_at = new Date(highlight.highlighted_at)
let wfHighlight = WF.createItem(WF.currentItem(),0)
newHighlightCount++
WF.setItemName(wfHighlight, highlight.text)
itemNotes = [];
if (highlight.location) {
itemNotes.push("Location: " + highlight.location);
} else {
itemNotes.push("Location: 0");
}
itemNotes.push("Highlighted: " + highlight.highlighted_at.toDateString());
itemNotes.push("Note ID: " + highlight.id);
highlight.tags = [];
let noteWords = highlight.note.split(" ");
noteWords.forEach(word => {
if (word.startsWith(".")) {
highlight.tags.push(word.replaceAll(".", "#"));
noteTags.push(word.replaceAll(".", "#"));
}
});
if (highlight.tags.length > 0) {
itemNotes.push("Tags: " + highlight.tags.join(" "));
}
WF.setItemNote(wfHighlight, itemNotes.join(" | "));
wfHighlights.push(wfHighlight)
if (highlight.note != ""){
let newNote = WF.createItem(wfHighlight,highlight.location)
WF.setItemName(newNote, highlight.note + " #readwise_notes")
bookHasNotes = true;
}
});
if (bookHasNotes){
WF.setItemName(wfBook, wfBook.getName().split(" #readwise_notes")[0] + " #readwise_notes")
}
// Credit to rawbytz (https://github.com/rawbytz/sort) for the code to sort the bullets
wfHighlights.sort(function(a, b){
a = a.getNote().split("Location: ")[1].split(" | ")[0];
b = b.getNote().split("Location: ")[1].split(" | ")[0];
return a - b;
});
WF.editGroup(() => {
wfHighlights.forEach((highlight, i) => {
if (highlight.getPriority() !== i) WF.moveItems([highlight], wfBook, i);
});
});
WF.moveItems(wfHighlights, wfBook);
}
/**
* Trigger the entire syncing process - adding or updating nodes baased on whether/when the sync was last run.
*
* @return {void} This function does not return a value.
*/
async function addAllHighlightsToWorkflowy() {
let highlightsByBook = await getAllHighlightsByBook();
let currentBook = 0;
let totalBooks = Object.keys(highlightsByBook).length;
Object.keys(highlightsByBook).forEach(book_id => {
++currentBook;
let book = highlightsByBook[book_id];
console.log("(" + currentBook + "/" + totalBooks + "): Adding '" + book.title + "' to WorkFlowy...");
// Does this book exist already in the books already listed in this WF node?
let existingBook = bookArray.findIndex(x => x.bookID == book.id);
// Yes, it does - update the book
if (existingBook != "-1"){
let existingBookID = bookArray.find(x => x.bookID == book.id).wfID;
updateBookInWF(existingBookID, book);
}
// No, it doesn't - add new book
else {
addBookToWF(book);
}
});
const timeElapsed = Date.now();
const today = new Date(timeElapsed);
WF.setItemNote(booksRoot, `Updated: ${today.toISOString()}...\n\nWelcome! This page stores your entire Readwise library.\n\nTIPS/TRICKS\n- Don't change any of the imported bullets\n- (Use sub-bullets instead)\n- Use the tags below to navigate\n- <a href=\"https://github.com/zackdn/wf-readwise-integration\">Reach out with questions/support!</a>\n\nSHORTCUTS\nUse these shortcuts to navigate through your library, highlights, and notes:\n#articles | #books | #podcasts | #readwise_notes | #supplementals | #tweets\n\n`);
// TODO: Add section in description for highlight tags via user's notes
// WF.setItemNote(booksRoot, `Updated: ${today.toDateString()}...\n\nWelcome! This page stores your entire Readwise library.\n\nTIPS/TRICKS\n- Don't change any of the imported bullets\n- (Use sub-bullets instead)\n- Use the tags below to navigate\n- <a href=\"https://github.com/zackdn/wf-readwise-integration\">Reach out with questions/support!</a>\n\nSHORTCUTS\nUse these shortcuts to navigate through your library, highlights, and notes:\n#articles | #books | #podcasts | #readwise_notes | #supplementals | #tweets\n\nYOUR NOTE TAGS\nUse these shortcuts to find highlights you've tagged via your notes:\n${noteTags.join(" ")}`);
console.log("Import complete!");
WF.showAlertDialog(`<strong>Success!</strong><br /><br /><strong>Imported:</strong><br />- ${newBookCount} new library items<br />- ${newHighlightCount} new highlights<br /><br /><strong>Updated:</strong><br />- ${oldBookCount} existing library items<br />- ${oldHighlightCount} existing highlights`)
}
addAllHighlightsToWorkflowy()