Search code examples
applescriptplist

Applescript: Edit bookmark.plist file with new URL


set some_host to "test.mydomain.com"

try
    -- Ping the host.
    do shell script "ping -c 1 -t 1 " & some_host
    -- Set ipAddr.
    set ipAddr to word 3 of result
    -- Combine the IP with the URL & port.
    set urlAddr to "http://" & ipAddr & ":80"
    display dialog "Connection Successful. " & urlAddr buttons {"OK"} default button 1
on error
    -- if we get here, the ping failed
    display dialog "Conection failed. " & some_host & " is down" buttons {"OK"} default button 1
    return
end try

-- Update the URL with the new IP.
tell application "System Events"
    tell property list file "~/Library/Safari/Bookmarks.plist"
    set value of property list item "key" to text of urlAddr
    end tell
end tell

Here's the error I get. I should mention that "key" would be replaced with the bookmarks name I'm trying to change.

error "System Events got an error: Can’t set property list item \"key\" of property list file \"~/Library/Safari/Bookmarks.plist\" to \"http://000.000.000.000:80\"." number -10006 from property list item "key" of property list file "~/Library/Safari/Bookmarks.plist"

Bookmark.plist example: So lets replace "Key" with "NVR Camera"

<dict>
        <key>URIDictionary</key>
        <dict>
            <key>title</key>
            <string>NVR Camera</string>
        </dict>
        <key>URLString</key>
        <string>https://12.123.456.789:88</string>
        <key>WebBookmarkType</key>
        <string>WebBookmarkTypeLeaf</string>
        <key>previewText</key>
        <string>your description</string>
        <key>previewTextIsUserDefined</key>
        <true/>
    </dict>

I'm trying to do this to update the URL automatically to the new IP if it changes. If anyone has a better solution I'm all ears! Thanks in advance!


Solution

  • Property List Data vs. XML Data

    Here's an extract of my Safari bookmarks.plist file:

        <?xml version=\"1.0\" encoding=\"UTF-8\"?>
        <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
        <plist version=\"1.0\">
        <dict>
            <key>Children</key>
            <array>
                <dict>
                    <key>Title</key>
                    <string>History</string>
                    <key>WebBookmarkIdentifier</key>
                    <string>History</string>
                    <key>WebBookmarkType</key>
                    <string>WebBookmarkTypeProxy</string>
                    <key>WebBookmarkUUID</key>
                    <string>92E8B75A-B335-4CA3-8DE9-08A09817792D</string>
                </dict>
                <dict>
                    <key>Children</key>
                    <array>
                        <dict>
                            <key>ReadingListNonSync</key>
                            <dict>
                                <key>neverFetchMetadata</key>
                                <false/>
    

    That's just the first 24 lines, in which a tag with the label <key> has cropped up 8 times--roughly once every three lines. You can imagine why AppleScript would get a bit confused when told to fetch the tag called "key" when there are so many.

    Having said that, that isn't why your script threw the error. I ran this command in Script Editor:

        tell application "System Events" to ¬
            tell the property list file ¬
                "~/Library/Safari/Bookmarks.copy.plist" to ¬
                get every property list item whose name is "key"
    

    (I'm using a copy of my bookmarks.plist file, because it's not out of AppleScript's capabilities to completely screw up any .plist file it reads from or writes to.)

    The output from that command was: {}, which tells us that there are no property list items labelled key.

    Although the plist file looks like XML, the way the data are handled by AppleScript is different to that of an XML file. With an XML file, you reference elements using the name of its tag, such as <key>. With a property list file, elements are referenced in key/value pairs, so that, where a <key> tag in XML data would have a value denoted by the name "key", for a plist, it will contain the name of the property whose value will immediately follow.

    For example:

        <key>Title</key>
        <string>History</string>
    

    With XML data in AppleScript, we'd have two XML elements: one called "key" with the value "Title"; and the other called "string", with the value "History". With plist data, we have but a single property list key/value pair (item), with the name "Title" whose value is "History".

    Going back to my initial point that there are, indeed, multiple tags with the same name; there are, likewise, multiple key/value pairs with the same key, and quite possibly, the same value.

    Therefore, whether you handle the data as XML or as a property list, it's always necessary to tell AppleScript which level of the property list tree you are referencing.

    AppleScripting

    Which level of the hierarchy to reference depends entirely on where you want to store the bookmark: in a folder of its own; in the favourites folder or another folder that already exists; or just in the top-level of the bookmarks collection, so that it appears immediately in the list when you open the bookmarks side-bar.

    For ease, I'll demonstrate the last option: adding a bookmark to the top-level of the folder tree. I've marked below where in the plist file new entries are to be inserted:

        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        <plist version="1.0">
        <dict>
            <key>Children</key>
            <array>
                <dict>...</dict>
                <dict>...</dict>
                <dict>...</dict>
                <!-- etc... //-->
                <dict>...</dict>
                <!-- insert new entry here //-->
            </array>
            <!-- other stuff //-->
        </dict>
        </plist>
    

    If you run this command:

        tell application "System Events" to ¬
            tell the property list file ¬
                "~/Library/Safari/Bookmarks.copy.plist" to ¬
                get the properties of the last property list item of ¬
                    the property list item "Children"
    

    AppleScript will return a record, which is its (lesser) attempt at storing data as key/value pairs. It is basically an AppleScript list, where each entry in the list has a label that is used to reference the item instead of an index number. The output returns looks like this (formatted for ease of reading):

         {value:{URIDictionary: ¬
             {title:"website.com"}, ¬
              previewText:"The title of the website", ¬
              ReadingListNonSync:{neverFetchMetadata:false}, ¬
              Sync:{ServerID:"DAV-DB576E16-E590-44D3-839F-63C0A1A8D3BC", ¬
                   |data|:«a whole lot of data»}, ¬
              WebBookmarkUUID:"A5E4774A-2C26-4AD1-9938-D192C26860B8", ¬
              URLString:"https://website.com/path/etc", ¬
              WebBookmarkType:"WebBookmarkTypeLeaf", ¬
              previewTextIsUserDefined:true}, ¬
          kind:record, ¬
          class:property list item, ¬
          name:missing value, ¬
          text:"The XML representation of the property list data."}
    

    This tells me what items make up a single bookmark entry in the plist file. Bear in mind that, from AppleScript's point of view, each of those items is itself a property list item, so will need to be created individually as such. I'm going to assume we can ignore WebBookmarkUUID and sync, both of which contain data we couldn't possibly generate ourselves. The rest of them are easy, though.

    Creating a New Bookmark Entry

        set urlAddr to "http://000.000.000.000:80"
    
        tell application "System Events" to tell ¬
            the property list file "~/Library/Safari/Bookmarks.plist" to tell ¬
            the property list item "Children" to tell ¬
            (make new property list item ¬
                at end of property list items ¬
                with properties {kind:record})
    
            make new property list item with properties ¬
                {name:"URIDictionary", kind:record}
    
            tell the result to make new property list item ¬
                with properties {name:"title", value:"mydomain.com", kind:string}
    
            make new property list item with properties ¬
                {name:"previewText", value:"Information about mydomain.com", kind:string}
    
            make new property list item with properties ¬
                {name:"URLString", value:urlAddr, kind:string}
    
            make new property list item with properties ¬
                {name:"WebBookmarkType", value:"WebBookmarkTypeLeaf", kind:string}
    
            make new property list item with properties ¬
                {name:"previewTextIsUserDefined", value:true, kind:boolean}
    
            make new property list item with properties ¬
                {name:"_tag", value:"AppleScript", kind:string}
        end tell
    
        return the result
    

    Running this on my system successfully created a new bookmark entry called "mydomain.com", which, when clicked, tried to take me to the URL "http://000.000.000.000" (as per the dummy value of your urlAddr variable.

    Updating the Bookmark

    To update it each time you run your script, all you have to do is obtain a reference to the bookmark, and then a reference to the property list item that contains the information about the URL to which the bookmark points.

    You'll notice that, above, I created a property list item called _tag, which the other bookmark entries do not have. I did this so it would be easy to get the reference to bookmark by simply searching for the one that contains _tag:

        tell application "System Events" to tell ¬
            the property list file "~/Library/Safari/Bookmarks.copy.plist" to tell ¬
            the property list item "Children" to ¬
            set bookmark to ¬
                the first property list item whose ¬
                name of property list items contains "_tag"
    

    With the bookmark reference stored in the variable bookmark, updating the URL to which it points is done like this:

        tell application "System Events" to tell ¬
            the property list file "~/Library/Safari/Bookmarks.plist" to tell ¬
            the property list item "Children"
    
            set bookmark to ¬
                the first property list item whose ¬
                name of property list items contains "_tag"
    
            set URLString to the first property list item ¬
                of bookmark whose name is "URLString"
    
            set the value of URLString to urlAddr
        end tell
    

    UPDATE:

    Upon further testing and going back in to check the bookmarks.plist file following the changes committed through AppleScript, it emerges that Safari automatically removes the user-defined key _tag, which is a shame. Conversely, it inserts WebBookmarkUUID and generates the value for it, which is great.

    So, unfortunately, we can't use the method described above to tag our bookmark entry for easy referencing later.

    This new script is, therefore, longer by necessity. It's designed to replace the part of your script that starts with tell application "System Events".

    It will first check to see if a bookmark matching the specified title exists at the top level of the bookmarks hierarchy; if not, it will create it; if it does, it will update the address to which it points with whatever is contained in the variable urlAddr.

        set urlAddr to "http://google.com"
        set myTitle to "mydomain.com"
        set plistFile to "~/Library/Safari/Bookmarks.plist"
    
    
        tell application "System Events"
            tell ¬
                the property list file plistFile to tell ¬
                the property list item "Children" to ¬
                set top_level to a reference to it
    
    
            set bookmarks to a reference to ¬
                (property list items of the top_level ¬
                    whose name of property list items ¬
                    contains "URLString")
    
            set labels to ¬
                a reference to property list item "title" of ¬
                    property list item "URIDictionary" of ¬
                    property list items of ¬
                    the bookmarks
    
    
            if the value of the result ¬
                does not contain myTitle then ¬
                return saveBookmark of me ¬
                    for urlAddr ¬
                    to plistFile ¬
                    at a reference to property list items of the top_level ¬
                    given |title|:myTitle, previewText:"your description"
    
    
            repeat with i from the number of bookmarks to 1 by -1
    
                if item i of ¬
                    the value of the labels is ¬
                    myTitle then ¬
                    exit repeat
    
            end repeat
    
            set bookmark to item i of bookmarks
            set the value of ¬
                property list item "URLString" of ¬
                the bookmark to ¬
                the urlAddr
        end tell
    
    
        to saveBookmark for www as string ¬
            at locationRef ¬
            to plist as string : "~/Library/Safari/Bookmarks.plist" given title:T ¬
            as string, previewText:_t as string : ""
    
            local www, locationRef, plist
            local T, _t
    
    
            tell application "System Events" to tell the property list file plist
    
                tell (make new property list item at end of locationRef ¬
                    with properties {kind:record})
    
                    tell (make new property list item with properties ¬
                        {name:"URIDictionary", kind:record}) to ¬
                        make new property list item with properties ¬
                            {name:"title", value:T, kind:string}
    
                    make new property list item with properties ¬
                        {name:"previewText", value:_t}
    
                    make new property list item with properties ¬
                        {name:"URLString", value:www}
    
                    make new property list item with properties ¬
                        {name:"WebBookmarkType", value:"WebBookmarkTypeLeaf"}
    
                    make new property list item with properties ¬
                        {name:"previewTextIsUserDefined", value:true}
    
                end tell
            end tell
        end saveBookmark
    

    If your bookmark exists elsewhere in the bookmarks hierarchy (such as in a folder), it currently won't be found, and a new bookmark will be created a the top level. I will leave you to use what I've demonstrated in this example script and perhaps implement a deeper traversal of the bookmarks folder tree.

    If you have any questions or problems, leave a comment and I'll get back to you.