PreNote: I am open and hungry for any information, advice, tip etc.
Hello Everyone!
I am trying to create automation with applescript. This is my first personal applescript task but I have some valuable questions. Basically I am trying to catch live notifications from a website and display them in mac os notification.
I am trying to build process for a few days but I don't want to give a mess to you :) so I have roughly explained my process below.
(* Variables used in whole process
set $webToCheck > This is Safari webpage which I want to run my script on it. It won't be front window, script should be run with its name or other property.
set $theClass > This is class name of DOM element to check if it is exist or not. This class is not always exist on DOM of $webpage. It comes with notifications so when use it in "do Javascript" I got error "variable is not defined"
set $num > number of class to use in "do Javascript"
set $input > variable to assign HTML text
set $modifiedInput > Text of input seperated from HTML tags
*)
-- Step 1
tell application "Safari"
work on $webToCheck
-- Step 2
repeat until $input is not empty
set input do Javascript
document.getElementsByClassName > $theClass, $num of $webToCheck
end repeat
-- Step 3
modify text of $input to seperate from RAW HTML -- For example: <a class="" value=""> TEXT to be seperated </a>
Display notification $modifiedInput
-- Step 4
Go back to step 1 or 2 to check and display notification again
First of all, here are some general tips though:
Applescript won't accept $ at the start of variable names.
The variable assignment you are looking for is set {variable} to {value}. You can optionally at the end of it clarify the variable's class using as {class} at the end of the assignment.
Focusing a certain website does not happen with work on {URL} but as with most object oriented things in Applescript with the tell-statement. It will be shown in the full solution.
Text concatenation in Applescript happens with &. So something like "Hello " & "World" is the standard way to do it.
Modification of most things in Applescript happens with set.
It is easier to use innerText instead of innerHTML as splitting text in Applescript is a bit of a pain.
There is no goto but you could wrap the first few steps into a function, which are declared with on or to in Applescript.
Here is the full code with some documentation sprinkled in there:
global webToCheck, theClass, num, input --This says that all variables can be used even in functions.
set webToCheck to "youtube.com" --Strings can only use double quotes.
set theClass to "style-scope yt-alert-with-actions-renderer" --I will use an actual demo to prove that it is working
set num to 0 as integer -- This is the type declaration I was talking about. For numbers we have integer, real(float) and number.
set input to "" -- You don't have define everything at the top, but I will do so for this.
on displayNotification()
tell application "Safari"
tell window 1 -- This will target only the first window. For multiple windows you would have to write a repeat with-loop(for-loop), which I'm not going to do, for the sake of simplicity.
tell (first tab whose URL contains webToCheck) -- This targets just the first tab which contains the webToCheck variable.
set input to do JavaScript "document.getElementsByClassName('" & theClass & "')[" & num & "].innerText" -- This is the way I would go about writing the Javascript. I think you had something different in mind, but this works for my example.
display notification (paragraph 1 of input) with title webToCheck -- This displays the first line of my input since that is the relevant part. I also set a title so I doesn't just say "Script Editor"
end tell
end tell
end tell
end displayNotification
repeat 4 times -- I think this is quite obvious. Adjust this to your needs.
displayNotification()
delay 4
end repeat
Running this while having not used youtube on Safari in a while it displays this:
Note that this isn't the most elegant solution, but I think it is readable and it (hopefully) works for your needs.
Related
Trying to get a single bit of information from this website (https://songbpm.com/one-of-these-nights-eagles) as part of an AppleScript to autofill the BPM on each of my tracks in iTunes. The script takes the track, opens up the webpage, and now I just need to get the first BPM listed (in this case 110) and send it back to my script.
How do I do this?
Figured it out using this bit of code, thanks entirely to http://www.cubemg.com/how-to-extract-information-from-a-website-using-applescript/:
to getInput(theClass, num)
tell application "Safari"
set input to do JavaScript "document.getElementsByClassName('" & theClass & "')[" & num & "].innerHTML;" in document 1 -- uses JavaScript to set the variable input to the information we want
return input
end tell
end getInput
set urlBPM to getInput("title", 3)
Is there a way to retrieve result of an Automator app script in an external Applescript app (not the Applescript lines in Automator)?
Something like:
tell application "My_Automator_App"
-- suppose My_Automator_App checks the Calendar to see if there some events today
-- "Show Result" in Automator will display a list
get the_Result -- list returned by Automator
end tell
I looked into this a little bit and didn't find a natural means by which AppleScript and Automator applets can communicate, although this doesn't mean one definitely doesn't exist.
In the meantime, you could implement one of a couple of workarounds/hacks that, although a little unseemly in their methods, do achieve the desired result without creating any side issues that would affect the functionality of an applet itself.
1. Use The Clipboard
Append a Copy to Clipboard action at the end of the applet's workflow, or following the action whose result you would wish to be reported.
Retrieving the clipboard from AppleScript is simple:
get the clipboard
This will probably suit return values that are simple text strings or a number. Passing an array of items from an Automator action to the clipboard isn't very reliable, sometimes only allowing access to the first item. However, this can be resolved with a small AppleScript within the workflow to process results arrays properly and convert them into an accessible format, e.g. a comma-delimited string.
However, the clipboard is also capable of storing image data, file references, and other data types, so it will be possible (if not always straightforward) to send those to be retrieved in an AppleScript.
Where possible, strings and numbers are the safest storage types.
2. Write Out To A Temporary File
To avoid using the clipboard as an intermediary, or if you wish the applet to report multiple variables without too much work, then writing the data to a temporary file is a fairly common practice, such as is done in shell scripts when persistant values are needed between multiple executions of the same script.
There's actually a special directory that gets periodically purged so that temporary data files don't accumulate: /tmp. It's hidden in Finder, but you can still create files and delete them as you would any other directory. Files that aren't access for 3 days get purged by the system.
There is a New Text File action that can write text to a file:
Specifying the /tmp directory is most easily done by creating a variable whose value is "/tmp" (without the quotes), and dragging that variable onto the appropriate field.
But my inclination would be to insert an AppleScript, or more suitably, a shell script into the workflow, with which file manipulation becomes easy and more capable.
Calendar Events Example
Using a similar example to the scenario you described, a simple applet that retrieves calendar events might have a workflow that looks like this:
where you can calibrate the first action to isolate the events you want, such as today's events. That action returns a type of object that isn't easily processed by AppleScript, but the second action extracts the relevant data in text format, summarising the list of events that the first action returned.
This is where a temporary file is useful to write out the data to a text file, which can then be retrieved in an AppleScript.
Given this Automator applet saved under the named "CalEvents", this AppleScript makes use of that applet and its result:
property tidEvents : [linefeed, linefeed, "EVENT", space] as text
property tidDetails : {tab, " to "}
property tid : a reference to my text item delimiters
run application id "com.apple.automator.CalEvents"
set tid's contents to tidEvents
set EventsSummary to read POSIX file "/tmp/EventsSummary.txt"
set EventsList to the EventsSummary's text items
set [[n], EventsList] to [it, rest] of EventsList
set n to n's last word as number
EventsList -- The final list of events from first to last
Upon its first run, the applet requires consent to access your calendar information, which only needs to be done once and will make the above script appear to fail. Once authorised, you can run the script as often as you like to get the most up-to-date content of the /tmp/EventsSummary.txt file.
Each item in the list variable EventsList is a block of text that looks like this (asterisks are my redactions for privacy, as are the address items in curly braces):
4 OF 8
Summary: GP Appointment
Status: none
Date: 07/12/2017 to 07/12/2017
Time: 14:45:00 to 15:45:00
Location: ******** Medical Centre
{Address Line 1}
{Address Line 2}
{County}
{Post Code}
United Kingdom
Notes: 01*** *****9
Each value is separated from the preceding colon by a tab character, which won't be obvious here. Also, as you can tell from the date format and address, these are British-formatted values, but yours will, of course, be whatever they are set as in Calendar.
But since each list item is much the same, extracting details for a particular event will be simple in AppleScript, first by splitting a particular event item into paragraphs, and then splitting a particular paragraph by either a tab or space character (or both) or some preposition that naturally delimits useful bits of text:
set |Event| to some item in the EventsList
set tid's contents to tidDetails
set EventDetails to {title:text item 2 of paragraph 2 ¬
, startTime:text item 2 of paragraph 5 ¬
, EndTime:text item 3 of paragraph 5} of the |Event|
which places the important event details, such as its name and start/end times, in an AppleScript record:
{title:"GP Appointment", startTime:"15:45:00", EndTime:"16:00:00"}
I am trying to create an AppleScript that can find text on a webpage and tell me the amount of matches I received(Command + F).
I already know how to do the "Find" part:
tell application "System Events"
delay 0.5
keystroke "f" using {command down}
end tell
However, I do not know how to interpret these results, such as tell me whether there is a match, or how many matches I have.
Is there any way to do this?(If it seems a bit vague, I can be more specific)
Thanks!
I agree with #user3439894 and his sentiments about using UI scripting (that is—in this case—getting System Events to issue mouse clicks and keypresses on your behalf). Although it has its uses in other areas, it's by far and away my personal least favourite method to achieve a goal, and one of last resort.
Two very quick reasons why it can be a fragile implementation is: 1) the CmdF shortcut used to initiate the Find... menu command could change, either by your own doing, or if it were to be overridden by a systemwide shortcut that supersedes Safari's claim to it (in fact, for this reason, I would personally trigger the Find... command via the menu bar, which System Events can click on your behalf. Menu items tend not to change like shortcuts, unless regional language settings do); and 2) if Safari loses focus during the time the keypresses are issued and the search is initiated, it messes up the whole command flow in your script, and will at best give you no results, but more likely, throw an error in a later part of the script.
I'm going to demonstrate two alternative methods of searching a Safari webpage for a piece of text, and obtaining the number of times it matches throughout the whole document.
1. Safari's do JavaScript AppleScript command
Most modern web browsers have the ability to run JavaScript code inside their tabs, and Safari can too. AppleScript can take command of this very useful function, provided you give it permission to do so by going into the Develop menu of Safari and ticking Allow JavaScript from Apple Events and Allow Remote Automation (the latter of which will already be on, I'm guessing). There's another menu item called Allow JavaScript from Smart Search Field—I would advise you keep this one off, otherwise it could potentially allow naughty websites to issue commands to your computer and cause mischief.
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
try
set n to (do JavaScript ¬
"document" & ¬
".body" & ¬
".innerText" & ¬
".match(/" & s & "/ig)" & ¬
".length;" in D) as integer -- the number of matches
on error
set n to 0
end try
To break this down: s is the search string that you would otherwise be typing into the search box. It is fed into a JavaScript command, that has the following components:
document: a reference to the Safari webpage document;
body: the body of the document, as opposed to, say, the header or the footer. It's the main bulk of the webpage that users see in front of them;
innerText: the text contained within the body of the document, free of any HTML formatting, but preserving whitespace;
match(): a method or function in JavaScript where the search string s is used to perform a search within the innerText and return an array listing all of the matches;
length: a property of the array returned by match() that reports how many elements is contains, and this equates to the number of matches found during the search.
It's all one command, which, written in full on a single line, looks like this (using the search string "AppleScript"):
document.body.innerText.match(/AppleScript/ig).length;
It returns a number, which is stored in the variable n, and that's it.
This is my favourite method that I would elect to use myself, as it's unlikely to break, and it's nice and fast.
I should point out that match() actually searches and matches using a Regular Expression. I won't go into them right now, but it means that the search string s will need to be a little careful if using any special characters:
\ [ ] { } ^ $ . | ? * + ( )
All you need to be aware of is that, if your search string uses any of these characters, you should precede it with a double-backslash \\. So, for example, if I wanted to search for "matches I received(Command + F)" (which uses (, ) and +), then I would declare my variable s as:
set s to "matches I received\\(Command \\+ F\\)"
2. Chop & Measure
This method is useful if you don't wish to enable Remote JavaScript in your browser, or simply want something that's straightforward to remember and implement off the top of your head next time.
It's simple text manipulation, using AppleScript's text item delimiters and a bit of counting:
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
set T to text of D -- the webpage text content
set l to {initialValue:length of T, finalValue:missing value}
set the text item delimiters to s
set T to text items of T
set the text item delimiters to ""
set T to T as text
set l's finalValue to length of T
set |𝚫l| to (l's initialValue) - (l's finalValue)
set n to |𝚫l| / (length of s)
Safari has a useful AppleScript property called text, which refers to the text content of the specified document or tab (it also has another property called source that contains the HTML source of the document or tab).
Here's the breakdown:
The value of Safari's text property—which is the text content of the webpage—is stored in a variable, T;
The length of T is read and stored. This equates to the number of characters on the whole webpage;
The text item delimiters are set to the search string, s, (which does not need to worry about special characters, so don't insert unnecessary backslashes in this one). The text item delimiters basically erase all occurrences of s from within T;
Then the length of T is read again. If s found any matches in T, it means that the length of T—the number of characters—will have reduced;
It will have reduced by the number of characters in s for each match that occurred. Therefore, turning the equation round a bit, the number of matches, n, is equal to the change in length of T divided by the length of s.
There are other ways to search a webpage with AppleScript, JavaScript, bash, etc., but I think these two serve as reasonable examples of how to achieve the same goal using very different methods. I refer to them as examples, because you might need to make small adjustments to the script to cater for your own needs, such as inserting backslashes where necessary in the first example, or considering in the second how you'd handle the situation if you set s to be an empty string "" (it will throw an error, but this is easily managed).
They also both return real values for n, i.e. 11.0. It's easy to see why in the second example, but I assume it's just a type conversion between JavaScript and AppleScript in the first example (I don't know). Therefore, purely for neatness, I would then coerce the returned value into an integer as I did in the first one, so it reads 11 instead of 11.0:
set n to (...) as integer
but you don't have to.
First of all I must say that UI Scripting can be messy and unreliable. I'd suggest you find a different way to accomplish whatever the real goal is.
That said, using Safari in macOS High Sierra 10.13.3 set to this web page, the following example AppleScript code will set the variable theSearchResult to the result of the search for the word "vague":
tell application "Safari" to activate
delay 0.5
tell application "System Events"
keystroke "f" using command down
delay 0.2
keystroke "vague"
delay 0.2
set theSearchResult to (value of static text 1 of group 2 of tab group 1 of splitter group 1 of window 1 of application process "Safari")
delay 0.2
key code 53 -- # Esc
end tell
return theSearchResult
Result:
"1 match"
Note that the value of the delay commands may need to be adjusted for your system, and or additional delay commands may or may not be needed. Adjust values of and or add/remove the delay commands as appropriate.
The search result can be one of the following, Not found or an integer followed by the word match, e.g. 1 match, and possibly something else, not sure as I've not done extensive testing.
How you want to interpret the result is up to you. You could use a if statement on the theSearchResult, e.g.:
if theSearchResult contains "Not found" then
-- # Do something.
-- # Your code goes here.
else
-- # Do something else.
-- # Your code goes here
end if
Another factor to consider is how is it being searched, i.e. Starts With or Contains. I believe the default in for Safari in macOS High Sierra 10.13.3 is Starts With.
Note: The example AppleScript code is just that and does not employ any error handling and is meant only to show one of many ways to accomplish a task. The onus is always upon the User to add/use appropriate error handling as needed/wanted.
thanks in advance for your help.
New to Applescript.
Trying to open in Preview all files in folder and save them.
The problem is that saving pops up a dialog half the time, which requires me to sit there hitting enter.
Code:
tell application "Finder"
set fl to files of folder POSIX file "/Users/myname/Desktop/myfolder/" as alias list
end tell
repeat with f in fl
tell application "Preview"
activate
open f
# Trying to save before the window has appeared will fail.
# Note: - This assumes that NO window was initially open.
# - The code should be made more robust to eventually time out.
repeat until (count of windows) > 0
delay 0.3
end repeat
save front document
close front document
end tell
end repeat
Thank you for the help
I made several PDF files and download from different sites and I get sample of version 1.3, 1.4, 1.5, 1.7, ..but no 1.6 ! For all of them no issue.
Anyway, because I have not been able to reproduce what you have, I took different approach.
1) I have added at top of the script, a list of encoding/version list which may required special treatment (like hit return key). You can of course amend these 2 lists to manage other cases you may have.
2) I changed your script to allow the script to get encoding and version values of the pdf. This is done by using spotlight data base via shell command 'mdls'. I use it 2 times to get version and encoding characteristics. The shell command returns characters before and after the value we want to get, so I use text x thru Y to extract the encoding and the version itself.
3) if PDF version/encoding are in the list predefined which requires special treatment, then I set OKReturn to true.
4) Just after the save instruction, the script now test if OKReturn is true. then I ask the script to hit return key for you. you may have to adjust this part, for instance, it could be not only 1 return, but 2 or something else. this is something I have not been able to test, because all my pdf are working. please keep in mind that because I simulate return key, you should not use the keyboard during the script run.
based on my test, I don't think the encoding is the blocking criteria. I think the version 1.6 is.Here is the script. It includes comment to make you able to adjust it:
set CodingReturn to {"Mac OS X 10.7.3 Quartz PDFContext"}
set VersionReturn to {"1.6"}
set myFolder to choose folder
tell application "Finder"
set fl to files of myFolder as alias list
end tell
repeat with f in fl
set FVersion to do shell script "mdls -name kMDItemVersion " & quoted form of POSIX path of f
set FEncoding to do shell script "mdls -name kMDItemEncodingApplications " & quoted form of POSIX path of f
if (length of FVersion) > 21 then set FVersion to text 19 thru -2 of FVersion -- extract only version number
if (length of FEncoding) > 42 then set FEncoding to text 38 thru -4 of FEncoding -- extract only the coding
set OKReturn to ((FVersion is in VersionReturn) and (FEncoding is in CodingReturn)) -- special treatment true/false
tell application "Preview"
activate
open f
repeat until (count of windows) > 0
delay 0.3
end repeat
save front document
if OKReturn then -- we need special key to be pressed
tell application "System Events" to keystroke return
end if
close front document
end tell
end repeat
I would be very interested to get your feedback about this version.
To save without the "save as" dialog popping up, add "in f". Here "f" was the filename. You could also put in a different path.
save front document in f
I have an filemaker Pro 13 database with around 120 records (it's for a conference). I want to team it up with BBEdit to create individual files for each abstract, thus applescript. Much to my surprise (and despite a lot of web tips on scripting) '[tag:current record]' is not recognised in the script.
The relevant bit is this:
FM Script:
Loop
Perform Applescript
tell application "FileMaker Pro"
activate
set MyFileName to cell "WebAbstractFileName" of table "SelectionProcess"
set MyWebAbstract to cell "WebAbstract" of table "SelectionProcess" as text
end tell
-- (BBEdit bit, which works fine in testing)
Go to Next Record (exit after last)
End Loop
This works fine if I only want to retrieve the first record!
This applescript is set within a filemaker script which loops through the records but the script doesn't care which record it's in.
I've tried adding 'of current record' before the table reference but it then gives me errors (eg error "FileMaker Pro got an error: Object not found." number -1728 from cell "WebAbstractFileName" of current record of table "SelectionProcess") Without 'current record' it works fine, but only gives me the first record.
Here's (roughly) how you could do this in a Filemaker script:
Go to Layout [ “YourTable” ]
# FIND THE RECORDS OF INTEREST
Perform Find [ Restore ]
Go to Record/Request/Page [ First ]
Loop
New Window [ ]
# ISOLATE THE CURRENT RECORD
Show All Records
Omit Record
Show Omitted Only
Set Variable [ $path; Value:Get ( DocumentsPath ) & "someFolder/" & YourTable::Somefield & ".html" ]
Export Records [ No dialog; “$path” ]
Close Window [ Current Window ]
Go to Record/Request/Page [ Next; Exit after last ]
End Loop
This will export every record in the found set as an individual file into the folder "someFolder" located in the user's Documents folder, using the contents of the YourTable::Somefield field as the filename.
If, as you say, you don't need the separate files, then of course this can be much simpler.
More tinkering solved this. The crucial bit was to change the syntax. The script now reads:
tell application "FileMaker Pro"
activate
tell current record to set MyFileName to cell "WebAbstractFileName"
tell current record to set MyWebAbstract to cell "WebAbstract"
end tell
What seems to happen is that the fields must be visible (yet I had that not be a problem at one point...go figure. If they're visible, you can drop the table specification). This script, wrapped in a Loop block, will act on the found set and (if instructed) exit after the last record.
I append the bbedit script to create a new file and save it with a variable taken from another field in case it's of interest.
tell application "BBEdit"
set notesPath to ":Users:ophiochos:Dropbox:TL Conference Admin:Webpage materials:Abstracts:"
set newFilePath to notesPath & MyFileName & ".html"
set newDoc to make new text document with properties {contents:MyWebAbstract}
tell newDoc
set source language to "HTML"
save to newFilePath
close window
end tell
end tell
Or you could simply create a calculated field that contains the contents of the desired export file for each record, loop through the records one by one, and just use an Export Field Contents ['YourCalculatedField_c'] script step.
You can also use FM to preview the html output by using a web viewer to display your html. This, along with exporting individually, you can get all this functionality without the need for an external program.
If you need to change the text encoding for output files, you can also specify those in an xslt for outputting files:
http://filemakerhacks.com/2012/09/23/export-field-contents-as-utf-8/
Also, if performing applescript from within FileMaker, the "tell application" line is not needed since it is implied.