bring in code made by che
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
8
.idea/Timtabla.iml
generated
Normal 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
@@ -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
@@ -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
73
main.py
@@ -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
|
After Width: | Height: | Size: 21 KiB |
BIN
static/birdforfavicon.avif
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
static/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
static/favicon.svg
Normal file
|
After Width: | Height: | Size: 524 KiB |
21
static/site.webmanifest
Normal 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"
|
||||||
|
}
|
||||||
BIN
static/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
templates/.DS_Store
vendored
Normal file
96
templates/index.html
Normal 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>
|
||||||