Action
import latest readwise highlights
created by @FlohGro / more on my Blog
import latest readwise highlights
This action will import the latest highlights from readwise into Drafts.
Each highlighted document will become a new draft and contain a link back to the original readwise ‘book‘.
The action will import ALL unimported documents from readwise which means it’ll run for a while when you run the action the first time (or after highlighting a big amount of documents).
Metadata of the highlighted documents will be imported, too. (E.g. if you use document tags in readwise they will be added to the draft)
note this is due to rate limiting at the readwise API - the action will keep you updated about the progress but give it a few minutes when you run it the first time
After all new drafts are created the action will import the latest highlighted document. If no new documents where available it will open the last imported document.
[Configuration]
You can configure the tags that should be added to each created draft in addition to the document tags. You need to edit the „Define Template Tag“ step in the action to modify this.
Do not change the name of the template tag!
Add the tags to the defined template tag tags_to_add
as a comma separated list. By default this is configured to reading,readwise,#2process
[Usage]
Run this action on a regular basis to retrieve your highlights from readwise and import them into drafts. There you can process them, link them to other drafts, review them or further summarize them.
If you find this useful and want to support me you can donate or buy me a coffe
Steps
-
defineTemplateTag
name tags_to_add
template readwise,reading,#2process
-
script
// import latest readwise highlights // created by @FlohGro let tagsToAdd = getTagsFromTemplateTag() let credential = Credential.create("Readwise", "insert your Readwise API token to allow Drafts to get data from Readwise.\nYou can retrieve the token by opening readwise.io/access_token and copy it from there"); credential.addPasswordField("authtoken", "authentication token"); credential.authorize(); const booksEndpointWithHighlights = "https://readwise.io/api/v2/books/?num_highlights__gt=0" const bookHighlightsEndpoint = "https://readwise.io/api/v2/highlights/?book_id=" function run() { let http = HTTP.create(); // create HTTP object let draftsToCreate = [] let responseData = performPaginatedRequestToGivenReadwiseApiEndpoint(booksEndpointWithHighlights) if (responseData) { let books = responseData //.results let continueImport = true let curBookIndex = 0 while (continueImport) { let success = true let latestBook = books[curBookIndex] if (isBookAlreadyImported(`> [Readwise Book Highlights](${latestBook.highlights_url})`)) { continueImport = false; } else { let response = http.request({ "url": bookHighlightsEndpoint + latestBook.id, "method": "GET", "headers": { "Authorization": "Token " + credential.getValue("authtoken"), } }); responseData = undefined; if (response.success) { responseData = response.responseData; } else { console.log(response.statusCode); console.log(response.error); app.displayInfoMessage("wait for " + response.headers["retry-after"] + "s due to rate limits...") let timeToWait = parseInt(response.headers["retry-after"]) * 1000 sleep(timeToWait); // wait for 5 seconds app.displayInfoMessage("continuing import") success = false } if (responseData && success) { let highlights = responseData.results let highlightsTexts = []; for (let highlight of highlights) { let tags = highlight.tags tags = tags.map((tag) => { return "*#" + tag.name + "*" }) const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; let foundEmojis = "" for (tag of tags) { let match = tag.match(emojiRegex); if (match) { foundEmojis = foundEmojis + match[0] + " " } } let text = highlight.text let highlightNote = highlight.note != "" ? " (" + highlight.note + ")" : "" highlightsTexts.push("- " + foundEmojis + text + " " + tags.join(", ") + highlightNote) } highlightsTexts = highlightsTexts.reverse() let sourceText = latestBook.source_url ? "\n- source: [" + latestBook.title + "](" + latestBook.source_url + ")" : "" let documentNoteText = latestBook.document_note ? "\n\n## Note\n\n" + latestBook.document_note : "" // create the text if (latestBook.highlights_url) { let content = `# ${latestBook.title} ![](${latestBook.cover_image_url}) - author: [[${latestBook.author}]]${sourceText} ## Highlights (${highlights.length}) ${highlightsTexts.join("\n")} --- > [Readwise Book Highlights](${latestBook.highlights_url}) ` let d = new Draft() d.content = content for(let tag of tagsToAdd){ d.addTag(tag) } // also add the document tags from readwise for (tag of latestBook.tags) { d.addTag(tag.name) } draftsToCreate.push(d) } } } if (success) { curBookIndex = curBookIndex + 1 if (curBookIndex >= books.length) { continueImport = false } } } } draftsToCreate = draftsToCreate.reverse() let lastDraft for (d of draftsToCreate) { d.update() lastDraft = d } editor.load(lastDraft) if (draftsToCreate.length == 0) { app.displayInfoMessage("no new highlights in readwise") } else { app.displaySuccessMessage("imported " + draftsToCreate.length + " new highlighted documents") } } run() function isBookAlreadyImported(searchText) { let foundDrafts = getDraftsContainsGivenText(searchText) if (foundDrafts.length > 0) { // highlights already imported // length should be 1, we can just open the draft now and display an alert editor.load(foundDrafts[0]) return true } else { return false; } } function getDraftsContainsGivenText(searchText) { let foundDrafts = Draft.query(searchText, "all", [], [], "modified", false, false) return foundDrafts } function sleep(milliseconds) { const date = Date.now(); let currentDate = null; let notificationFactor = 1 do { currentDate = Date.now(); if (currentDate - date > (15000 * notificationFactor)) { const remainingSeconds = Math.round((milliseconds - (currentDate - date)) / 1000) app.displayInfoMessage(remainingSeconds + "s remaining") notificationFactor = notificationFactor + 1 } } while (currentDate - date < milliseconds); } function performPaginatedRequestToGivenReadwiseApiEndpoint(firstEndpoint) { let http = HTTP.create(); // create HTTP object let continueRequest = true let responseData = []; let currentPageRequest = firstEndpoint while (continueRequest) { let response = http.request({ "url": currentPageRequest, "method": "GET", "headers": { "Authorization": "Token " + credential.getValue("authtoken"), } }); if (response.success) { let data = response.responseData responseData = responseData.concat(data.results); let nextPage = data.next if (nextPage) { currentPageRequest = nextPage } else { continueRequest = false } // alert(nextPage + "\n" + data.count + "\n" + responseData.length) } else { alert("error:\n" + response.statusCode + "\n" + response.error) } } return responseData } function getTagsFromTemplateTag(){ let tagsStr = draft.processTemplate("[[tags_to_add]]") let tags = tagsStr.split(",") tags = tags.map((tag) => {return tag.trim()}) return tags }
Options
-
After Success Nothing Notification Error Log Level Info