Skip to main content

Get and manipulate the contents of a page in Safari with "do JavaScript"

Here’s an AppleScript which runs document.querySelector in a Safari tab and returns the matching HTML:

tell application "Safari"
    tell document 1
        get (do JavaScript "document.querySelector(\"main\").innerHTML")
    end tell
end tell

You need that .innerHTML property

If you try to run the AppleScript without the .innerHTML property, e.g.

set selectedText to (do JavaScript "document.querySelector(\"main\")")

You get an error further down in the script:

The variable selectedText is not defined.

I suspect this is because document.querySelector returns some sort of DOM object which AppleScript doesn’t know how to store in a variable, whereas .innerHTML returns a string that it can store quite happily.

Use case #1: Get the contents of tweets I have open in Safari

I wanted a way to programatically get the contents of tweets I had open in Safari. This is the AppleScript I used:

tell application "Safari"
    tell document 1
        get (do JavaScript "document.querySelector('article').innerHTML")
    end tell
end tell

Alternatives that didn’t work:

But the rendered HTML is available to JavaScript running on the page, so we can exfilitrate it this way.

Use case #2: Move videos between YouTube accounts

I had two YouTube accounts, and I wanted to move videos from the “Watch Later” playlist in one to the other. I opened the two accounts in different windows, and then I repeatedly ran this AppleScript:

tell application "Safari"
    -- Get the path to the video at the top of the playlist in window 1
    -- e.g. /watch?v=123456789
    tell tab 1 of window 1
        set theYouTubeUrl to do JavaScript "document.querySelector('a#video-title.ytd-playlist-video-renderer').getAttribute('href')"
    end tell

    tell tab 1 of window 2
        -- Open that video in window 2, and wait for it to load
        set the URL to "https://www.youtube.com" & theYouTubeUrl
        delay 5

        -- Open the actions menu, which contains the "Save" button, and wait
    -- for the menu to open
        do JavaScript "document.querySelector('button[aria-label=\"More actions\"]').click()"
        delay 1

        -- Click the "Save" button, and wait for the next dialog to appear
        do JavaScript "document.querySelectorAll('.ytd-menu-service-item-renderer').forEach(function(r) { if (r.innerHTML === 'Save') { r.click(); } })"
        delay 1

        -- The next dialog will show a list of playlists; click the checkbox
        -- for "Watch Later"
        do JavaScript "document.querySelector('yt-formatted-string[aria-label=\"Watch Later Private\"]').click()"
    end tell

    tell tab 1 of window 1
        -- Open the "Actions" menu in window 1, which includes the "Remove from
    -- Watch Later" button.
        do JavaScript "document.querySelector('.ytd-playlist-video-list-renderer button[aria-label=\"Action menu\"]').click()"
    end tell
end tell

I don’t automatically click the “Remove from Watch Later” button, because this isn’t 100% reliable – sometimes it won’t get to the Save button if the video takes too long to load, or it’s a “YouTube for Kids” video that can’t be saved for later. But it did save me a lot of clicking!