Skip to content

Instantly share code, notes, and snippets.

@ndw
Created May 2, 2019 13:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ndw/e76a3dad862989deb0cb74b3e2b557b4 to your computer and use it in GitHub Desktop.
Save ndw/e76a3dad862989deb0cb74b3e2b557b4 to your computer and use it in GitHub Desktop.
Create an emacs diary file out of Mac calendars
#!/usr/bin/env python
import icalendar
from icalevents.icalevents import events
from datetime import datetime, timezone, timedelta
from dateutil.tz import UTC, gettz
import re
import tzlocal
import pytz
import os
import sys
import glob
import plistlib
import subprocess
import traceback
# I could put this in a config file, but ...
TITLES = {"Home": "",
"Family": "",
"Calendar": "",
"Phases of the Moon": "☽"}
# This script was designed to run on my MacOS (Mojave) box. The core
# functionality should be reasonably portable, but you'll have to work out
# where to find the calendars.
LOCAL_TIMEZONE = datetime.now(timezone.utc).astimezone().tzinfo
NOW = datetime.now(LOCAL_TIMEZONE)
MIDNIGHT = datetime(NOW.year, NOW.month, NOW.day, 0, 0, 0, 0, tzinfo=LOCAL_TIMEZONE)
DATE_START = MIDNIGHT - timedelta(days=30)
DATE_END = MIDNIGHT + timedelta(days=90)
CALDIR = os.path.join(os.getenv("HOME"), "Library/Calendars")
# Get the metadata from the MacOS cache.
CALMETA = {}
rows = subprocess.check_output(["sqlite3", os.path.join(CALDIR, "Calendar Cache"),
"select ZUID,ZCOLORSTRING,ZTITLE,ZOWNERDISPLAYNAME from ZNODE where ZISENABLED"]) \
.decode("UTF-8")
for row in rows.split("\n"):
cols = row.split("|")
if cols[0] != "":
CALMETA[cols[0]] = cols[2]
CALENDARS = {}
for path in glob.iglob(os.path.join(CALDIR, "**/*.calendar"), recursive=True):
uid = path[path.rfind("/")+1 : len(path)-9]
if uid in CALMETA:
title = CALMETA[uid]
if title in TITLES:
CALENDARS[path] = TITLES[title]
else:
pass
#print("Skip: %s" % title)
EVENTDICT = {}
def diary_events(icsfile, prefix):
if prefix == "":
pfx = ""
else:
pfx = prefix + ": "
try:
calevents = events(file=icsfile, start=DATE_START, end=DATE_END)
for event in calevents:
eventhash = "%s %s" % (str(event.start)[0:10], event.uid)
if eventhash in EVENTDICT:
# For any given event, only output it once for any given day
break
EVENTDICT[eventhash] = 1
if event.all_day:
start = event.start
end = event.end
else:
localtz = event.start.astimezone().tzinfo
start = event.start.astimezone(localtz)
end = event.end.astimezone(localtz)
if event.all_day:
# Calculate the range on midnight, inclusive
dstart = datetime(start.year, start.month, start.day, 0, 0, 0)
dend = dstart + timedelta((end-start).days - 1)
print("%%%%(and (diary-block %d %d %d %d %d %d)) %s%s" % (
dstart.month, dstart.day, dstart.year,
dend.month, dend.day, dend.year,
pfx, event.summary))
else:
print("%d/%d/%d %02d:%02d-%02d:%02d %s%s" % (
start.month, start.day, start.year,
start.hour, start.minute,
end.hour, end.minute,
pfx, event.summary))
except ValueError as ve:
if str(ve) == "RRULE UNTIL values must be specified in UTC when DTSTART is timezone-aware":
bruteForceAndIgnorance(icsfile, pfx)
else:
print("ERR: %s: %s" % (icsfile, ve), file=sys.stderr)
def bruteForceAndIgnorance(icsfile, pfx):
# See https://github.com/irgangla/icalevents/issues/47
start = None
end = None
freq = None
interval = 1
until = None
summary = ""
#print(icsfile, file=sys.stderr)
ics = open(icsfile, "r")
for line in ics:
line = line.rstrip("\r\n")
if line.startswith("DTSTART;VALUE=DATE:"):
start = line[len(line)-8:]
if line.startswith("DTEND;VALUE=DATE:"):
end = line[len(line)-8:]
if line.startswith("RRULE:"):
m = re.search("FREQ=([A-Z]+)", line)
if m:
freq = m.group(1)
m = re.search("INTERVAL=([0-9]+)", line)
if m:
interval = int(m.group(1))
m = re.search("UNTIL=([0-9]+)", line)
if m:
until = m.group(1)
if line.startswith("SUMMARY:"):
summary = line[8:]
if line.startswith("END:VEVENT"):
if freq:
processEvent(icsfile,pfx,start,end,freq,interval,until,summary)
start = end = freq = until = None
interval = 1
summary = ""
ics.close()
def processEvent(icsfile,pfx,start,end,freq,interval,until,summary):
if freq != "YEARLY" and freq != "DAILY":
print("ERR: %s not parseable with bruteForceAndIgnorance" % icsfile, file=sys.stderr)
return
dtstart = datetime(int(start[0:4]), int(start[4:6]), int(start[6:8]), 12, 0, 0, tzinfo=LOCAL_TIMEZONE)
dtend = datetime(int(end[0:4]), int(end[4:6]), int(end[6:8]), 12, 0, 0, tzinfo=LOCAL_TIMEZONE)
if dtstart > DATE_END or dtend < DATE_START:
return
if until:
until = datetime(int(until[0:4]), int(until[4:6]), int(until[6:8]), 12, 0, 0, tzinfo=LOCAL_TIMEZONE)
else:
until = DATE_END + timedelta(days=1)
#print(dtstart,dtend,"f:",freq,"i:",interval,"u:",until,"sum:",summary, file=sys.stderr)
while dtstart <= until:
if dtstart >= DATE_START and dtstart <= DATE_END:
print("%%%%(and (diary-block %d %d %d %d %d %d)) %s%s" % (
dstart.month, dstart.day, dstart.year,
dtstart.month, dtstart.day, dstart.year,
pfx, summary))
if freq == "DAILY":
dtstart += timedelta(days=interval)
else:
dtstart = datetime(dtstart.year+1, dtstart.month, dtstart.day, 12, 0, 0, tzinfo=LOCAL_TIMEZONE)
# ======================================================================
for caldir in list(CALENDARS.keys()):
for ics in glob.iglob(os.path.join(caldir, "Events/*.ics")):
diary_events(ics, CALENDARS[caldir])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment