bring in code made by che

This commit is contained in:
Ugric
2026-01-23 01:39:54 +00:00
parent d961d1f80c
commit ecd728fe27
17 changed files with 187 additions and 32 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/Timtabla.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Timtabla.iml" filepath="$PROJECT_DIR$/.idea/Timtabla.iml" />
</modules>
</component>
</project>

0
db/init.py Normal file
View File

73
main.py
View File

@@ -3,6 +3,9 @@ from datetime import datetime, timezone
import pytz import pytz
import hashlib import hashlib
import os import os
from flask import Flask, request, send_file, render_template
import requests
from requests_ntlm import HttpNtlmAuth
# ------------------------------------------------------------ # ------------------------------------------------------------
# Config # Config
@@ -12,15 +15,12 @@ RESPONSE_FILE = "response.txt"
OLD_CALENDAR = "old_calendar.ics" OLD_CALENDAR = "old_calendar.ics"
NEW_CALENDAR = "calendar.ics" NEW_CALENDAR = "calendar.ics"
LOCAL_TZ = pytz.timezone("Europe/London") LOCAL_TZ = pytz.timezone("Europe/London")
URL = "https://webapp.coventry.ac.uk/Timetable-main/Timetable/Current"
# ------------------------------------------------------------ app = Flask(__name__)
# Parsing timetable JS
# ------------------------------------------------------------
def parse_events(events_data): def parse_events(events_data):
# Replace JS date objects
events_data = events_data.replace("new Date", "") events_data = events_data.replace("new Date", "")
cleaned_data = "" cleaned_data = ""
for line in events_data.split("\n"): for line in events_data.split("\n"):
@@ -67,9 +67,6 @@ def get_events_data_from_file(path):
return source.split("events:")[1].split("]")[0] + "]" return source.split("events:")[1].split("]")[0] + "]"
# ------------------------------------------------------------
# ICS helpers
# ------------------------------------------------------------
def ics_time(dt): def ics_time(dt):
return dt.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ") return dt.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
@@ -89,13 +86,9 @@ def create_ics_event(event):
"end": event["end"], "end": event["end"],
} }
# ------------------------------------------------------------
# Load existing calendar (UIDs only)
# ------------------------------------------------------------
def load_existing_uids(path): def load_existing_uids(path):
uids = set() uids = set()
if not os.path.exists(path): if not os.path.exists(path):
return uids return uids
@@ -109,31 +102,30 @@ def load_existing_uids(path):
if line == "BEGIN:VEVENT": if line == "BEGIN:VEVENT":
current_uid = None current_uid = None
cancelled = False cancelled = False
elif line.startswith("UID:"): elif line.startswith("UID:"):
current_uid = line[4:] current_uid = line[4:]
elif line == "STATUS:CANCELLED": elif line == "STATUS:CANCELLED":
cancelled = True cancelled = True
elif line == "END:VEVENT" and current_uid and not cancelled: elif line == "END:VEVENT" and current_uid and not cancelled:
uids.add(current_uid) uids.add(current_uid)
return uids return uids
# ------------------------------------------------------------ def fetch_timetable(username, password):
# Main session = requests.Session()
# ------------------------------------------------------------ session.auth = HttpNtlmAuth(username, password)
def main(): r = session.get(URL)
r.raise_for_status()
with open("response.txt", "w", encoding="utf-8") as f:
f.write(r.text)
def build_calendar():
events_js = get_events_data_from_file(RESPONSE_FILE) events_js = get_events_data_from_file(RESPONSE_FILE)
parsed_events = parse_events(events_js) parsed_events = parse_events(events_js)
new_events = [ new_events = [create_ics_event(e) for e in parsed_events if e]
create_ics_event(e)
for e in parsed_events
if e
]
old_uids = load_existing_uids(OLD_CALENDAR) old_uids = load_existing_uids(OLD_CALENDAR)
new_uids = {e["uid"] for e in new_events} new_uids = {e["uid"] for e in new_events}
@@ -147,7 +139,6 @@ def main():
"CALSCALE:GREGORIAN", "CALSCALE:GREGORIAN",
] ]
# Add / update events
for ev in new_events: for ev in new_events:
lines.extend([ lines.extend([
"BEGIN:VEVENT", "BEGIN:VEVENT",
@@ -160,7 +151,6 @@ def main():
"END:VEVENT", "END:VEVENT",
]) ])
# Cancel removed events
for uid in old_uids - new_uids: for uid in old_uids - new_uids:
lines.extend([ lines.extend([
"BEGIN:VEVENT", "BEGIN:VEVENT",
@@ -174,12 +164,31 @@ def main():
with open(NEW_CALENDAR, "w", encoding="utf-8") as f: with open(NEW_CALENDAR, "w", encoding="utf-8") as f:
f.write("\n".join(lines)) f.write("\n".join(lines))
print(f"Added / updated: {len(new_events)}")
print(f"Removed: {len(old_uids - new_uids)}")
print(f"Wrote {NEW_CALENDAR}")
# ------------------------------------------------------------ # ------------------------------------------------------------
# Flask
# ------------------------------------------------------------
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login-and-download", methods=["POST"])
def login_and_download():
data = request.get_json()
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"
)
except Exception as e:
return str(e), 400
if __name__ == "__main__": if __name__ == "__main__":
main() app.run(debug=True)

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/birdforfavicon.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
static/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
static/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 524 KiB

21
static/site.webmanifest Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Timtabla (Cov Uni Timetable Converter)",
"short_name": "Timtabla",
"icons": [
{
"src": "/images/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
templates/.DS_Store vendored Normal file

Binary file not shown.

96
templates/index.html Normal file
View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96x96.png') }}">
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<meta name="apple-mobile-web-app-title" content="Timtabla">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
<meta charset="UTF-8">
<title>Timtabla</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #03551B, #0b3c5d);
}
.login-box {
background:white;
padding:30px;
border-radius:8px;
box-shadow:0 0 10px rgba(0,0,0,0.1);
width:300px;
}
input {
width:100%;
padding:8px;
margin:8px 0;
}
button {
width:100%;
padding:10px;
background:#0066cc;
color:white;
border:none;
cursor:pointer;
}
button:hover {
background:#004999;
}
.error {
color:red;
margin-top:10px;
}
</style>
</head>
<body>
<div class="login-box">
<h2>Login to Download</h2>
<input type="text" id="username" placeholder="Username" required>
<input type="password" id="password" placeholder="Password" required>
<button onclick="loginAndDownload()">Login & Download</button>
<div class="error" id="errorMsg"></div>
</div>
<script>
async function loginAndDownload() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const errorMsg = document.getElementById("errorMsg");
errorMsg.textContent = "";
try {
const response = await fetch("/login-and-download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error("Invalid login or server error");
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "timetable.ics";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
errorMsg.textContent = err.message;
}
}
</script>
</body>
</html>