#!/usr/bin/env python3 # # Converts output of remind to ICS format: # # Usage example: rem -ppp12 | rem2ics # # https://tools.ietf.org/html/rfc5545 import datetime import json import re import sys from datetime import datetime, timedelta, timezone from os.path import basename, splitext def escape_text(text): # https://tools.ietf.org/html/rfc5545#section-3.3.11 return re.sub(r'[\\;:,]', r'\\\g<0>', text) class Event: def __init__(self, uid, event_json): self.event_json = event_json self.uid = uid def summary(self): e = self.event_json summary = e.get('body', '') match = re.search(r'%"(.+)%"', summary) if match: summary = match.group(1) else: summary = self.__remove_spec(e, summary) return escape_text(summary) def description(self): e = self.event_json summary = e.get('body', '') if '%"' not in summary: return "" summary = summary.replace(r'%"', '') summary = self.__remove_spec(e, summary) return escape_text(summary) def categories(self): filename = basename(self.event_json['filename'].upper()) return splitext(filename)[0] def dtstart(self): e = self.event_json if 'eventstart' in e: dt = datetime.strptime(e['eventstart'], '%Y-%m-%dT%H:%M') return self.__datetime_format(dt) return e['date'].replace('-', '') def dtend(self): e = self.event_json if is_multiple_days_event(e): dt = datetime.strptime(e['until'], '%Y-%m-%d') return (dt + timedelta(days=1)).strftime('%Y%m%d') elif 'eventduration' in e and 'eventstart' in e: dt = datetime.strptime(e['eventstart'], '%Y-%m-%dT%H:%M') return self.__datetime_format(dt + timedelta(minutes=int(e['eventduration']))) return self.dtstart() @staticmethod def __datetime_format(dt): return dt.astimezone(timezone.utc).strftime('%Y%m%dT%H%M%SZ') def __remove_spec(self, event, summary): if 'time' in event: summary = summary.split(' ', 1)[1] if 'passthru' in event and 'COLOR' in event['passthru']: summary = ' '.join(summary.split(' ')[3:]) return summary def create_uid(event): if is_multiple_days_event(event): date = event['until'] else: date = event['date'] return "%s:%s:%s" % (basename(event['filename']), event['lineno'], date) def is_multiple_days_event(event): repeat = 'rep' in event has_until_date = 'until' in event has_time = 'time' in event return repeat and has_until_date and not has_time def print_ics(events): print('BEGIN:VCALENDAR') print('VERSION:2.0') for event in events: print('BEGIN:VEVENT') print('UID:%s' % event.uid) print('DTSTART:%s' % event.dtstart()) print('DTEND:%s' % event.dtend()) print('SUMMARY:%s' % event.summary()) print('DESCRIPTION:%s' % event.description()) print('CATEGORIES:%s' % event.categories()) print('END:VEVENT') print('END:VCALENDAR') def rem2ics(): added = set() events = [] data = json.load(sys.stdin) for month in data: for event_json in month['entries']: uid = create_uid(event_json) if uid in added: continue added.add(uid) events.append(Event(uid, event_json)) print_ics(events) if __name__ == '__main__': rem2ics()