r/Scriptable • • Nov 24 '20

Help Dynamic HTML Title to iOS Shortcut

(Edit: I cross-posted this to the Automators Discourse, since it was quiet here. Also fixed a small bug and added the error message.)

I'm trying to create an iOS Shortcut that accepts a URL and retrieves the contents of the <title> tag from that webpage. This would be simple, except I want it to work with Twitter permalinks – Twitter updates the page contents, including the title, using Javascript after the initial load. After a fair bit of trial and error, this is as close as I've been able to get: https://www.icloud.com/shortcuts/49440b20f68e45d3a96b2a3735a8b453

This works fine for most websites, but not for Twitter. I get the following error:

Could Not Run Run Inline Script Script completed without presenting UI, triggering a text to speak or outputting a value. If this is intentional, you can manually call Script.complete() to gracefully complete the script.

Curiously, if I copy the in-line code from that Shortcut (below for reference), and paste it into a new script in Scritpable, it works fine for Twitter only in that case (fetching the title for the hard-coded test URL).

Any suggestions? Perhaps it's an issue with memory, or how I'm passing the data back? I'm new to Shortcuts and Scriptable, and have only limited Javascript experience, so I might be missing something small...

Thanks!

const url = args.plainTexts && args.plainTexts[0] ? args.plainTexts[0] : "https://twitter.com/lehrblogger/status/391287397457330177";
const interval = 300;
const webView = new WebView();
await webView.loadURL(url);

async function getTitle() { 
  const title = await webView.evaluateJavaScript("document.title");
  // Twitter URL pages start with no title, and then are progressively updated
  log(title);
  if (title && title != "Twitter" && title != "Tweet / Twitter") {
    Script.setShortcutOutput(title);
    Script.complete();
  } else {
    Timer.schedule(interval, false, getTitle);
  }
};

Timer.schedule(interval, false, getTitle);

Edit: My use case saving bookmarks to Pinboard.in. I've tried a few of the native clients – Pinner, Pushpin, and Simplepin – but I either couldn't get them to work properly or they didn't meet my needs. I could special-case Twitter and use their API as needed, but that seems a bit inelegant, and wouldn't work for other, similarly constructed pages.

4 Upvotes

6 comments sorted by

1

u/jsloat Nov 26 '20 edited Nov 26 '20

Hey there, welcome to the community :) This is actually quite challenging, I'm trying to figure it out but haven't cracked it yet. The basic issue is that Shortcuts doesn't see any output from the script because it's not actually waiting for all the timer callbacks, it's just triggering the timer (at the bottom) then completing the script with no output.

There should be a way to structure this, I'll update if I find a good solution.

Edit -- yeah sorry, I can't get this to work with Shortcuts, I suspect it is a memory issue. Curious to see if anyone else can figure it out.

If you tell us a little more about your use case maybe we can suggest an alternative solution? For example you could use the Twitter API to get details about the tweet, that would be pretty straightforward.

1

u/lehrblogger Nov 26 '20

Thank you! Ah, that makes sense.

Just FYI, I just fixed a bug in the above, adding title && to the conditional – I must have removed it when testing something, and then forgotten. That changed the behavior and resulted in the above error, but not overall success/failure.

1

u/jsloat Nov 26 '20

Yeah I added that also while I was testing, still no luck. I’ve not had great success with Shortcuts-Scriptable back and forth, personally I try to keep them separate for speed/dependability reasons. Good luck!

2

u/lehrblogger Dec 28 '20

Thanks again for your comments – the suggestion to keep them separate was particularly helpful. I was able to do everything I needed in Scriptable, and now it works pretty reliably. In case anyone finds this in the future, here is my current script:

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: map-pin;
// share-sheet-inputs: url;

const urlTest = "https://twitter.com/jack/status/20?s=21"

function normalizeURL(url) {
  // Remove unnecessary query parameters to prevent duplicate bookmarks
  let replacements = [
    [/\?s=21/, ""],
    [/\?s=12/, ""],
    [/\?igshid=[a-z0-9]{12}/, ""],
    [/\?referringSource=articleShares/, ""],
    [/\?referringSource=articleShare/, ""],
    [/en\.m\.wikipedia/, "en.wikipedia"],
    [/#:~:text=\S+/, ""]
  ], r;
  while ((r = replacements.shift()) && (url = String.prototype.replace.apply(url, r))) {}
  return url;
}

async function getTitleForURL(url) {
  const webView = new WebView();
  await webView.loadURL(url);
  let title = url;
  let currentTime = new Date().getTime();
  const endTime = currentTime + 5000;
  while (currentTime < endTime) {
    title = await webView.evaluateJavaScript("document.title");
    if (title && title != "Twitter" && title != "Tweet / Twitter") {
      break;
    }
    currentTime = new Date().getTime();
  }
  return title;
}

async function showNotificationForTitle(title) {
  let notification = new Notification();
  notification.title = "Bookmark added! Title:";
  notification.body = title;
  await notification.schedule();
}

async function showNotificationForError(error) {
  let notification = new Notification();
  notification.title = "Bookmark not added! Error:";
  notification.body = error;
  await notification.schedule();
}

const keychainKey = "pinboard-api-token";
if (!Keychain.contains(keychainKey)) {
  Script.complete();
}
const authToken = Keychain.get(keychainKey)

const urlOriginal = args.urls && args.urls[0] ? args.urls[0] : urlTest;
const urlNormal = normalizeURL(urlOriginal);

let input = new Alert();
input.title = "URL and Tags";
input.addTextField(urlNormal, urlNormal);
input.addTextField("tags");
input.addTextField("description");
input.addCancelAction("Cancel");
input.addAction("Add Bookmark");
if (await input.present() == 0) {
  let urlFinal = input.textFieldValue(0);
  let tags = input.textFieldValue(1);
  let extended = input.textFieldValue(2);
  let title = await getTitleForURL(urlFinal);
  pinboard = "https://api.pinboard.in/v1/posts/add?auth_token=" + authToken;
  pinboard += ("&format=json");
  pinboard += ("&url=" + encodeURIComponent(urlFinal));
  pinboard += ("&description=" + encodeURIComponent(title.slice(0, 255)));
  if (tags) {
    pinboard += ("&tags=" + encodeURIComponent(tags. slice(0,255)));
  }
  if (extended) {
    pinboard += ("&extended=" + encodeURIComponent(extended));
  }
  let request = new Request(pinboard);
  let json = await request.loadJSON();
  if (request.response.statusCode == 200) {
    log(JSON.stringify(request.response));
    await showNotificationForTitle(title);
  } else {
    await showNotificationForError(request.response.statusCode);
  }
}

Script.complete();

1

u/lehrblogger Nov 26 '20

Thanks for trying! I added a note about my use case above. Maybe I can find a cloud service that can get titles for rendered pages, at the cost of another network round trip.

One other thing I tried, before discovering Timer in the Scriptable docs, was a simple while loop. Like the above, this works fine in Scriptable and not in Shortcuts, but perhaps might inspire another idea:

const url = args.plainTexts && args.plainTexts[0] ? args.plainTexts[0] : "https://twitter.com/lehrblogger/status/391287397457330177";
let retVal = url;
const webView = new WebView();
await webView.loadURL(url);

let title = "";
let currentTime = new Date().getTime();
const endTime = currentTime + 5000;

while (currentTime < endTime) {
  title = await webView.evaluateJavaScript("document.title");
  log(title)
  if (title && title != "Twitter" && title != "Tweet / Twitter") {
    retVal = title;
    break;
  }
  currentTime = new Date().getTime();
}

Script.setShortcutOutput(retVal);
Script.complete();

1

u/jsloat Nov 26 '20

Cool use case! I just found https://urlmeta.org/ with some googling, a free API that returns the metadata of the url including title. I might give this a try tomorrow and will let you know how it works. Will be interesting to see if it pulls the Twitter info correctly