Created
May 2, 2019 13:37
-
-
Save ndw/e76a3dad862989deb0cb74b3e2b557b4 to your computer and use it in GitHub Desktop.
Create an emacs diary file out of Mac calendars
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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