|
@@ -0,0 +1,171 @@
|
|
|
+#!/usr/bin/python3
|
|
|
+
|
|
|
+import argparse
|
|
|
+import os.path
|
|
|
+from typing import Iterable
|
|
|
+from pathlib import Path
|
|
|
+import yaml
|
|
|
+from datetime import timedelta
|
|
|
+
|
|
|
+from icalendar import Calendar, Event
|
|
|
+
|
|
|
+
|
|
|
+def get_parser():
|
|
|
+ p = argparse.ArgumentParser()
|
|
|
+ p.add_argument("files", nargs="+", type=str)
|
|
|
+ p.add_argument(
|
|
|
+ "--out-md-dir",
|
|
|
+ help="Output directory for markdown files",
|
|
|
+ default="content/talks/",
|
|
|
+ type=Path,
|
|
|
+ )
|
|
|
+ p.add_argument(
|
|
|
+ "--out-schedule",
|
|
|
+ help="Output schedule.yml file",
|
|
|
+ default="data/schedule.yml",
|
|
|
+ type=Path,
|
|
|
+ )
|
|
|
+ p.add_argument('--night-threshold', metavar='HOUR', default=5, type=int)
|
|
|
+ p.add_argument("--mode", choices=["hugo"], default="hugo")
|
|
|
+ return p
|
|
|
+
|
|
|
+
|
|
|
+def round_down(num, divisor):
|
|
|
+ """
|
|
|
+ >>> round_down(1000, 10)
|
|
|
+ 1000
|
|
|
+ >>> round_down(1001, 10)
|
|
|
+ 1000
|
|
|
+ >>> round_down(1009, 10)
|
|
|
+ 1000
|
|
|
+ """
|
|
|
+ return num - (num % divisor)
|
|
|
+
|
|
|
+
|
|
|
+class Converter:
|
|
|
+ """
|
|
|
+ This class takes care of everything converter-related.
|
|
|
+
|
|
|
+ Objects are used to enable multiple output formats to be added pretty easily by subclassing
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, args):
|
|
|
+ self.args = args
|
|
|
+ self.rooms = []
|
|
|
+ self.talks = {}
|
|
|
+ self.talk_room = {} # map talk uid to room name
|
|
|
+ self.changed_files = []
|
|
|
+
|
|
|
+ def _fname_to_room(self, fpath: str) -> str:
|
|
|
+ return os.path.splitext(os.path.basename(fpath))[0]
|
|
|
+
|
|
|
+ def get_vevents_from_calendar(self, cal: Calendar) -> Iterable[Event]:
|
|
|
+ for subc in cal.subcomponents:
|
|
|
+ if type(subc) is Event:
|
|
|
+ yield subc
|
|
|
+
|
|
|
+ def load_input(self):
|
|
|
+ for fpath in self.args.files:
|
|
|
+ room = self._fname_to_room(fpath)
|
|
|
+ with open(fpath) as buf:
|
|
|
+ file_content = buf.read()
|
|
|
+ cal = Calendar.from_ical(file_content, multiple=True)
|
|
|
+ for subcal in cal:
|
|
|
+ for ev in self.get_vevents_from_calendar(subcal):
|
|
|
+ uid: str = ev.decoded("uid").decode("ascii")
|
|
|
+ self.talks[uid] = ev
|
|
|
+ self.talk_room[uid] = room
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ self.rooms = [self._fname_to_room(fpath) for fpath in self.args.files]
|
|
|
+ self.load_input()
|
|
|
+ self.output()
|
|
|
+ for fpath in self.changed_files:
|
|
|
+ print(fpath)
|
|
|
+
|
|
|
+
|
|
|
+class HugoConverter(Converter):
|
|
|
+ """
|
|
|
+ add relevant output features to the base converter
|
|
|
+ """
|
|
|
+
|
|
|
+ def output_markdown(self):
|
|
|
+ for uid, talk in self.talks.items():
|
|
|
+ fname = "%s.md" % uid
|
|
|
+ fpath = self.args.out_md_dir / fname
|
|
|
+ self.changed_files.append(fpath)
|
|
|
+ with open(fpath, "w") as buf:
|
|
|
+ # preamble
|
|
|
+ buf.write("---\n")
|
|
|
+ yaml.safe_dump(
|
|
|
+ dict(
|
|
|
+ key=uid,
|
|
|
+ title=talk.decoded("SUMMARY").decode("utf8"),
|
|
|
+ format="conference",
|
|
|
+ tags=[],
|
|
|
+ ),
|
|
|
+ buf,
|
|
|
+ )
|
|
|
+ buf.write("---\n\n")
|
|
|
+ if "DESCRIPTION" in talk:
|
|
|
+ buf.write(talk.decoded("DESCRIPTION").decode("utf8"))
|
|
|
+
|
|
|
+ def output_schedule(self):
|
|
|
+ days = {}
|
|
|
+ for uid, talk in self.talks.items():
|
|
|
+ # TODO: talk just after midnight should belong to the preceding day
|
|
|
+ dt = talk.decoded("dtstart")
|
|
|
+ after_midnight = dt.time().hour < self.args.night_threshold
|
|
|
+ if after_midnight:
|
|
|
+ dt = dt - timedelta(days=1)
|
|
|
+ day = dt.strftime("%Y-%m-%d")
|
|
|
+
|
|
|
+ hour = talk.decoded("dtstart").time().hour
|
|
|
+ minute = talk.decoded("dtstart").time().minute
|
|
|
+ if after_midnight:
|
|
|
+ hour += 24
|
|
|
+
|
|
|
+ start = '%02d:%02d' % (hour, minute)
|
|
|
+ if day not in days:
|
|
|
+ days[day] = dict(day=day, start=start, rooms={})
|
|
|
+ if days[day]['start'] > start:
|
|
|
+ days[day]['start'] = start
|
|
|
+
|
|
|
+ room = self.talk_room[uid]
|
|
|
+ days[day]["rooms"].setdefault(room, dict(room=room, slots=[]))
|
|
|
+
|
|
|
+ talkstart = round_down(hour * 100 + minute, 10)
|
|
|
+ duration = talk.decoded("dtend") - talk.decoded("dtstart")
|
|
|
+ duration_minutes = int(duration.total_seconds() // 60)
|
|
|
+ duration_minutes = round_down(duration_minutes, 30)
|
|
|
+ slot = "%04d-%dmin" % (talkstart, duration_minutes)
|
|
|
+ days[day]["rooms"][room]["slots"].append(dict(slot=slot, talk=uid))
|
|
|
+
|
|
|
+ # convert from our intermediate format to the correct one
|
|
|
+ for d in days:
|
|
|
+ # vanity: let's sort
|
|
|
+ for room in days[d]["rooms"]:
|
|
|
+ days[d]["rooms"][room]["slots"].sort(key=lambda x: x["slot"])
|
|
|
+ # convert dict to list
|
|
|
+ days[d]["rooms"] = list(days[d]["rooms"].values())
|
|
|
+ out = {"schedule": list(days.values())}
|
|
|
+
|
|
|
+ # dump, finally
|
|
|
+ with open(self.args.out_schedule, "w") as buf:
|
|
|
+ yaml.safe_dump(out, buf)
|
|
|
+ self.changed_files.append(self.args.out_schedule)
|
|
|
+
|
|
|
+ def output(self):
|
|
|
+ self.output_markdown()
|
|
|
+ self.output_schedule()
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ converter_register = {"hugo": HugoConverter}
|
|
|
+ args = get_parser().parse_args()
|
|
|
+ c = converter_register[args.mode](args)
|
|
|
+ c.run()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|