IDS_Commute / app.py
Elfsong's picture
Update app.py
99e0e39 verified
# coding: utf-8
# Author: Du Mingzhe (mingzhe@nus.edu.sg)
# Date: 2025/03/22
import os
import re
import json
import requests
import streamlit as st
from datetime import datetime
from bs4 import BeautifulSoup
from streamlit_autorefresh import st_autorefresh
nextbus_token = os.getenv("NEXTBUS_TOKEN")
datamall_token = os.getenv("DATAMALL_TOKEN")
mrt_token = os.getenv("MRT_TOKEN")
mrt_cookie = os.getenv("MRT_COOKIE")
def wide_space_default():
st.set_page_config(layout='wide')
wide_space_default()
count = st_autorefresh(interval=30000)
def get_all_nus_stops():
url = "https://nnextbus.nus.edu.sg/BusStops"
payload = {}
headers = {
'Host': 'nnextbus.nus.edu.sg',
'Content-Type': 'application/json',
'Connection': 'keep-alive',
'Accept': 'application/json',
'User-Agent': 'nusnextbusv2/1 CFNetwork/978.0.7 Darwin/18.7.0',
'Authorization': nextbus_token,
'Accept-Language': 'en-us',
'Accept-Encoding': 'br, gzip, deflate'
}
response = requests.request("GET", url, headers=headers, data=payload)
return response.json()["BusStopsResult"]["busstops"]
def get_nus_bus_arrival(bus_stop_code):
url = f"https://nnextbus.nus.edu.sg/ShuttleService?busstopname={bus_stop_code}"
payload = {}
headers = {
'Host': 'nnextbus.nus.edu.sg',
'Content-Type': 'application/json',
'Connection': 'keep-alive',
'Accept': 'application/json',
'User-Agent': 'nusnextbusv2/1 CFNetwork/978.0.7 Darwin/18.7.0',
'Authorization': nextbus_token,
'Accept-Language': 'en-us',
'Accept-Encoding': 'br, gzip, deflate'
}
response = requests.request("GET", url, headers=headers, data=payload)
return response.json()['ShuttleServiceResult']['shuttles']
def get_lta_bus_arrival(bus_stop_code):
url = f"https://datamall2.mytransport.sg/ltaodataservice/v3/BusArrival?BusStopCode={bus_stop_code}"
payload = {}
headers = {
'AccountKey': datamall_token
}
response = requests.request("GET", url, headers=headers, data=payload)
return response.json()
def get_smrt_train_arrival(station_code):
url = "https://trainarrivalweb.smrt.com.sg/"
payload = f"ScriptManager1=UP1%7CddlStation&stnCode=&stnName=&ddlStation={station_code}&{mrt_token}"
headers = {
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh-HK;q=0.6,zh-TW;q=0.5,zh;q=0.4',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://trainarrivalweb.smrt.com.sg',
'Referer': 'https://trainarrivalweb.smrt.com.sg/',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
'X-MicrosoftAjax': 'Delta=true',
'X-Requested-With': 'XMLHttpRequest',
'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'Cookie': mrt_cookie
}
response = requests.request("POST", url, headers=headers, data=payload)
raw_html = response.text
soup = BeautifulSoup(raw_html, 'html.parser')
tables = soup.find_all("table", id="gvTime")
trains = list()
for table in tables:
time_row = table.find_all("tr")[1]
time_cells = [td.get_text(strip=True) for td in time_row.find_all("td")]
direction_row = table.find_all("tr")[2]
direction_cells = [td.get_text(strip=True) for td in direction_row.find_all("td")]
for eta_text, direction in zip(time_cells, direction_cells):
eta = re.findall(r'\d+', eta_text)
if eta:
trains.append({
"direction": direction,
"eta": eta[0]
})
return trains
# NUS Bus Stops
nus_bus_stops = [
{
"caption": "COM 3",
"name": "COM3",
"LongName": "COM 3",
"ShortName": "COM 3",
"latitude": 1.294431,
"longitude": 103.775217
},
{
"caption": "Opp TCOMS",
"name": "TCOMS-OPP",
"LongName": "Opp TCOMS",
"ShortName": "Opp TCOMS",
"latitude": 1.293789,
"longitude": 103.776715
},
{
"caption": "TCOMS",
"name": "TCOMS",
"LongName": "TCOMS",
"ShortName": "TCOMS",
"latitude": 1.293654,
"longitude": 103.776898
},
{
"caption": "Prince George's Park Foyer",
"name": "PGPR",
"LongName": "Prince George's Park Foyer",
"ShortName": "PGP Foyer",
"latitude": 1.290994,
"longitude": 103.781153
},
]
# Public Bus Stops
public_bus_stops = [
{
'name': "Opp HMK",
'code': "16061"
},
{
'name': "HMK",
'code': "16069"
},
]
# MRT Stations
public_mrt_stations = [
{
'name': "KR MRT",
'code': 'CKRG'
}
]
# Hack the CSS to hide the delta icon
st.write(
"""
<style>
[data-testid="stMetricDelta"] svg {
display: none;
}
</style>
""",
unsafe_allow_html=True,
)
# Title
st.title(":blue[IDS] Commute 🚌")
# Acknowledgement
st.write(":green[[Acknowledgement]] I would like to thank [NextBus](https://nnextbus.nus.edu.sg), [LTA](https://datamall2.mytransport.sg), and [SMRT](https://trainarrivalweb.smrt.com.sg/) for providing data, albeit perhaps unintentionally. If you plan to use their data as well, please be considerate with your network traffic to avoid disrupting their services.")
# Layout
coloumns = [2,1,1,1,1,1,1,1,1,1,1]
number_of_coloumns = len(coloumns)
# NUS Bus
for stop_info in nus_bus_stops:
shuttle_info = get_nus_bus_arrival(stop_info['name'])
buses = list()
for shuttle in shuttle_info:
if "_etas" not in shuttle: continue
for bus in shuttle["_etas"]:
plate = bus["plate"]
eta = bus["eta"]
shuttle_name = shuttle['name']
buses.append({
"shuttle_name": shuttle_name,
"plate": plate,
"eta": eta
})
buses.sort(key=lambda x: x["eta"])
with st.container(border=True):
cols = st.columns(coloumns)
cols[0].metric("NUS Stop", stop_info['name'])
for i, bus in enumerate(buses[:number_of_coloumns-1]):
cols[i+1].metric(bus['plate'], bus["shuttle_name"], str(bus["eta"]))
# Public Bus
for stop_info in public_bus_stops:
bus_info = get_lta_bus_arrival(stop_info['code'])
buses = list()
for shuttle in bus_info["Services"]:
try:
service_no = shuttle["ServiceNo"]
for bus_seq in ['NextBus', 'NextBus2', 'NextBus3']:
bus_type = shuttle[bus_seq]['Type']
bus_load = shuttle[bus_seq]['Load']
arrival_time = datetime.fromisoformat(shuttle[bus_seq]['EstimatedArrival'])
now = datetime.now(arrival_time.tzinfo)
time_diff = arrival_time - now
eta = int(time_diff.total_seconds() / 60)
buses.append({
"service": service_no,
"eta": eta,
"type": f'{bus_type} - {bus_load}'
})
except Exception as e:
pass
buses.sort(key=lambda x: x["eta"])
with st.container(border=True):
cols = st.columns(coloumns)
cols[0].metric("Public Stop", stop_info['name'])
for i, bus in enumerate(buses[:number_of_coloumns-1]):
cols[i+1].metric(bus["type"], bus['service'], bus["eta"])
# SMRT Train
for station in public_mrt_stations:
smrt_data = get_smrt_train_arrival(station['code'])
trains = list()
for train in smrt_data:
trains.append({
"direction": train["direction"],
"eta": int(train["eta"])
})
trains.sort(key=lambda x: x["eta"])
with st.container(border=True):
cols = st.columns(coloumns)
cols[0].metric("MRT Station", station['name'])
for i, train in enumerate(trains[:number_of_coloumns-1]):
cols[i+1].metric(train['direction'], 'CC', str(train['eta']))