Files
timtabla/main.py

216 lines
5.7 KiB
Python

from bs4 import BeautifulSoup
from datetime import datetime, timezone
import pytz
import hashlib
import os
from flask import Flask, request, send_file, render_template, Response
import requests
from requests_ntlm import HttpNtlmAuth
import urllib
import html
# ------------------------------------------------------------
# Config
# ------------------------------------------------------------
LOCAL_TZ = pytz.timezone("Europe/London")
URL = "https://webapp.coventry.ac.uk/Timetable-main/Timetable/Current"
print(URL.format(urllib.parse.quote_plus("hello/world")))
app = Flask(__name__)
def parse_events(events_data):
events_data = events_data.replace("new Date", "")
cleaned_data = ""
for line in events_data.split("\n"):
comment_pos = line.find("//")
if comment_pos != -1:
line = line[:comment_pos]
if ":" in line:
key, val = line.split(":", 1)
line = f"'{key}': {val}"
cleaned_data += line + "\n"
parsed_data = eval(cleaned_data)
for event in parsed_data:
if "start" in event:
s = list(event["start"])
s[1] += 1
s.append(0)
event["start"] = LOCAL_TZ.localize(datetime(*s))
if "end" in event:
e = list(event["end"])
e[1] += 1
e.append(0)
event["end"] = LOCAL_TZ.localize(datetime(*e))
return parsed_data
def get_events_data(page_data):
soup = BeautifulSoup(page_data, features="html.parser")
for script in soup.head.find_all("script", {"type": "text/javascript"}):
if not script.has_attr("src"):
source = html.unescape(script.text)
break
else:
raise RuntimeError("Could not find inline timetable script")
return source.split("events:")[1].split("]")[0] + "]"
def ics_time(dt):
return dt.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
def make_uid(event):
key = f"{event['moduleDesc']}|{event['title']}|{event['start'].isoformat()}"
return hashlib.sha1(key.encode()).hexdigest() + "@timetable"
def create_ics_event(event):
return {
"uid": make_uid(event),
"summary": f"{event['moduleDesc']} - {event['title']}",
"description": f"{event['lecturer']} - {event['room']}",
"start": event["start"],
"end": event["end"],
}
def load_existing_uids(path):
uids = set()
return uids
if not os.path.exists(path):
return uids
with open(path, "r", encoding="utf-8") as f:
current_uid = None
cancelled = False
for line in f:
line = line.strip()
if line == "BEGIN:VEVENT":
current_uid = None
cancelled = False
elif line.startswith("UID:"):
current_uid = line[4:]
elif line == "STATUS:CANCELLED":
cancelled = True
elif line == "END:VEVENT" and current_uid and not cancelled:
uids.add(current_uid)
return uids
def fetch_timetable(username, password):
session = requests.Session()
session.auth = HttpNtlmAuth(username, password)
session.verify = False
r = session.get(URL)
r.raise_for_status()
session.close()
return r.text
def build_calendar(page_data):
events_js = get_events_data(page_data)
parsed_events = parse_events(events_js)
new_events = [create_ics_event(e) for e in parsed_events if e]
old_uids = set()
new_uids = {e["uid"] for e in new_events}
now = ics_time(datetime.now(timezone.utc))
# --- work out date range ---
if new_events:
start_dt = min(e["start"] for e in new_events)
end_dt = max(e["end"] for e in new_events)
start_year = start_dt.year
start_month = start_dt.strftime("%b")
end_year = end_dt.year
end_month = end_dt.strftime("%b")
else:
# sensible fallback if no events
start_year = start_month = end_year = end_month = None
lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Timetable Sync//EN",
"CALSCALE:GREGORIAN",
]
for ev in new_events:
lines.extend(
[
"BEGIN:VEVENT",
f"UID:{ev['uid']}",
f"DTSTAMP:{now}",
f"DTSTART:{ics_time(ev['start'])}",
f"DTEND:{ics_time(ev['end'])}",
f"SUMMARY:{ev['summary']}",
f"DESCRIPTION:{ev['description']}",
"END:VEVENT",
]
)
for uid in old_uids - new_uids:
lines.extend(
[
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTAMP:{now}",
"STATUS:CANCELLED",
"END:VEVENT",
]
)
lines.append("END:VCALENDAR")
data = "\n".join(lines)
return data, start_year, start_month, end_year, end_month
# ------------------------------------------------------------
# Flask
# ------------------------------------------------------------
@app.route("/")
def index():
return render_template("login.jinja")
@app.route("/login-and-download", methods=["POST"])
def login_and_download():
data = request.form
username = data["username"]
password = data["password"]
try:
data, start_year, start_month, end_year, end_month = build_calendar(fetch_timetable(f"COVENTRY\\{username}", password))
return Response(
data,
mimetype="text/calendar",
headers={"Content-Disposition": f'attachment; filename="{username}_timetable_{start_month}-{start_year}_to_{end_month}-{end_year}.ics"'},
)
except Exception as e:
return render_template("login.jinja", error=str(e)), 401
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")