diff --git a/main.py b/main.py index 5a57cae..751092b 100644 --- a/main.py +++ b/main.py @@ -3,22 +3,23 @@ from datetime import datetime, timezone import pytz import hashlib import os -from flask import Flask, request, send_file, render_template +from flask import Flask, request, send_file, render_template, Response import requests from requests_ntlm import HttpNtlmAuth +import urllib # ------------------------------------------------------------ # Config # ------------------------------------------------------------ -RESPONSE_FILE = "response.txt" -OLD_CALENDAR = "old_calendar.ics" -NEW_CALENDAR = "calendar.ics" 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 = "" @@ -52,10 +53,7 @@ def parse_events(events_data): return parsed_data -def get_events_data_from_file(path): - with open(path, "r", encoding="utf-8") as f: - page_data = f.read() - +def get_events_data(page_data): soup = BeautifulSoup(page_data, features="html.parser") for script in soup.head.find_all("script", {"type": "text/javascript"}): @@ -89,6 +87,7 @@ def create_ics_event(event): def load_existing_uids(path): uids = set() + return uids if not os.path.exists(path): return uids @@ -111,6 +110,7 @@ def load_existing_uids(path): return uids + def fetch_timetable(username, password): session = requests.Session() session.auth = HttpNtlmAuth(username, password) @@ -118,16 +118,17 @@ def fetch_timetable(username, password): r = session.get(URL) r.raise_for_status() - with open("response.txt", "w", encoding="utf-8") as f: - f.write(r.text) + session.close() + return r.text -def build_calendar(): - events_js = get_events_data_from_file(RESPONSE_FILE) + +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 = load_existing_uids(OLD_CALENDAR) + old_uids = set() new_uids = {e["uid"] for e in new_events} now = ics_time(datetime.now(timezone.utc)) @@ -140,55 +141,58 @@ def build_calendar(): ] 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", - ]) + 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.extend( + [ + "BEGIN:VEVENT", + f"UID:{uid}", + f"DTSTAMP:{now}", + "STATUS:CANCELLED", + "END:VEVENT", + ] + ) lines.append("END:VCALENDAR") - with open(NEW_CALENDAR, "w", encoding="utf-8") as f: - f.write("\n".join(lines)) + return "\n".join(lines) + + # ------------------------------------------------------------ # Flask # ------------------------------------------------------------ @app.route("/") def index(): - return render_template("index.html") + return render_template("login.jinja") + @app.route("/login-and-download", methods=["POST"]) def login_and_download(): - data = request.get_json() + data = request.form username = data["username"] password = data["password"] - username = f'COVENTRY\\{username}' try: - fetch_timetable(username, password) #request - build_calendar() - return send_file( - NEW_CALENDAR, - as_attachment=True, - download_name="timetable.ics", - mimetype="text/calendar" + data = build_calendar(fetch_timetable(f"COVENTRY\\{username}", password)) + return Response( + data, + mimetype="text/calendar", + headers={"Content-Disposition": f'attachment; filename="{username}-timetable.ics"'}, ) except Exception as e: - return str(e), 400 + return render_template("login.jinja", error=str(e)), 401 if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt index 1e61b30..15976db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,24 @@ beautifulsoup4==4.14.3 +blinker==1.9.0 bs4==0.0.2 +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +click==8.3.1 +cryptography==46.0.3 +Flask==3.1.2 +gunicorn==24.0.0 +idna==3.11 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +packaging==26.0 +pycparser==3.0 +pyspnego==0.12.0 pytz==2025.2 +requests==2.32.5 +requests_ntlm==1.3.0 soupsieve==2.8.2 typing_extensions==4.15.0 +urllib3==2.6.3 +Werkzeug==3.1.5 diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..55d1342 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,94 @@ +:root { + --text: #e3f2fc; + --background: #031621; + --primary: #a1c5da; + --secondary: #6a139b; + --accent: #52ace0; +} + + +body { + font-family: Arial, sans-serif; + color: var(--text); + margin: 0; + background: var(--background); + /* fallback dark color */ +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 200vw; + height: 200vh; + background: radial-gradient(circle at 50% 0%, var(--accent) 0%, var(--background) 70%); + animation: moveLight 30s ease-in-out infinite alternate; + z-index: -1; +} + +@keyframes moveLight { + 0% { + transform: translate(-50%, -50%); + } + + 50% { + transform: translate(-25%, -40%); + } + + 100% { + transform: translate(0%, -50%); + } +} + +.login-box { + position: absolute; + inset: 0px; + width: fit-content; + height: fit-content; + max-width: 100vw; + max-height: 100dvh; + margin: auto; + background: var(--background); + padding: 30px; + border-radius: 8px; + text-align: center; + border: solid 1px var(--accent); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; + width: 300px; +} + +input { + width: 100%; + padding: 8px; + margin: 8px 0; + background-color: var(--background); + color: var(--text); + border: solid 1px var(--accent); + border-radius: 8px; +} + +button { + margin-top: 1rem; + width: 100%; + padding: 10px; + background: var(--accent); + color: var(--background); + font-size: large; + font-weight: bold; + border: none; + cursor: pointer; +} + +button:hover { + background: var(--primary); + color: var(--background); +} + +.error { + color: red; + margin-top: 10px; +} \ No newline at end of file diff --git a/templates/base.jinja b/templates/base.jinja new file mode 100644 index 0000000..e914fc7 --- /dev/null +++ b/templates/base.jinja @@ -0,0 +1,21 @@ + + + +
+ + + + + + + +