I’ve just published instructions for setting up a perhaps-interesting new automation for your procrastination enjoyment.
In case you don’t want to download a .scpt file from my site, here’s the script in full:
property summaryNotesGroupUUID : "E940D2EB-5B4A-4D29-B64F-E585AA756826"
property readingSessionNotePrefix : "∎ " -- This is a prefix I use to indicate summary notes. If you don't want to use a prefix, switch it to ""
property readingSessionNoteSuffix : " - Reading Session "
property delayAfterReadingSessionInMinutes : 15
property debug : false -- debug flag. See immediately below.
property annotationFileYAML : "annotation-status: new" -- default YAML annotation fields and values. Separate each with "& return &" in order to make sure each field has its own line. E.g., `"annotation-status: new" & return & "some-other-field: someValue" will give you annotation-status and some-other-field on two different lines.
property newLineCharacter : "
" -- Using "return" does not work when trying to use findAndReplaceText and splitText on newlines. The script uses this variable to clean up the code.
on run
if debug then -- debug routine. Switch the debug : false above to true, and run the script manually with a test record selected to immediately test the script on the selected file. Note that this will fail to debug anything to do with delayAfterReadingSessionInMinutes.
set previousDelayAfterReadingSessionInMinutes to delayAfterReadingSessionInMinutes
set delayAfterReadingSessionInMinutes to 0
tell application id "DNtp"
set theSelection to get the selection
my performSmartRule(theSelection) -- runs the performSmartRule function below.
end tell
set delayAfterReadingSessionInMinutes to previousDelayAfterReadingSessionInMinutes
end if
end run
-- # Script info
-- Author: @ryanjamurphy
-- Created in April 2022
-- Requires DEVONthink 3 Pro (for the custom metadata feature)
-- ## Explanation
-- This script is designed to be executed by a DEVONthink Smart Rule configured to pick up an increase in the number of annotations on a recently-modified PDF.
-- When everything is all set up, this is how the workflow works:
-- You read a PDF, and make some highlights/strikethroughs/underlines. Fifteen minutes after you last modify the PDF (i.e., after you've "put the PDF down"), this script runs. When it runs, it extracts any of the newly-added annotations (note the assumptions below) into a uniquely-titled "Reading Session" markdown note (see "Generating a unique note title" below).
-- ## Generating a unique note title
-- Notes will be titled with the syntax `readingSessionNotePrefix (configured as a property above) recordName readingSessionNoteSuffix (again, configured above) date+timestamp`. Like this: `∎ The Recurse Center User’s Manual - Recurse Center - Reading Session 202204271206`
-- ## Set up
-- Before you use the script, you have to configure one thing yourself, and you can modify the prefix and suffix used to distinguish reading notes from other notes.
-- ### You _must_ configure the following
-- #### Where to save reading session notes
-- **Summary note group (summaryNotesGroupUUID):** Notes are saved to a specific DEVONthink group. Choose the group you want the notes to end up in, right-click it, and select "Copy Item Link." Paste that copied value above, for the property `summaryNotesGroupUUID`, again deleting the `x-devonthink-item://` part at the front of the link.
-- #### Optional configurations
-- **Reading Session note prefix (readingSessionNotePrefix):** I like to have a reliable, visible prefix in summary note filenames, so I don't get them confused with other notes and so that I can easily find them (or avoid them) when using "quick open"/"quick switcher"-type features. Switch this property to whatever you want. If you don't want a prefix at all, make the property "".
-- **Reading Session note suffix (readingSessionNoteSuffix):** Similar to the above, this property appends the specified text to every reading note filename, _before_ the date+timestamp. Change it to whatever you want or make the property "" to avoid using a suffix at all (except for the date+timestamp, which is necessary).
-- **The time to wait after annotating a reading before extracting highlights (delayAfterReadingSessionInMinutes):** How many minutes' delay you want between when you've annotated a reading and when the annotations are extracted into a new Reading Session note. I recommend that this be at least 10 minutes and not more than an hour, unless you are careful about how you configure the Smart Rule that executes this script.
-- **Annotation file YAML (annotationFileYAML):** If you're using a markdown editor that leverages YAML (https://en.wikipedia.org/wiki/YAML), you may want certain fields automatically inserted in your Reading Session Notes. Modify this property to change this. Set it to just "" if you don't want to use YAML. (I use `annotation-status: new` to be able to query my reading session notes for newly added annotations in Obsidian (https://obsidian.md) with the Dataview plugin (https://blacksmithgu.github.io/obsidian-dataview/).)
on performSmartRule(theRecords)
-- Calculate a date+timestamp to make sure the summary notes created by this script are unique.
set {year:yr, month:mn, day:dy, hours:hr, minutes:mins} to (current date)
set dateandtimestamp to "20" & my pad(yr as integer) & my pad(mn as integer) & my pad(dy as integer) & my pad(hr as integer) & my pad(mins as integer) -- Got this from https://macscripter.net/viewtopic.php?id=44567 as a quick and dirty way of getting a Zk-style timestamp. It didn't include the "20" in "2022" so I prepended it manually. That'll become a problem in 87 years or so...
set datestamp to "20" & my pad(yr as integer) & my pad(mn as integer) & my pad(dy as integer)
set timestamp to my pad(hr as integer) & ":" & my pad(mins as integer)
tell application id "DNtp"
set theInbox to inbox
set summaryNotesGroup to get record with uuid summaryNotesGroupUUID
repeat with eachRecord in theRecords
-- First, the script makes sure it's been at least fifteen minutes since the note was modified.
-- This is because we do not want the script to execute every time we make any change at all. Instead, the goal is to extract the highlights for every "reading session." So, the
set recordModified to eachRecord's modification date
set recordOpened to eachRecord's opening date
set recordCreated to eachRecord's creation date
set recordAdded to eachRecord's addition date
if (((current date) - recordModified) > delayAfterReadingSessionInMinutes * minutes) and (((current date) - recordOpened) > delayAfterReadingSessionInMinutes * minutes) and (((current date) - recordCreated) > delayAfterReadingSessionInMinutes * minutes) and (((current date) - recordAdded) > delayAfterReadingSessionInMinutes * minutes) then -- If this is true, it has been `delayAfterReadingSessionInMinutes` since the file has changed, and we can therefore go ahead and create a Reading Session note.
-- Before we get too excited, the script will make sure the file content has _actually_ changed. (We don't want to run the script and waste resources if the file's contents haven't actually been modified.)
set currentFileSize to eachRecord's size
set previousFileSize to get custom meta data for "Previous filesize" from eachRecord
if previousFileSize is missing value then
set previousFileSize to currentFileSize - 1
end if
if (currentFileSize ≠ previousFileSize) then -- the file content has changed
-- Create the annotation note name.
set annotationNoteName to readingSessionNotePrefix & (eachRecord's (name without extension)) & readingSessionNoteSuffix & dateandtimestamp
-- The script uses DEVONthink's Summarize Highlights feature to extract annotations from the PDF.
set highlightsSummary to summarize highlights of records eachRecord as list to markdown in incoming group -- The script creates this summary in the inbox, because it will be deleted if there are no new annotations.
if highlightsSummary is not missing value then -- Highlights were successfully summarized, now we have to clean the resulting syntax
-- Get the text of the newly-created summary, then convert the list into an array of the new highlights using splitText.
set highlightsSummaryText to plain text of highlightsSummary
set highlightsArray to my splitText(highlightsSummaryText, (newLineCharacter & "* "))
-- Make sure there are new annotations by comparing the current annotation count to the previous annotation count metadata.
set newAnnotationsCount to eachRecord's annotation count
set previousAnnotationCount to get custom meta data for "Previous annotation count" from eachRecord
if previousAnnotationCount is missing value then
set previousAnnotationCount to 0
end if
set numberOfNewAnnotations to newAnnotationsCount - previousAnnotationCount
if (numberOfNewAnnotations > 0) then -- There are some new annotations.
-- The script is now going to iterate through the array of annotations.
set annotationIterator to 0
-- Extract the first item in the array of annotations and get the title from it. This is the default title generated by DEVONthink's Summarize Highlights feature.
set annotationFileOriginalHeader to the first item in highlightsArray
set linesOfAnnotationFileHeader to my splitText(annotationFileOriginalHeader, newLineCharacter)
-- Prepend a YAML header to the note, if using.
if annotationFileYAML is not equal to "" then
set annotationFileHeader to "---" & return & annotationFileYAML & return & "---" & return & return & the first item in linesOfAnnotationFileHeader
else
set annotationFileHeader to the first item in linesOfAnnotationFileHeader
end if
-- Initialize the reading session annotations with a subtitle.
set newAnnotations to annotationFileHeader & return & "Reading session from [[" & datestamp & "]] at " & timestamp & return & return
-- Iterate through the highlights, and extract any newly-added annotations to the newAnnotations array.
repeat with eachAnnotation in highlightsArray
if annotationIterator > previousAnnotationCount then
set newAnnotations to newAnnotations & return & "* " & eachAnnotation -- Prepend each line with "* " in order to keep each line consistent for cleanup in a moment.
end if
set annotationIterator to annotationIterator + 1
end repeat
-- Clean up the extracted annotations using replaceText. DEVONthink by default uses Critic Markup (https://fletcher.github.io/MultiMarkdown-6/syntax/critic.html), which I don't want to use. DEVONthink also keeps the highlighting around highlights, which I want to remove and convert to quotes. (I can then later highlight these highlights, if I want.) I also don't want to use asterisks for bulleted lines. If you want to remove these and/or add your own, do so while referencing an actual Summarize Highlights file so that you're sure it's replacing exactly and only what you want and adding exactly and only what you want!
set newAnnotations to my replaceText(newAnnotations, "* {==", "- > ") -- Get rid of criticmarkup's highlights
set newAnnotations to my replaceText(newAnnotations, newLineCharacter & "* ", newLineCharacter & "- ") -- Switch asterisk-based bulleted lists with hyphen-based bulleted lists
set newAnnotations to my replaceText(newAnnotations, "==}" & newLineCharacter, "" & newLineCharacter) -- Get rid of criticmarkup's highlights
set newAnnotations to my replaceText(newAnnotations, "\\", "") -- Get rid of backslashes, which DEVONthink inserts to escape things sometimes.
set newAnnotations to my replaceText(newAnnotations, (")" & return & "- "), (")" & return & return & "- ")) -- ensure there's a blank line between headings and the next extracted highlight.
-- "Save" the new annotations by setting our summary note's `plain text` to our newly-assembled and cleaned text.
set plain text of highlightsSummary to newAnnotations
-- Rename the summary note with our unique, date+timestamped name.
set name of highlightsSummary to annotationNoteName
-- Remember how the summary note was initially created in the inbox? Now the script will move it to the final destination you configured at the top of the script.
move record highlightsSummary from incoming group to summaryNotesGroup
-- Update the PDF's Previous Annotation Count metadata.
add custom meta data newAnnotationsCount for "Previous Annotation Count" to eachRecord
-- Update the PDF's Previous Filesize metadata.
add custom meta data currentFileSize for "Previous Filesize" to eachRecord
-- Post a notification to let you know what we've done.
if numberOfNewAnnotations is equal to 1 then -- only one newly-extracted annotation
set notificationMessage to (numberOfNewAnnotations as text) & " annotation has been automatically extracted from \"" & eachRecord's name without extension & "\"."
else -- more than one newly-extracted annotation
set notificationMessage to (numberOfNewAnnotations as text) & " annotations have been automatically extracted from \"" & eachRecord's name without extension & "\"."
end if
display notification notificationMessage
else
-- There are no new annotations, so (1) update the previous filesize metadata so the script doesn't trigger again and (2) delete the temporary Summarize Highlights note we created in the inbox.
add custom meta data currentFileSize for "Previous filesize" to eachRecord
delete record highlightsSummary
end if
end if
end if
end if
end repeat
end tell
end performSmartRule
on pad(v) -- got this from https://macscripter.net/viewtopic.php?id=44567
return text -2 thru -1 of ((v + 100) as text)
end pad
-- Apple utility functions, from https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/ManipulateText.html
on splitText(theText, theDelimiter)
set AppleScript's text item delimiters to theDelimiter
set theTextItems to every text item of theText
set AppleScript's text item delimiters to ""
return theTextItems
end splitText
on replaceText(this_text, search_string, replacement_string)
set prevTIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to the search_string
set the item_list to every text item of this_text
set AppleScript's text item delimiters to the replacement_string
set this_text to the item_list as string
set AppleScript's text item delimiters to prevTIDs
return this_text
end replaceText