diff --git a/calendar_fetcher.py b/calendar_fetcher.py index cf0e9d4..a343f84 100755 --- a/calendar_fetcher.py +++ b/calendar_fetcher.py @@ -1,193 +1,325 @@ #!/usr/bin/env python3 -import time -import datetime -import json -from pytz import UTC # timezone -import caldav -import caldav.elements.ical # for calendarColor import icalendar +import recurring_ical_events import requests +import datetime +import pickle +import pprint +import json import config as cfg -from collections import OrderedDict -from operator import getitem +numberOfDaysToShow = 5 +#numberOfDaysToShow = 1 -import pprint as pp +useCachedData = False +storeDataInCache = False +includedCalendarIDs = [] +for m in cfg.calendarsMetadata: + excluded = False + for excludedCalendar in cfg.excludedCalendarIDs: + #if excludedCalendar == m["calendarName"]: + if excludedCalendar == m["displayName"]: + excluded = True -def fetch_calendar(): - numberOfDaysToShow = 5 + if excluded: + continue + includedCalendarIDs.append(m["calendarName"]) - #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos'] - excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos'] - #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael'] - #excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage'] - client = caldav.DAVClient(url=cfg.url, username=cfg.userN, password=cfg.passW) - principal = client.principal() - calendars = principal.calendars() +def fetchCalendar(calendarName): + url = cfg.urlBase + "/" + calendarName + cfg.urlSuffix + #print(f"Fetching calendar \"{calendarName}\" from {url}...") + response = requests.get(url, auth=(cfg.userN, cfg.passW)) + return response.text + +def fetchCalendarsMetadata(): + size = 0 + icsData = {} + for id in includedCalendarIDs: + icsData[id] = fetchCalendar(id) + #print(id, icsData[id]) + size += len(icsData[id]) + + #print(f"Fetched {(float(size) / 1024 / 1024):.4} MB") + return icsData + + +def get_calendar_data(): + # Fetch calendars + if useCachedData: + #print("Using cached data") + with open('cacheData.pkl', 'rb') as fp: + icsData = pickle.load(fp) + else: + icsData = fetchCalendarsMetadata() + if storeDataInCache: + # save dictionary to .pkl file (for debugging only) + with open('cacheData.pkl', 'wb') as fp: + pickle.dump(icsData, fp) + #print('ICS data saved successfully to file') + + # Prepare list of days firstDay = datetime.date.today() lastDay = firstDay + datetime.timedelta(days=numberOfDaysToShow) #print("Showing all events between %s and %s" % (firstDay, lastDay)) days = [] - day = firstDay while day < lastDay: days.append(day) day += datetime.timedelta(days=1) #print("Days to list: %s" % days) - - # Get offset from local time to UTC, see also https://stackoverflow.com/questions/3168096/getting-computers-utc-offset-in-python - # TODO: This should be done for every event individually! - ts = time.time() - utcOffset = (datetime.datetime.fromtimestamp(ts) - - datetime.datetime.utcfromtimestamp(ts)).total_seconds() - utcOffset = int(utcOffset / 3600) # in hours - #print("UTC offset:", utcOffset) - #print("All calendars:") - #for calendar in calendars: - #print(" %s" % str(calendar).split("/")[-2]) + # extracting events + events = {} + for day in days: + dayId = day.strftime("%m/%d/%Y") + events[dayId] = {} + #print(f"Extracting events for {day}...") + for id in includedCalendarIDs: + calendarData = icalendar.Calendar.from_ical(icsData[id]) + es = recurring_ical_events.of(calendarData).at(day) + for e in es: + #print(e) + start = e["DTSTART"].dt + startDate = start.strftime("%m/%d/%Y") + startTime = start.strftime("%H:%M") + end = e["DTEND"].dt + endDate = end.strftime("%m/%d/%Y") + endTime = end.strftime("%H:%M") + summary = str(e["SUMMARY"]) - calendarsFiltered = [] + for m in cfg.calendarsMetadata: + if id == m["calendarName"]: + name = m["displayName"] + break - # exclude some calendars: - for calendar in calendars: - #print(excludedCalendars) - #print(calendar) - #if str(calendar).split("/")[-2] in excludedCalendars: - if str(calendar) in excludedCalendars: - #print("Ignoring %s" % str(calendar).split("/")[-2]) - #print("Ignoring %s" % str(calendar)) - continue - else: - #print("Adding %s" % str(calendar).split("/")[-2]) - #print("Adding %s" % str(calendar)) - calendarsFiltered.append(calendar) + d = {"calendar": name, "summary": summary, + "startDate": startDate, "startTime": startTime, + "endDate": endDate, "endTime": endTime + } + uid = start.strftime("%H-%M") + "_" + str(e["UID"]) + events[dayId][uid] = d + #print(d) - #print("Filtered calendars:") - #for calendar in calendarsFiltered: - #print(" %s" % str(calendar).split("/")[-2]) + # Sort by time + myKeys = list(events[dayId].keys()) + myKeys.sort() + events[dayId] = {i: events[dayId][i] for i in myKeys} + + #pprint.pprint(events) - eventsList = {} metaData = {} - - for day in days: - eventsList[day] = {} - - #print("processing calendars") - for calendar in calendarsFiltered: - #calendarName = str(calendar).split("/")[-2] - calendarName = str(calendar) - metaData[calendarName] = {} - metaData[calendarName]['displayname'] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0] - metaData[calendarName]['color'] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0] - #print(metaData) - - for day in days: - #print(" Looking for events between %s and %s" % (day, day + datetime.timedelta(days=1))) - results = calendar.date_search(day, day + datetime.timedelta(days=1)) # get event for selected day (24h) - - for eventraw in results: - eventUuid = str(eventraw).split("/")[-1].split(".")[0] - #print(" - %s" % eventUuid) - - # print(str(eventraw)) - #event = icalendar.Calendar.from_ical(eventraw._data) - - url = "https://" + cfg.userN + ":" + cfg.passW + "@" + str(eventraw).replace("Event: https://", "") - # print(url) - data = requests.get(url).text - - event = icalendar.Calendar.from_ical(data) - for component in event.walk(): - if component.name == "VEVENT": - eventsList[day][eventUuid] = {} - ##eventsList[day][eventUuid]["calendar"] = str(calendar).split("/")[-2] - eventsList[day][eventUuid]["calendar"] = str(calendar) - - # print(" summary: %s" % component.get('summary')) - eventsList[day][eventUuid]["summary"] = str(component.get('summary')) - - # print(" description: %s" % component.get('description')) - #eventsList[day][eventUuid]["description"] = str(component.get('description')) - - startDate = component.get('dtstart') - # print(" dtstart: %s == %s?" % (startDate.dt.strftime('%m/%d/%Y %H:%M'), day)) - if startDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event starts today - # print(" single day event") - eventsList[day][eventUuid]["startDate"] = startDate.dt.strftime('%m/%d/%Y') - eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') - else: # event started before today, set startdate to day and start time to midnight (for multi day events with start/end time) - # print(" multi day event") - eventsList[day][eventUuid]["startDate"] = day.strftime('%m/%d/%Y') - # eventsList[day][eventUuid]["startTime"] = "00:00" # No longer working! - eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') - - try: - endDate = component.get('dtend') - #print(" dtend: %s" % endDate.dt.strftime('%m/%d/%Y %H:%M')) - if endDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today - eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y') - eventsList[day][eventUuid]["endTime"] = (endDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') - else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time) - eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y') - # eventsList[day][eventUuid]["endTime"] = "24:00" # No longer working! - eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y') - - except AttributeError as ae: # event has no endtime, use duration - #print(" %s" % ae) - duration = component.get('duration') - #print(" Event misses end time, using duration") - #print(" duration: %s" % duration.dt) - if (startDate.dt + duration.dt).strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today - eventsList[day][eventUuid]["endDate"] = (startDate.dt + duration.dt).strftime('%m/%d/%Y') - eventsList[day][eventUuid]["endTime"] = (startDate.dt + duration.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') - else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time) - eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y') - eventsList[day][eventUuid]["endTime"] = "24:00" - - #print('') - #break - - - # order by time per day, see https://www.geeksforgeeks.org/python-sort-nested-dictionary-by-key/ - for day in days: - sortedbyTimePerDay = OrderedDict(sorted(eventsList[day].items(), key = lambda x: getitem(x[1], 'startTime'))) - eventsList[day] = sortedbyTimePerDay - - # Sort days - eventsList = OrderedDict(sorted(eventsList.items())) - - - # reformat day key (datetime => string) - for day in days: - eventsList[day.strftime('%m/%d/%Y')] = eventsList.pop(day) - - - #pp.pprint(eventsList) - - data = {} - data['metadata'] = metaData - data['events'] = OrderedDict(eventsList) - - #pp.pprint(data) - + for m in cfg.calendarsMetadata: + #print(m) + if m["calendarName"] in includedCalendarIDs: + metaData[m["displayName"]] = { + "displayname": m["displayName"], + "color": m["color"] + } # Converting to JSON - dataJson = json.dumps(data, indent=4) - return dataJson + return json.dumps({"metadata": metaData, "events": events}, indent=4) + + + + + + + + + + + + + + + + + + + + + +# +# +# +# numberOfDaysToShow = 5 +# +# #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos'] +# excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos'] +# #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael'] +# #excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage'] +# +# client = caldav.DAVClient(url=cfg.url, username=cfg.userN, password=cfg.passW) +# principal = client.principal() +# calendars = principal.calendars() +# +# firstDay = datetime.date.today() +# lastDay = firstDay + datetime.timedelta(days=numberOfDaysToShow) +# #print("Showing all events between %s and %s" % (firstDay, lastDay)) +# +# days = [] +# +# day = firstDay +# while day < lastDay: +# days.append(day) +# day += datetime.timedelta(days=1) +# +# #print("Days to list: %s" % days) +# +# # Get offset from local time to UTC, see also https://stackoverflow.com/questions/3168096/getting-computers-utc-offset-in-python +# # TODO: This should be done for every event individually! +# ts = time.time() +# utcOffset = (datetime.datetime.fromtimestamp(ts) - +# datetime.datetime.utcfromtimestamp(ts)).total_seconds() +# utcOffset = int(utcOffset / 3600) # in hours +# #print("UTC offset:", utcOffset) +# +# #print("All calendars:") +# #for calendar in calendars: +# #print(" %s" % str(calendar).split("/")[-2]) +# +# calendarsFiltered = [] +# +# # exclude some calendars: +# for calendar in calendars: +# #print(excludedCalendars) +# #print(calendar) +# #if str(calendar).split("/")[-2] in excludedCalendars: +# if str(calendar) in excludedCalendars: +# #print("Ignoring %s" % str(calendar).split("/")[-2]) +# #print("Ignoring %s" % str(calendar)) +# continue +# else: +# #print("Adding %s" % str(calendar).split("/")[-2]) +# #print("Adding %s" % str(calendar)) +# calendarsFiltered.append(calendar) +# +# +# #print("Filtered calendars:") +# #for calendar in calendarsFiltered: +# #print(" %s" % str(calendar).split("/")[-2]) +# +# eventsList = {} +# metaData = {} +# +# for day in days: +# eventsList[day] = {} +# +# #print("processing calendars") +# for calendar in calendarsFiltered: +# #calendarName = str(calendar).split("/")[-2] +# calendarName = str(calendar) +# metaData[calendarName] = {} +# metaData[calendarName]['displayname'] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0] +# metaData[calendarName]['color'] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0] +# #print(metaData) +# +# for day in days: +# #print(" Looking for events between %s and %s" % (day, day + datetime.timedelta(days=1))) +# results = calendar.date_search(day, day + datetime.timedelta(days=1)) # get event for selected day (24h) +# +# for eventraw in results: +# eventUuid = str(eventraw).split("/")[-1].split(".")[0] +# #print(" - %s" % eventUuid) +# +# # print(str(eventraw)) +# #event = icalendar.Calendar.from_ical(eventraw._data) +# +# url = "https://" + cfg.userN + ":" + cfg.passW + "@" + str(eventraw).replace("Event: https://", "") +# # print(url) +# data = requests.get(url).text +# +# event = icalendar.Calendar.from_ical(data) +# for component in event.walk(): +# if component.name == "VEVENT": +# eventsList[day][eventUuid] = {} +# ##eventsList[day][eventUuid]["calendar"] = str(calendar).split("/")[-2] +# eventsList[day][eventUuid]["calendar"] = str(calendar) +# +# # print(" summary: %s" % component.get('summary')) +# eventsList[day][eventUuid]["summary"] = str(component.get('summary')) +# +# # print(" description: %s" % component.get('description')) +# #eventsList[day][eventUuid]["description"] = str(component.get('description')) +# +# startDate = component.get('dtstart') +# # print(" dtstart: %s == %s?" % (startDate.dt.strftime('%m/%d/%Y %H:%M'), day)) +# if startDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event starts today +# # print(" single day event") +# eventsList[day][eventUuid]["startDate"] = startDate.dt.strftime('%m/%d/%Y') +# eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') +# else: # event started before today, set startdate to day and start time to midnight (for multi day events with start/end time) +# # print(" multi day event") +# eventsList[day][eventUuid]["startDate"] = day.strftime('%m/%d/%Y') +# # eventsList[day][eventUuid]["startTime"] = "00:00" # No longer working! +# eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') +# +# try: +# endDate = component.get('dtend') +# #print(" dtend: %s" % endDate.dt.strftime('%m/%d/%Y %H:%M')) +# if endDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today +# eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y') +# eventsList[day][eventUuid]["endTime"] = (endDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') +# else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time) +# eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y') +# # eventsList[day][eventUuid]["endTime"] = "24:00" # No longer working! +# eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y') +# +# except AttributeError as ae: # event has no endtime, use duration +# #print(" %s" % ae) +# duration = component.get('duration') +# #print(" Event misses end time, using duration") +# #print(" duration: %s" % duration.dt) +# if (startDate.dt + duration.dt).strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today +# eventsList[day][eventUuid]["endDate"] = (startDate.dt + duration.dt).strftime('%m/%d/%Y') +# eventsList[day][eventUuid]["endTime"] = (startDate.dt + duration.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M') +# else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time) +# eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y') +# eventsList[day][eventUuid]["endTime"] = "24:00" +# +# #print('') +# #break +# +# +# # order by time per day, see https://www.geeksforgeeks.org/python-sort-nested-dictionary-by-key/ +# for day in days: +# sortedbyTimePerDay = OrderedDict(sorted(eventsList[day].items(), key = lambda x: getitem(x[1], 'startTime'))) +# eventsList[day] = sortedbyTimePerDay +# +# # Sort days +# eventsList = OrderedDict(sorted(eventsList.items())) +# +# +# # reformat day key (datetime => string) +# for day in days: +# eventsList[day.strftime('%m/%d/%Y')] = eventsList.pop(day) +# +# +# #pp.pprint(eventsList) +# +# data = {} +# data['metadata'] = metaData +# data['events'] = OrderedDict(eventsList) +# +# #pp.pprint(data) +# +# +# # Converting to JSON +# dataJson = json.dumps(data, indent=4) +# return dataJson if __name__ == '__main__': - print(fetch_calendar()) + print(get_calendar_data()) diff --git a/config.py b/config.py index 0718ba4..c8612b4 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,26 @@ #!/usr/bin/env python # Configuration for the CalDAV server -url = "https://cloud.ruinelli.ch/remote.php/dav/principals/users/" +urlBase = 'https://cloud.ruinelli.ch/remote.php/dav/calendars/gruinelli' +urlSuffix = "?export" userN = "gruinelli" passW = "attgigoc1" + + +"""calendar metaData +Fetched with the helper.py script""" +calendarsMetadata = [ + {'calendarName': 'george', 'color': '#55aaff', 'displayName': 'George'}, + {'calendarName': 'gemeinsam', 'color': '#ffff00', 'displayName': 'Gemeinsam'}, + {'calendarName': 'todo', 'color': '#ffaaff', 'displayName': 'ToDo'}, + {'calendarName': 'todogemeinsam', 'color': '#cc66cc', 'displayName': 'ToDo Gemeinsam'}, + {'calendarName': 'contact_birthdays', 'color': '#FFFFCA', 'displayName': 'Geburtstage von Kontakten'}, + {'calendarName': 'geburtstage', 'color': '#74e7d2', 'displayName': 'Geburtstage'}, + {'calendarName': 'jael', 'color': '#16fb04', 'displayName': 'Kinder'}, + {'calendarName': 'infos', 'color': '#0090a1', 'displayName': 'Infos'}, + {'calendarName': 'anita_shared_by_anita', 'color': '#FF8000', 'displayName': 'Anita'}, + {'calendarName': 'anitagesch%C3%A4ft%28bga%2Cpa%29_shared_by_anita', 'color': '#007B00', 'displayName': 'Anita Arbeit (Alixon, PAI)'}, + {'calendarName': 'evtermine_shared_by_anita', 'color': '#FFE5CC', 'displayName': 'ev Termine'} +] + +excludedCalendarIDs = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos'] diff --git a/helper.py b/helper.py new file mode 100644 index 0000000..55ba624 --- /dev/null +++ b/helper.py @@ -0,0 +1,51 @@ +"""Script to fetch the calendar names and colors""" + +import caldav +import caldav.elements.ical # for calendarColor +import pprint + +# Configuration for the CalDAV server +url = "https://cloud.ruinelli.ch/remote.php/dav/principals/users/" +userN = "gruinelli" +passW = "attgigoc1" + +def fetch_calendar(): + numberOfDaysToShow = 5 + + # excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos'] + excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos'] + # excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael'] + # excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage'] + + client = caldav.DAVClient(url=url, username=userN, password=passW) + principal = client.principal() + calendars = principal.calendars() + + #print(calendars) + #print("") + + calendarNames = (str(calendars).replace("Calendar(https://cloud.ruinelli.ch/remote.php/dav/calendars/gruinelli/", ""). + replace(")", "").replace("/", "").replace("[", "").replace("]", ""). + split(",")) + #print(calendarNames) + #print("") + + + + metaData = [] + + i = 0 + for calendar in calendars: + #print(calendar) + data = {} + data["calendarName"] = calendarNames[i] + i += 1 + data["displayname"] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0] + data["color"] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0] + metaData.append(data) + + print("") + pprint.pprint(metaData) + +if __name__ == '__main__': + fetch_calendar() diff --git a/readme.md b/readme.md index 9a64ca8..5e6a7cf 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,6 @@ # Docker +## Local run for testing +`python webserver.py` ## Build it manually `docker build -t calendar-fetcher .` @@ -16,4 +18,3 @@ docker stop calendar-fetcher docker rm calendar-fetcher docker run -d -P --name calendar-fetcher -p 8014:8014 -v /volume1/web/smartmirror/data:/data --label=com.centurylinklabs.watchtower.enable=false calendar-fetcher ``` - diff --git a/requirements.txt b/requirements.txt index 9ad0b3d..7316583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ -pytz -caldav +icalendar +recurring_ical_events +requests +datetime +#pickle +#pprint diff --git a/webserver.py b/webserver.py index 902cf53..e950ca1 100644 --- a/webserver.py +++ b/webserver.py @@ -3,7 +3,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs -from calendar_fetcher import fetch_calendar +from calendar_fetcher import * internalPort = 8014 @@ -29,28 +29,26 @@ class myHandler(BaseHTTPRequestHandler): self.wfile.write(b"

Calendar Fetcher

\n") self.wfile.write(b"fetching...
\n") - print("fetching...") + print("fetching and processing...") self.wfile.flush() - data = fetch_calendar() + data = get_calendar_data() self.wfile.write(b"storing...
\n") - print("storing...") self.wfile.flush() + print("storing...") f = open(outfile, "w") f.write(data) f.close() - - - self.wfile.write(b"Done.\nData got written to %s}
\n" % bytes(outfile, "utf8")) + + try: # Might trow a "[Errno 32] Broken pipe" becasue the client closed the socket already (eg. due to a timeout) + self.wfile.write(b"Done.\nData got written to %s}
\n" % bytes(outfile, "utf8")) + self.wfile.flush() + except: + pass print("Done.\nData got written to %s}" % outfile) - self.wfile.flush() - - - - if __name__ == '__main__': # Run the service