I hate AppleScript. Really hate it. But I love Devonthink, and had an itch to scratch. This is somewhat similar to other scripts that do bibliographic lookups, but it uses crossref.org.
Feedback, etc. welcome.
I hate AppleScript. Really hate it. But I love Devonthink, and had an itch to scratch. This is somewhat similar to other scripts that do bibliographic lookups, but it uses crossref.org.
Feedback, etc. welcome.
Looks like an interesting script. However, as it requires either a text selection or a certain name I wonder what kind of input crossref.org accepts/expects.
By the way, do your documents contain a DOI (digital object identifier)? Then the smart rule script Download Bibliographic Metadata might be useful too.
Okay, I should obviously read the comments how to use it first
It uses crossref’s query.bibliographic
, which allows pretty broad input; authors, ISBNs, titles, etc.
My workflow when I download papers is to usually to set the name of the paper using control-command-I (awesome feature, was so happy when I found that), that’s why it takes the name if you don’t select any text.
Some papers contain DOIs, but many don’t I might make it a bit more nuanced if there’s existing metadata…
For hating AppleScript, it looks like you made something useful… and I hope that itch is gone now
Hi, I followed your instructions (I think!), but when I select the script in DT, nothing happens?
Since I’m not particulary fond of AS either, I took the liberty to rewrite the script in JavaScript. It seems to work (tested it with one record only, though). Also, I stumbled upon results from the API call without a title
field, the script then fills in a stupid placeholder. Don’t know if that is reasonable or not.
Here goes
(() => {
const app = Application("DEVONthink 3");
/* Need currentApplication() for user interaction */
const curApp = Application.currentApplication();
curApp.includeStandardAdditions=true;
/* Basic error checking */
const thinkWindow = app.thinkWindows();
const contentRec = app.contentRecord();
if (!thinkWindow|| !thinkWindow[0]) throw "No window open";
if (!contentRec) throw "No document selected";
/* Query for either selected text or the name of the record if no text is selected */
const query = thinkWindow[0].selectedText() || contentRec.name();
const apiURL = 'https://api.crossref.org/works';
const shellCmd = `curl -A "(https://gist.github.com/mnot/0d7825bde9b9d3233f623c71765f20ca)" -G ${apiURL} --data-urlencode query.bibliographic='${query}' -d rows=5 -d select=author,title,created,type,publisher,published,subject`;
const apiResult = JSON.parse(curApp.doShellScript(shellCmd));
/* Basic error checking for the return value of the API call */
if (apiResult.status !== "ok") {
throw "API response not OK: " & apiResult.message;
}
const itemList = apiResult.message.items;
if (itemList.length === 0) {
curApp.displayAlert("No matches found!");
return;
}
/* Arrays to save the dates and authors, no need to extract them twice */
const choices = [], dates = [], authors = [];
/* Build list of choices from results */
itemList.forEach(item => {
const title = item.title ? item.title[0] : "No Title?";
let detailString = extractAuthor(item);
authors.push(detailString);
const dateString = extractDate(item);
dates.push(dateString);
detailString += `${detailString ? ', ' : ''}${dateString}`;
choices.push(`${title} (${detailString})`);
})
const selection = curApp.chooseFromList(choices, {withPrompt: "Select:"});
if (!selection) return;
/* User selected fromt the list of references: get the selected index */
const selectedIndex = choices.indexOf(selection[0]);
/* get the corresponding item from the API result */
const selectedItem = apiResult.message.items[selectedIndex];
/* get the corresponding date */
const selectedDate = dates[selectedIndex].split('-');
selectedDate[1]--; /* months are 0-indexed in JS! */
/* Set the record's date to the date of the selected item */
contentRec.date = new Date(...selectedDate);
/* add the 'type' field from the result to the record's tags */
const typeTag = selectedItem.type;
const recordTags = contentRec.tags();
if (recordTags.indexOf(typeTag) === -1) {
recordTags.push(typeTag);
contentRec.tags = recordTags;
}
/* Set PDF metadata fields Titel and Author */
const recordPath = contentRec.path();
setPDFMetadata(curApp, "Title", selectedItem.title[0], recordPath);
setPDFMetadata(curApp, "Author", authors[selectedIndex], recordPath);
})()
function setPDFMetadata(curApp, key, value, path) {
const pathToExiftool = '/usr/local/bin/exiftool';
const shellCommand = `${pathToExiftool} -overwrite_original -${key}='${value}' '${path}'`;
curApp.doShellScript(shellCommand);
}
/* Extract the first author from one item of the API result */
function extractAuthor(item) {
if (!item.author) return "";
const firstAuthor = item.author[0];
let authorString = `${firstAuthor.given || ""} ${firstAuthor.family || ""}`;
return authorString.trim();
}
/* extract the published date from the API result.
If it does not exist or is not complete, use create date */
function extractDate(item) {
let year, month, day;
if (item.published && item.published['date-parts']) {
[year, month, day] = item.published['date-parts'][0];
}
/* if one of year, month or day are still undefined, get values from 'created' field */
if (!(year && month && day)) {
[year, month, day] = item.created['date-parts'][0];
}
return `${year}-${month}-${day}`;
}
Thanks for sharing the script @mnot.
I’m encountering the same issue as @extracampine after following the installation instructions.
Has anyone found the cause or, even better, a way to fix this?
Is anything reported in DT’s log window?
Nothing is logged and there is no visible sign that anything is happening otherwise.
You could run the script from script editor and open its message area. Then you’ll see all Apple everts happening (or not) which might give some ideas as to what’s going on (or not)
That was a great hint, thanks!
The script shows the selection dialogue as expected if I run it through script editor. If I instead run the same script by clicking on the entry in DT’s script menu, nothing happens. Will try some stuff and report back if something solves it.
// Edit: It’s working now in DT! In case it helps someone, these are the steps that seem to have worked:
Apparently homebrew does not always automatically install applications to the usr/local/bin path - however, this is the path referenced in the script. After not finding Exiftool there although it was already installed, I found it in my homebrew install directory and changed the path in the script accordingly. Then, after running the script through Script Editor once and then trying again in DT, I got the below message and confirmed. It’s now fully functional - looking forward to further testing.
Good to see that it works now. Bad to see that Apple(Script) doesn’t throw an error when it can’t find the external program.
And another argument for going with self-contained scripts as much as possible. The same holds, btw, for JSONHelper. If one used JavaScript instead of AppleScript, this tools were not necessary at all. Less stuff to install, less cruft, less problems.
It would actually be possible to set the PDF metadata using Objective-C from the script, avoiding exiftool completely. Probably more elegantly, too, than calling exiftool for Title
and Author
separately.
I’ve updated the script, including the installation instructions; see how that goes for you (note especially the osacompile
step).
Does this script still exist? It does not appear to be one installed in Devonthink 3 Pro (at least on my computer) and I couldn’t find it in More Scripts…
It’s a smart rule script for use in smart rules with an Execute Script action.
Hi @BLUEFROG,
I modified the script of @mnot to also extract the DOI from crossref.org I can put the DOI into one of the metadata fields via exiftool in the same way author and title are written. At the moment I am writing the DOI data in the subject field of the file, since the lag of a better field available at the moment.
But I would like to go one step further and pass the DOI into the custom metadata field DOI provided by Devonthink. Because when I do that I can run the smart rule script mentioned before to fill even more metadata automatically.
But I couldn’t find a way to access that field in the apple script, can you help me out here please.
Below the modified version of the script. I added all the DOI parts, rest is the same as before.
-- Look up document metadata on CrossRef.org in DevonThink 3
--
-- Currently sets:
-- * Created date to the document's publication date
-- * Title in document properties
-- * Author in document properties (first author only)
-- * A tag for the type of document
--
--
-- To install:
-- 0. Quit DevonThink
-- 1. Place this script in the DevonThink Scripts folder (`Scripts -> Open Scripts Folder`)
-- 2. In Terminal, navigate to that folder and run `osacompile -o Crossref\ Lookup.scpt crossref-lookup.applescript; rm crossref-lookup.applescript`
-- 3. Install JSON Helper: https://apps.apple.com/au/app/json-helper-for-applescript/id453114608?mt=12
-- 4. Install exiftool: `brew install exiftool` (using homebrew; see https://brew.sh)
-- 5. Give the script a keyboard shortcut (`System Preferences -> Keyboard -> Shortcuts -> App Shortcuts`) (optional)
--
-- To use:
-- 0. Open a PDF in DevonThink
-- 1. Using your cursor, select a paper's title, ISSN, or similar identifying information
-- 2. Trigger the script (e.g., using they keyboard shortcut
-- 3. Select the best fitting candidate from the list
-- 4. Click `OK`
tell application id "DNtp"
try
if not (exists think window 1) then error "No window open."
if not (exists content record) then error "No document selected."
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
return
end try
set theRecord to content record
set query to the selected text of think window 1 as string
if query is "" then
set query to name of theRecord
end if
show progress indicator "Looking up references..." steps -1
end tell
-- Make an API request to crossref.org
set apiURL to "https://api.crossref.org/works"
set shellScript to ("curl -A \"(https://gist.github.com/mnot/0d7825bde9b9d3233f623c71765f20ca)\" -G " & apiURL ¬
& " --data-urlencode query.bibliographic=" & quoted form of query ¬
& " -d rows=5 -d select=author,title,created,type,publisher,published,subject,DOI")
set apiResult to (do shell script shellScript)
tell application id "DNtp"
hide progress indicator
end tell
-- parse json
tell application "JSON Helper"
set json to read JSON from apiResult
end tell
if not status of json is equal to "OK" then
error "API response not OK: " & message of json
return
end if
-- Populate and display a dialogue
try
set theItems to |items| of message of json
set theChoices to {}
repeat with a from 1 to length of theItems
set currentItem to item a of theItems
set titleList to title of currentItem
set title to item 1 of titleList
set dateString to my extractDate(currentItem)
set detailString to my extractAuthor(currentItem)
if not detailString is equal to "" then
set detailString to detailString & ", "
end if
set detailString to detailString & dateString
set theChoice to title & " (" & detailString & ")"
set end of theChoices to theChoice
end repeat
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
return
end try
set theChoice to choose from list theChoices with prompt "Select:"
if theChoice is false then return
repeat with a from 1 to length of theChoices
set currentItem to item a of theChoices
if currentItem as string is equal to theChoice as string then
set chosenItem to item a of theItems
end if
end repeat
-- update document
set dateString to my extractDate(chosenItem)
set appleDate to date (dateString)
tell application id "DNtp"
try
if not (exists think window 1) then error "No window open."
if not (exists content record) then error "No document selected."
set theRecord to content record
set recordPath to path of theRecord
-- date
set the date of theRecord to appleDate
-- tags
set typeTag to |type| of chosenItem
if typeTag is not in tags of theRecord then
set tagList to tags of theRecord
set end of tagList to typeTag
set tags of theRecord to tagList
end if
-- title
set recordTitle to item 1 of title of chosenItem
my setPDFMetadata("Title", recordTitle, recordPath)
-- author
set authorString to my extractAuthor(chosenItem)
my setPDFMetadata("Author", authorString, recordPath)
-- DOI
set doiString to my extractDOI(chosenItem)
my setPDFMetadata("Subject", doiString, recordPath)
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
end try
end tell
on extractDate(record_item)
try
set [iyear, imonth, iday] to item 1 of |date-parts| of published of record_item
on error
set [iyear, imonth, iday] to item 1 of |date-parts| of created of record_item
end try
set dateString to ((iday as string) & "-" & imonth as string) & "-" & iyear as string
return dateString
end extractDate
on extractDOI(record_item)
try
set doi_entries to DOI of record_item
on error
set doi_entries to "missing"
end try
set doiString to doi_entries
return doiString
end extractDOI
on extractAuthor(record_item)
set firstAuthor to ""
try
set author_entries to author of record_item
on error
return ""
end try
repeat with a from 1 to length of author_entries
set this_entry to item a of author_entries
if sequence of this_entry is equal to "first" then
set firstAuthor to this_entry
end if
end repeat
if firstAuthor is equal to "" then
return ""
end if
try
set authorString to |given| of firstAuthor & " " & family of firstAuthor
on error
try
set authorString to family of firstAuthor
on error
set authorString to |name| of firstAuthor
end try
end try
return authorString
end extractAuthor
on setPDFMetadata(mdKey, mdValue, recordPath)
set shellCommand to "/opt/homebrew/bin/exiftool -overwrite_original -" & mdKey & "=" & quoted form of mdValue & " " & quoted form of recordPath
set exifresult to do shell script shellCommand
end setPDFMetadata
Thanks & BR
AWD
Problem solved
-- Look up document metadata on CrossRef.org in DevonThink 3
--
-- Currently sets:
-- * Created date to the document's publication date
-- * Title in document properties
-- * Author in document properties (first author only)
-- * A tag for the type of document
--
--
-- To install:
-- 0. Quit DevonThink
-- 1. Place this script in the DevonThink Scripts folder (`Scripts -> Open Scripts Folder`)
-- 2. In Terminal, navigate to that folder and run `osacompile -o Crossref\ Lookup.scpt crossref-lookup.applescript; rm crossref-lookup.applescript`
-- 3. Install JSON Helper: https://apps.apple.com/au/app/json-helper-for-applescript/id453114608?mt=12
-- 4. Install exiftool: `brew install exiftool` (using homebrew; see https://brew.sh)
-- 5. Give the script a keyboard shortcut (`System Preferences -> Keyboard -> Shortcuts -> App Shortcuts`) (optional)
--
-- To use:
-- 0. Open a PDF in DevonThink
-- 1. Using your cursor, select a paper's title, ISSN, or similar identifying information
-- 2. Trigger the script (e.g., using they keyboard shortcut
-- 3. Select the best fitting candidate from the list
-- 4. Click `OK`
tell application id "DNtp"
try
if not (exists think window 1) then error "No window open."
if not (exists content record) then error "No document selected."
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
return
end try
set theRecord to content record
set query to the selected text of think window 1 as string
if query is "" then
set query to name of theRecord
end if
show progress indicator "Looking up references..." steps -1
end tell
-- Make an API request to crossref.org
set apiURL to "https://api.crossref.org/works"
set shellScript to ("curl -A \"(https://gist.github.com/mnot/0d7825bde9b9d3233f623c71765f20ca)\" -G " & apiURL ¬
& " --data-urlencode query.bibliographic=" & quoted form of query ¬
& " -d rows=5 -d select=author,title,created,type,publisher,published,subject,DOI")
set apiResult to (do shell script shellScript)
tell application id "DNtp"
hide progress indicator
end tell
-- parse json
tell application "JSON Helper"
set json to read JSON from apiResult
end tell
if not status of json is equal to "OK" then
error "API response not OK: " & message of json
return
end if
-- Populate and display a dialogue
try
set theItems to |items| of message of json
set theChoices to {}
repeat with a from 1 to length of theItems
set currentItem to item a of theItems
set titleList to title of currentItem
set title to item 1 of titleList
set dateString to my extractDate(currentItem)
set detailString to my extractAuthor(currentItem)
if not detailString is equal to "" then
set detailString to detailString & ", "
end if
set detailString to detailString & dateString
set theChoice to title & " (" & detailString & ")"
set end of theChoices to theChoice
end repeat
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
return
end try
set theChoice to choose from list theChoices with prompt "Select:"
if theChoice is false then return
repeat with a from 1 to length of theChoices
set currentItem to item a of theChoices
if currentItem as string is equal to theChoice as string then
set chosenItem to item a of theItems
end if
end repeat
-- update document
set dateString to my extractDate(chosenItem)
set appleDate to date (dateString)
tell application id "DNtp"
try
if not (exists think window 1) then error "No window open."
if not (exists content record) then error "No document selected."
set theRecord to content record
set recordPath to path of theRecord
-- date
set the date of theRecord to appleDate
-- tags
set typeTag to "import_Metadata"
if typeTag is not in tags of theRecord then
set tagList to tags of theRecord
set end of tagList to typeTag
set tags of theRecord to tagList
end if
set typeTag to |type| of chosenItem
if typeTag is not in tags of theRecord then
set tagList to tags of theRecord
set end of tagList to typeTag
set tags of theRecord to tagList
end if
-- title
set recordTitle to item 1 of title of chosenItem
my setPDFMetadata("Title", recordTitle, recordPath)
-- author
set authorString to my extractAuthor(chosenItem)
my setPDFMetadata("Author", authorString, recordPath)
-- DOI
set doiString to my extractDOI(chosenItem)
my setPDFMetadata("Subject", doiString, recordPath)
add custom meta data doiString for "doi" to theRecord
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
end try
end tell
on extractDate(record_item)
try
set [iyear, imonth, iday] to item 1 of |date-parts| of published of record_item
on error
set [iyear, imonth, iday] to item 1 of |date-parts| of created of record_item
end try
set dateString to ((iday as string) & "-" & imonth as string) & "-" & iyear as string
return dateString
end extractDate
on extractDOI(record_item)
try
set doi_entries to doi of record_item
on error
set doi_entries to "missing"
end try
set doiString to doi_entries
return doiString
end extractDOI
on extractAuthor(record_item)
set firstAuthor to ""
try
set author_entries to author of record_item
on error
return ""
end try
repeat with a from 1 to length of author_entries
set this_entry to item a of author_entries
if sequence of this_entry is equal to "first" then
set firstAuthor to this_entry
end if
end repeat
if firstAuthor is equal to "" then
return ""
end if
try
set authorString to |given| of firstAuthor & " " & family of firstAuthor
on error
try
set authorString to family of firstAuthor
on error
set authorString to |name| of firstAuthor
end try
end try
return authorString
end extractAuthor
on setPDFMetadata(mdKey, mdValue, recordPath)
set shellCommand to "/opt/homebrew/bin/exiftool -overwrite_original -" & mdKey & "=" & quoted form of mdValue & " " & quoted form of recordPath
set exifresult to do shell script shellCommand
end setPDFMetadata
Glad you got it worked out.
Just an update –
I’ve added the DOI URL in the ‘URL’ field, and stored a formatted citation in the finder comments; see the script for details.
I’ve also changed the User-Agent header to be friendlier to crossref.org (as they request); as a result, updating should make lookups faster and more reliable.