Monday, November 5, 2012

batch moving iCal events from one calendar to another with AppleScript

What with all the iOS devices and the cloud and all I thought I would finally sort out the mess which is our iCal calendars around here and put them all on the cloud so that everyone could have read/write access and everything would be shiny and new and wonderful. That took a little more work than it should have, iCal is currently my least favorite of all Apple's software. Searching on the internet though I see I'm not the only one who has had these problems so I document my solution for anyone who might benefit from it.

I had the calendars hosed on just a webDav directory on my own server but this did not allow read/write access for all family members subscribed. I wanted to move them to my iCloud account and then resubscribe everyone (well, technically my wife is the only other member of everyone ;) my first thought was to just export them through iCal to my disk as .ics files and then re-import them into new calendars created on iCloud.

This did not work. I got the following modal error message which had to be dismissed for EVERY SINGLE event in the file before I could get back to normal.


Access to "some event" in "the new calendar name" in account "name of my account" is not permitted. The server responded: "403" to operation CalDAVWriteEntityQueuableOperation.

This is not particularly helpful and the buttons are not really very apple like or helpful either. After signing out and back in again and recreating my iCloud account I was able to get ONE calendar to import without errors, and then they returned.  Thats an old event, so I thought perhaps it was just complaining that events too far in the past weren't allowed to be imported. So I created manually a small ICS file to test with that only had future events in it, but that failed too.

I resorted to applescript. The dictionary has both a duplicate and a move command one or the other should be able to copy or move the events in the calendars I wanted to change. I can change the events from one to the other one at a time by just changing the "calendar" in the popup for the event. That worked fine, so it was allowing me to add things to the calendar, it just didn't like importing it. Or, as it turned out the duplicate or move command. Both of those tries resulted in the same error and total failure. Finally I resorted to the make new event command which requires a bit more work because I had to create the record of all the old properties one bit at a time. And since it will happily return "no value" or "undefined" for properties that aren't set for that event, but it will not accept those as properties for a new event you have to check to make sure each property exists before adding it to the new record. I did not attempt to preserver attendees or other event properties not in the list below so I dont know how to do that. But with this script you can move all the events from one calendar to another without errors from iCal even where an import fails. I suspect it may have something to do with including the unique ID in the export file. Possibly you cannot specify the UID of a new event, or possibly it really means unique, and not just unique within each individual calendar and that is what is causing the error. I left the UID out of the script below but I did not test with trying to include it.

It is not fast, it takes some time for each event to be created, so be patient.


tell application "Calendar"
--Script to move events from one iCal calendar to another
--James Sentman james@sentman.com 11/4/2012
--
--  I found myself unable to import ical calendars that I had exported from another
--  server. I would get an error from the iCal server for each and every event.
--  I have no idea why, it may be just duplicate UID's or something.
--  The "duplicate" command and the "move" command both of which I tried
--  failed with the same error, but I was able to create a new one with the 
--  info gotten from the old calendar. But that wasn't so easy either as not
--  all events have all information and it would not except "no value" answers
--  so I had to add the if exists portion to build the record for the new event.
--  I did not attempt to move things like attendees or other elements of the event
--  just the data you see below.
--
--  INSTRUCTIONS: change the name of these next 2 variables. MyOldCal is
--  the calendar you want to move events FROM and MyNewCal is the name of the
--  calendar that they will be newly created in.
set MyOldCal to calendar "SCDS"
set MyNewCal to calendar "SCDS iCloud"
-- find out how many events we have to create
-- for some reason I would get an error when it reached "count"
-- which generally means that while the count tells you how many records
-- referencing them by index begins at 0 instead of 1, so start at 0
-- and subtract one.
set EventCount to (count of events in MyOldCal) - 1
repeat with i from 0 to EventCount
--get a reference to the old event in the old calendar
set WorkEvent to event i of MyOldCal
--since the work record with the info is appended instead
--of created fresh each time in one step it is necessary to clear 
--it out each time through the loop.
set WorkRecord to {}
--checking if the various properties exist or not 
--stops an error later if one property was undefined
--it will happily return no value for one of them without
--an error, but it will not then accept that in the make
--statement below. So we only add them to the record
--if they are actually there.
if exists description of WorkEvent then
set TheDescription to description of WorkEvent
set WorkRecord to WorkRecord & {description:TheDescription}
end if
if exists start date of WorkEvent then
set TheStartDate to start date of WorkEvent
set WorkRecord to WorkRecord & {start date:TheStartDate}
end if
if exists end date of WorkEvent then
set TheEndDate to end date of WorkEvent
set WorkRecord to WorkRecord & {end date:TheEndDate}
end if
if exists allday event of WorkEvent then
set TheAllDay to allday event of WorkEvent
set WorkRecord to WorkRecord & {allday event:TheAllDay}
end if
if exists recurrence of WorkEvent then
set TheRecurrence to recurrence of WorkEvent
set WorkRecord to WorkRecord & {recurrence:TheRecurrence}
end if
if exists sequence of WorkEvent then
set TheSequence to sequence of WorkEvent
set WorkRecord to WorkRecord & {sequence:TheSequence}
end if
if exists stamp date of WorkEvent then
set TheStampDate to stamp date of WorkEvent
set WorkRecord to WorkRecord & {stamp date:TheStampDate}
end if
if exists excluded dates of WorkEvent then
set TheExcludedDates to excluded dates of WorkEvent
set WorkRecord to WorkRecord & {excluded dates:TheExcludedDates}
end if
if exists status of WorkEvent then
set TheStatus to status of WorkEvent
set WorkRecord to WorkRecord & {status:TheStatus}
end if
if exists summary of WorkEvent then
set TheSummary to summary of WorkEvent
set WorkRecord to WorkRecord & {summary:TheSummary}
end if
if exists location of WorkEvent then
set TheLocation to location of WorkEvent
set WorkRecord to WorkRecord & {location:TheLocation}
end if
--you can try copying the UID also
--but it occurred to me that this may be the whole problem
--as a UID should be unique, and it would not be so
--if you had tried to create an event and specifying it at the time
--so I commented this out and iCal will happily make a new UID for the 
--new event as it should.
--if exists uid of WorkEvent then
-- set TheUID to uid of WorkEvent
-- copy {uid:TheUID} to the end of WorkRecord
--end if
if exists url of WorkEvent then
set TheURL to url of WorkEvent
set WorkRecord to WorkRecord & {url:TheURL}
end if
--and lastly actually make the new event with the old data.
make new event at MyNewCal with properties WorkRecord
end repeat
return EventCount & " events moved"
end tell

Or you can download the script as a script editor file: icalmover.zip

Let me know if thats of any help to anyone, thanks.
.code { background:#f5f8fa; background-repeat:no-repeat; border: solid #5C7B90; border-width: 1px 1px 1px 20px; color: #000000; font: 13px 'Courier New', Courier, monospace; line-height: 16px; margin: 10px 0 10px 10px; max-height: 200px; min-height: 16px; overflow: auto; padding: 28px 10px 10px; width: 90%; } .code:hover { background-repeat:no-repeat; }