commit c9d77c745e58b41d1d64c4b38918d2698d630eec Author: George Ruinelli Date: Sun Oct 8 23:12:55 2023 +0200 init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1c5f9c5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3 + +WORKDIR / + + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + + +EXPOSE 80 +CMD ["python3", "webserver.py"] + + +ENV AM_I_IN_A_DOCKER_CONTAINER Yes + +RUN date >/build-date.txt diff --git a/calendar_fetcher.py b/calendar_fetcher.py new file mode 100755 index 0000000..35f8772 --- /dev/null +++ b/calendar_fetcher.py @@ -0,0 +1,189 @@ +#!/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 requests + +import config as cfg + + +from collections import OrderedDict +from operator import getitem + +import pprint as pp + + + +def fetch_calendar(): + numberOfDaysToShow = 5 + + excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', '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]) + continue + else: + #print("Adding %s" % str(calendar).split("/")[-2]) + 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" + + + 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" + + 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()) diff --git a/config.py b/config.py new file mode 100644 index 0000000..0718ba4 --- /dev/null +++ b/config.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +# Configuration for the CalDAV server +url = "https://cloud.ruinelli.ch/remote.php/dav/principals/users/" +userN = "gruinelli" +passW = "attgigoc1" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9a64ca8 --- /dev/null +++ b/readme.md @@ -0,0 +1,19 @@ +# Docker + +## Build it manually +`docker build -t calendar-fetcher .` + +## Run it in a Docker Container +1. Adjust the parameters in `Dockerfile` +1. Build the docker image with `docker build -t calendar-fetcher .` +1. Run it with `docker run -it --rm -v /volume1/web/calendar-fetcher:/data --name my-calendar-fetcher -p 8014:8014 --label=com.centurylinklabs.watchtower.enable=false calendar-fetcher` +1. Call it in a webbrowser: `http://localhost:8014` + +## replace existing Docker Container (Update) +``` +docker build -t calendar-fetcher . +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 new file mode 100644 index 0000000..9ad0b3d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pytz +caldav diff --git a/webserver.py b/webserver.py new file mode 100644 index 0000000..902cf53 --- /dev/null +++ b/webserver.py @@ -0,0 +1,70 @@ +#!/usr/bin/python + +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse, parse_qs + +from calendar_fetcher import fetch_calendar + + +internalPort = 8014 +outfile = "data/calendar.json" + + +# This class will handle any incoming request from +# a browser +class myHandler(BaseHTTPRequestHandler): + # Handler for the GET requests + def do_GET(self): + self.send_response(200) + + parsed_url = urlparse(self.path) + query = parse_qs(parsed_url.query) + + print("File: %s, query: %s" % (parsed_url.path, query), flush=True) + + if parsed_url.path == "/": + self.send_header('Content-type','text/html') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + + self.wfile.write(b"

Calendar Fetcher

\n") + self.wfile.write(b"fetching...
\n") + print("fetching...") + self.wfile.flush() + + data = fetch_calendar() + + self.wfile.write(b"storing...
\n") + print("storing...") + self.wfile.flush() + + f = open(outfile, "w") + f.write(data) + f.close() + + + self.wfile.write(b"Done.\nData got written to %s}
\n" % bytes(outfile, "utf8")) + print("Done.\nData got written to %s}" % outfile) + self.wfile.flush() + + + + + + +if __name__ == '__main__': + # Run the service + + try: + print("Starting...") + server = HTTPServer(('', internalPort), myHandler) + + print("Listening on port %d" % internalPort) + print("Ready to receive requests") + + # Wait forever for incoming http requests + server.serve_forever() + + except KeyboardInterrupt: + print ('^C received, shutting down the web server') + server.socket.close()