ktu-papers-api / timetables.py
GitHub Action
Deploy clean API instance with LFS and README metadata
7f6b634
Raw
History Blame Contribute Delete
25 kB
import os
import json
import re
import PyPDF2
from pydantic import BaseModel
from playwright.sync_api import sync_playwright
from google import genai
from google.genai import types
# --- NEW: FIREBASE IMPORTS ---
import firebase_admin
from firebase_admin import credentials, messaging, db
# -------------------------------------------------------------------
# FIREBASE SETUP
# -------------------------------------------------------------------
firebase_creds_json = os.environ.get('FIREBASE_CREDENTIALS')
if firebase_creds_json:
try:
cred_dict = json.loads(firebase_creds_json)
cred = credentials.Certificate(cred_dict)
firebase_admin.initialize_app(cred, {
'databaseURL': 'https://kerala-timetable-db-default-rtdb.asia-southeast1.firebasedatabase.app'
})
print("✅ Firebase Admin initialized successfully.")
except Exception as e:
print(f"⚠️ Firebase Init Error: {e}")
else:
print("⚠️ FIREBASE_CREDENTIALS not found in environment variables. Push notifications will be skipped.")
def send_push_notification(title, body):
"""Fetches tokens from Firebase and broadcasts the push notification."""
if not firebase_creds_json:
print("⏭️ Skipping Push: No Firebase credentials found.")
return
try:
# Fetch all subscriber tokens from the database
ref = db.reference('subscribers')
subscribers = ref.get()
if not subscribers:
print("📭 No subscribers found in database.")
return
# Extract just the token strings
tokens = list(subscribers.keys())
print(f"📢 Preparing to send push notification to {len(tokens)} devices...")
# Firebase has a limit of 500 tokens per multicast message.
# This chunks the tokens into batches of 500 automatically.
success_total = 0
failure_total = 0
for i in range(0, len(tokens), 500):
batch_tokens = tokens[i:i + 500]
message = messaging.MulticastMessage(
notification=messaging.Notification(
title=title,
body=body
),
tokens=batch_tokens
)
response = messaging.send_multicast(message)
success_total += response.success_count
failure_total += response.failure_count
print(f"🚀 Push Sent! Success: {success_total} | Failed: {failure_total}")
except Exception as e:
print(f"❌ Error sending push notification: {e}")
# -------------------------------------------------------------------
# AI SETUP & SCHEMA
# -------------------------------------------------------------------
client = genai.Client()
class TimetableData(BaseModel):
id: str
title: str
semester: str
categoryBadge: str
type: str
scheme: str
startDate: str
endDate: str
viewLink: str
pdfLink: str
# -------------------------------------------------------------------
# UTILITY FUNCTIONS
# -------------------------------------------------------------------
def create_seo_slug(title):
"""Converts a title into a clean, URL-friendly string (e.g., ktu-btech-s4-exam)"""
slug = title.lower()
slug = re.sub(r'[^a-z0-9]+', '-', slug)
return slug.strip('-')
def generate_html_page(title, pdf_filename, html_output_dir="./timetable_pages"):
"""Injects the scraped data into your HTML template and saves the file."""
os.makedirs(html_output_dir, exist_ok=True)
slug = create_seo_slug(title)
html_filename = f"{slug}.html"
html_filepath = os.path.join(html_output_dir, html_filename)
# Relative path from the HTML folder to the PDF folder
pdf_link = f"../downloads_timetable/{pdf_filename}"
# Your beautiful, advanced HTML template with dynamic placeholders
html_template = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="google-site-verification" content="cPHgzg8741NBFQ5HszgVIPA3EMrKJLlj7IHcyLGM2Lo" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7313827303298932" crossorigin="anonymous"></script>
<title>[[TITLE]] | KTU Timetable</title>
<link rel="canonical" href="https://www.keralatimetable.in/timetable_pages/[[SLUG]].html" />
<meta name="description" content="Download the official [[TITLE]] PDF. Check the latest exam dates, slots, and subjects for the APJAKTU examinations.">
<meta name="keywords" content="[[TITLE]], KTU Time Table, KTU Exam Date PDF Download, Kerala Tech University, ktu latest updates">
<meta name="robots" content="index, follow">
<script src="/tailwind-local.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
.bg-mesh {
background-color: #f8fafc;
background-image: radial-gradient(at 40% 20%, hsla(228,100%,74%,0.15) 0px, transparent 50%),
radial-gradient(at 80% 0%, hsla(189,100%,56%,0.15) 0px, transparent 50%),
radial-gradient(at 0% 50%, hsla(228,100%,74%,0.15) 0px, transparent 50%);
}
.btn-glow { box-shadow: 0 0 20px rgba(79, 70, 229, 0.4); }
</style>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Kerala Timetable",
"url": "https://www.keralatimetable.in/",
"description": "Web application for APJAKTU students providing server status, exam timetables, and academic updates.",
"applicationCategory": "EducationalApplication",
"operatingSystem": "All",
"inLanguage": "en-IN"
}
</script>
<link rel="icon" href="/favicon.png" type="image/png">
<meta property="og:site_name" content="Kerala Timetable">
<meta property="og:image" content="https://www.keralatimetable.in/social-preview.png">
<meta property="og:type" content="website">
</head>
<body class="bg-mesh text-slate-800 antialiased flex flex-col min-h-screen relative overflow-x-hidden">
<div id="navigation-container"></div>
<main class="flex-grow max-w-3xl mx-auto px-4 py-10 w-full flex flex-col items-center">
<div class="bg-white rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-slate-100 p-8 md:p-10 text-center w-full relative overflow-hidden">
<div class="absolute -top-24 -right-24 w-48 h-48 bg-indigo-50 rounded-full blur-3xl opacity-60"></div>
<div class="relative z-10">
<div class="inline-flex items-center justify-center p-3 bg-indigo-50 rounded-2xl mb-5 text-indigo-600">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</div>
<h2 class="text-3xl md:text-4xl font-extrabold text-slate-900 mb-3 tracking-tight">[[TITLE]]</h2>
<p class="text-slate-500 font-medium mb-8">Official Exam Schedule • <strong>APJAKTU</strong></p>
<div class="text-left bg-slate-50 border border-slate-100 p-5 rounded-2xl mb-8 text-sm text-slate-600 shadow-inner">
<p class="mb-3 leading-relaxed"><strong>Important Update:</strong> The <strong>APJ Abdul Kalam Technological University (APJAKTU)</strong> has officially released this latest exam schedule. Download the secure PDF below to verify your specific exam dates and slot codes.</p>
</div>
<div id="download-section" class="py-4 flex flex-col items-center justify-center gap-4">
<div id="loading-container" class="flex flex-col items-center justify-center space-y-4 w-full">
<div class="w-8 h-8 border-4 border-indigo-100 border-t-indigo-600 rounded-full animate-spin"></div>
<p id="timer-text" class="text-lg font-semibold text-slate-700">Connecting to Server in <span id="countdown" class="text-indigo-600 font-bold w-6 inline-block text-center">15</span>s...</p>
</div>
<a id="download-btn" href="#" target="_blank" class="hidden w-full md:w-auto bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-4 px-10 rounded-xl transition-all duration-300 transform hover:-translate-y-1 items-center justify-center gap-2">
<svg class="w-5 h-5 inline-block -mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Download PDF Timetable
</a>
<a id="whatsapp-btn" href="https://api.whatsapp.com/send?text=🚨 *[[TITLE]] is Out!* Download the official PDF directly here: https://www.keralatimetable.in/timetable_pages/[[SLUG]].html" target="_blank" class="hidden w-full md:w-auto bg-[#25D366] hover:bg-[#20bd5a] text-white font-bold py-3 px-8 rounded-xl transition-all duration-300 transform hover:-translate-y-1 items-center justify-center gap-2 shadow-sm">
<svg class="w-5 h-5 inline-block -mt-1" fill="currentColor" viewBox="0 0 24 24"><path d="M12.031 0C5.385 0 0 5.385 0 12.031c0 2.125.553 4.179 1.604 5.99L.492 23.508l5.635-1.478A11.968 11.968 0 0012.031 24c6.646 0 12.031-5.385 12.031-12.031S18.677 0 12.031 0zm3.834 17.202c-.161.455-.938.868-1.332.915-.36.042-.818.121-2.502-.538-2.035-.798-3.344-2.884-3.444-3.018-.1-.134-.823-1.096-.823-2.091 0-.995.518-1.485.698-1.684.18-.198.39-.248.52-.248.13 0 .26.002.378.007.126.005.293-.048.455.34.168.402.571 1.397.621 1.498.05.1.08.218.015.348-.065.13-.098.212-.195.328-.098.116-.207.248-.293.348-.095.108-.195.228-.083.422.112.194.498.825 1.07 1.335.738.658 1.353.86 1.545.955.193.095.305.08.418-.04.113-.12.485-.563.615-.758.13-.195.26-.163.435-.098.175.065 1.107.522 1.297.618.19.095.318.142.365.222.048.08.048.468-.113.923z"/></svg>
Share via WhatsApp
</a>
<a id="whatsapp-channel-btn" href="https://whatsapp.com/channel/0029Vb7zhxw5vKABedfmht0D" target="_blank" class="hidden w-full md:w-auto bg-slate-800 hover:bg-slate-900 text-white font-bold py-3 px-8 rounded-xl transition-all duration-300 transform hover:-translate-y-1 items-center justify-center gap-2 shadow-sm">
<i class="fab fa-whatsapp text-xl"></i>
Follow WhatsApp Channel
</a>
</div>
<div class="mt-12 text-left border-t border-slate-100 pt-8">
<h3 class="text-lg font-bold text-slate-800 mb-4">Frequently Asked Questions</h3>
<div class="space-y-3">
<div class="bg-slate-50 p-4 border border-slate-100 rounded-xl">
<h4 class="font-bold text-slate-700 text-sm">When do these exams start?</h4>
<p class="text-xs text-slate-500 mt-1">Please refer to the official PDF download above for the exact start dates and subject codes for this batch.</p>
</div>
<div class="bg-slate-50 p-4 border border-slate-100 rounded-xl">
<h4 class="font-bold text-slate-700 text-sm">Is this the final timetable or tentative?</h4>
<p class="text-xs text-slate-500 mt-1">This document contains the schedule published directly by the APJ Abdul Kalam Technological University (KTU). We mirror the official portal's data.</p>
</div>
</div>
</div>
<div class="mt-8 pt-8 border-t border-slate-100 text-left">
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3">Related Topics & Searches</h3>
<div class="flex flex-wrap gap-2">
<span class="bg-white border border-slate-200 text-slate-500 text-xs px-3 py-1.5 rounded-md">[[TITLE]]</span>
<span class="bg-white border border-slate-200 text-slate-500 text-xs px-3 py-1.5 rounded-md">KTU Timetable 2026 PDF</span>
<span class="bg-white border border-slate-200 text-slate-500 text-xs px-3 py-1.5 rounded-md">APJAKTU Exam Schedule Download</span>
</div>
</div>
</div>
</div>
</main>
<footer class="mt-auto border-t border-slate-200/60 bg-white/50 py-8 text-center">
<div class="max-w-3xl mx-auto px-4">
<h3 class="text-slate-800 font-bold text-lg mb-2">Kerala Timetable</h3>
<p class="text-slate-500 text-sm leading-relaxed max-w-lg mx-auto">Providing fast and direct access to university resources. We are an independent platform and not officially affiliated with APJ Abdul Kalam Technological University.</p>
<div class="mt-4 space-x-4">
<a href="../about.html" class="text-indigo-600 text-xs font-bold hover:underline">About Us</a>
<a href="https://keralatimetable.stck.me/profile" target="_blank" class="text-indigo-600 text-xs font-bold hover:underline">Support Us</a>
</div>
</div>
</footer>
<script src="../components.js?v=12"></script>
<script>
// Load navigation menu correctly adjusted for the /timetable_pages/ subfolder
loadNavigation('', '../', false);
</script>
<script>
// --- Countdown & Download Logic ---
const pdfLink = "[[PDF_LINK]]";
let timeLeft = 15;
const countdownEl = document.getElementById('countdown');
const downloadBtn = document.getElementById('download-btn');
const whatsappBtn = document.getElementById('whatsapp-btn');
const whatsappChannelBtn = document.getElementById('whatsapp-channel-btn');
const loadingContainer = document.getElementById('loading-container');
const timer = setInterval(() => {
timeLeft--;
countdownEl.textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(timer);
loadingContainer.classList.add('hidden');
// Show Download Button
downloadBtn.href = pdfLink;
downloadBtn.classList.remove('hidden');
downloadBtn.classList.add('flex', 'btn-glow');
// Show WhatsApp Buttons
whatsappBtn.classList.remove('hidden');
whatsappBtn.classList.add('flex');
whatsappChannelBtn.classList.remove('hidden');
whatsappChannelBtn.classList.add('flex');
}
}, 1000);
</script>
</body>
</html>"""
final_html = html_template.replace('[[TITLE]]', title).replace('[[PDF_LINK]]', pdf_link).replace('[[SLUG]]', slug)
with open(html_filepath, 'w', encoding='utf-8') as f:
f.write(final_html)
return html_filepath, f"timetable_pages/{html_filename}"
# -------------------------------------------------------------------
# AI EXTRACTION FUNCTION (UPDATED TO READ FULL PDF)
# -------------------------------------------------------------------
def extract_dashboard_data_with_ai(pdf_path, original_title, pdf_link, html_link):
"""Reads the PDF text and uses Gemini to extract structured dates/tags."""
print(f"🧠 Asking AI to analyze: {original_title}...")
raw_text = ""
try:
with open(pdf_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
# FIX: Read every single page of the PDF to ensure we catch the final exams
for i in range(len(reader.pages)):
text = reader.pages[i].extract_text()
if text:
raw_text += text + "\n"
except Exception as e:
print(f"❌ Error reading PDF: {e}")
return None
# Prompt updated to command scanning the entire document
prompt = f"""
You are an expert data extractor. Analyze this KTU exam timetable text.
Original Long Title: {original_title}
Tasks based on the schema:
1. id: Create a short, unique snake_case ID (e.g., "mca_s2_may2026").
2. title: Create a short, clean title (e.g., "MCA S2 Regular/Supply May 2026").
3. startDate: Scan the ENTIRE document and find the VERY FIRST exam date and time. Format: YYYY-MM-DDTHH:MM:SS
4. endDate: Scan the ENTIRE document and find the VERY LAST/LATEST exam date and time. Format: YYYY-MM-DDTHH:MM:SS
5. semester: Extract the specific semester (e.g., 'S4', 'S1', 'S1-S5').
6. categoryBadge: Extract a category for filtering (e.g., 'S1', 'S3', 'S5', 'S7').
7. type: 'Regular', 'Supply', 'Honours', or 'Regular/Supply'.
8. scheme: (e.g., '2019 Scheme', '2024 Scheme').
9. pdfLink: Return exactly "{pdf_link}".
10. viewLink: Return exactly "{html_link}".
Messy PDF Text:
{raw_text[:40000]}
"""
try:
response = client.models.generate_content(
model='gemini-3.1-flash-lite',
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=TimetableData,
temperature=0.0, # Lowered for maximum mathematical strictness
),
)
ai_data = json.loads(response.text)
print(f"✅ AI Extraction Successful! Start: {ai_data['startDate']} | End: {ai_data['endDate']}")
return ai_data
except Exception as e:
print(f"❌ AI Extraction Failed: {e}")
return None
# -------------------------------------------------------------------
# CONFIG UPDATER FUNCTION (UNTOUCHED)
# -------------------------------------------------------------------
def update_timetable_config(ai_data_dict, config_path="timetable-config.js"):
"""Safely appends the AI's JSON object into window.timetablesData."""
if not ai_data_dict: return
try:
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
content = f.read()
else:
content = "window.timetablesData = [\n];"
clean_json = json.dumps(ai_data_dict, indent=4)
last_bracket_idx = content.rfind(']')
if last_bracket_idx != -1:
array_content = content[content.find('[')+1:last_bracket_idx].strip()
if array_content:
new_content = content[:last_bracket_idx] + ",\n" + clean_json + "\n" + content[last_bracket_idx:]
else:
new_content = content[:last_bracket_idx] + "\n" + clean_json + "\n" + content[last_bracket_idx:]
with open(config_path, "w", encoding="utf-8") as f:
f.write(new_content)
print("💾 Dashboard config updated!")
else:
print("❌ Error: Could not find closing bracket in config file.")
except Exception as e:
print(f"❌ Error updating config: {e}")
# -------------------------------------------------------------------
# MAIN SCRAPER FUNCTION
# -------------------------------------------------------------------
def scrape_exam_timetables(pdf_dir="./downloads_timetable", html_dir="./timetable_pages"):
os.makedirs(pdf_dir, exist_ok=True)
os.makedirs(html_dir, exist_ok=True)
with sync_playwright() as p:
print("Launching headless Chromium browser...")
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
page = context.new_page()
timetable_metadata = []
def on_response(response):
if "anon/timetable" in response.url:
try:
data = response.json()
if 'content' in data:
for idx, item in enumerate(data['content']):
timetable_metadata.append({
'index': idx,
'id': item.get('id'),
'title': item.get('timeTableTitle') or item.get('title'),
'date': item.get('createdDate', '').split('T')[0],
'encryptId': item.get('encryptId'),
'fileName': item.get('fileName')
})
print(f"Captured {len(timetable_metadata)} exam timetables from the API response.")
except Exception as e:
print(f"Error parsing API response: {e}")
page.on("response", on_response)
print("Navigating to KTU Exam Timetable page...")
page.goto("https://ktu.edu.in/exam/timetable", wait_until="domcontentloaded", timeout=60000)
page.wait_for_timeout(5000)
downloaded_files = []
print(f"\nProcessing {len(timetable_metadata)} timetables:")
for tb in timetable_metadata:
title = tb['title']
encrypt_id = tb['encryptId']
if not encrypt_id:
continue
predicted_filename = f"{tb['date']}_{tb['fileName']}"
save_path = os.path.join(pdf_dir, predicted_filename)
if os.path.exists(save_path):
print(f"Skipping (Already Exists): '{title}'")
continue
print(f"\nDownloading NEW file: '{title}'")
try:
button_selector = f"button[value='{encrypt_id}']"
button = page.locator(button_selector)
if button.count() > 0:
with page.expect_download(timeout=15000) as download_info:
button.click()
download = download_info.value
filename = f"{tb['date']}_{tb['fileName'] or download.suggested_filename}"
save_path = os.path.join(pdf_dir, filename)
download.save_as(save_path)
print(f" -> PDF Saved: {filename}")
# 1. Generate the HTML Sub-page
html_filepath, relative_html_link = generate_html_page(title, filename, html_dir)
print(f" -> HTML Generated: {html_filepath}")
# 2. Extract Data via Gemini AI
relative_pdf_link = f"downloads_timetable/{filename}"
ai_extracted_data = extract_dashboard_data_with_ai(save_path, title, relative_pdf_link, relative_html_link)
# 3. Update the JavaScript Config
update_timetable_config(ai_extracted_data)
# --- NEW: TRIGGER PUSH NOTIFICATION ---
if ai_extracted_data:
short_title = ai_extracted_data.get('title', title)
send_push_notification(
title="🚨 New KTU Timetable Published!",
body=f"{short_title} is now available on the dashboard."
)
downloaded_files.append({
"title": title,
"date": tb['date'],
"filename": filename,
})
else:
print(f" -> Error: Download button not found on UI.")
except Exception as e:
print(f" -> Failed to process download: {e}")
browser.close()
return downloaded_files
if __name__ == "__main__":
results = scrape_exam_timetables()
print("\n" + "="*50 + "\n NEW DOWNLOADS SUMMARY\n" + "="*50)
if not results:
print("No new timetables found today.")
else:
for r in results:
print(f"[{r['date']}] {r['title']} -> Page & Config Updated (Push Sent)")