Created
May 5, 2025 17:35
-
-
Save LutfiTekin/0b8c1251fff88fcac210b76ebf26fa8e to your computer and use it in GitHub Desktop.
load departures from given station using deutsche bahn api
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -euo pipefail | |
# ---------------------------------------------------------------- | |
# departures.sh β fetch scheduled departures via Timetables 'plan' endpoint only | |
# Includes ICE, RE, S-Bahn (S), buses, etc. | |
# Supports filtering by ICE, RE, S and a rolling window in hours | |
# Deduplicates entries and limits output count | |
# Adds color highlighting: time (black), train number (red), destination (yellow) | |
# ---------------------------------------------------------------- | |
# ANSI color codes | |
BLACK='\033[0;30m' | |
RED='\033[0;31m' | |
YELLOW='\033[0;33m' | |
NC='\033[0m' # No Color | |
# Base URL for Timetables API | |
BASE="https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1" | |
# Ensure credentials are set | |
: "${DB_CLIENT_ID:?Please set DB_CLIENT_ID}" | |
: "${DB_API_KEY:?Please set DB_API_KEY}" | |
# Defaults | |
STATION="Berlin" | |
FILTER="" # ICE | RE | S | |
WINDOW_HOURS=2 # lookahead window (hours) | |
MAX_ENTRIES=10 # maximum departures to list | |
# Parse CLI arguments | |
while [[ $# -gt 0 ]]; do | |
case "$1" in | |
--station) STATION="$2"; shift 2;; | |
--filter) FILTER="$2"; shift 2;; | |
--window) WINDOW_HOURS="$2"; shift 2;; | |
--limit) MAX_ENTRIES="$2"; shift 2;; | |
*) echo "Usage: $0 --station \"Name\" [--filter ICE|RE|S] [--window hours] [--limit count]" >&2; exit 1;; | |
esac | |
done | |
# URL-encode station name | |
ENC_STATION=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$STATION") | |
# 1) Lookup station EVA via XML | |
echo "π Looking up station: $STATION" >&2 | |
station_xml=$(curl -s \ | |
-H "Accept: application/xml" \ | |
-H "DB-Client-Id: $DB_CLIENT_ID" \ | |
-H "DB-Api-Key: $DB_API_KEY" \ | |
"$BASE/station/$ENC_STATION") | |
eva=$(xmllint --xpath 'string(//station/@eva)' - <<<"$station_xml" 2>/dev/null || echo) | |
NAME=$(xmllint --xpath 'string(//station/@name)' - <<<"$station_xml" 2>/dev/null || echo) | |
if [[ -z "$eva" ]]; then | |
echo "β Station not found: $STATION" >&2 | |
exit 1 | |
fi | |
NAME=${NAME:-$STATION} | |
echo "β Station: $NAME (EVA: $eva)" >&2 | |
# 2) Compute time window | |
t_now=$(date +%s) | |
if date -v+${WINDOW_HOURS}H >/dev/null 2>&1; then | |
t_end=$(date -v+${WINDOW_HOURS}H +%s) | |
else | |
t_end=$(date -d "+${WINDOW_HOURS} hours" +%s) | |
fi | |
# 3) Fetch and combine 'plan' for each hour in window | |
echo "π Fetching schedule for next ${WINDOW_HOURS}h..." >&2 | |
date_tag=$(date '+%y%m%d') | |
tmpfile=$(mktemp) | |
echo '<root>' > "$tmpfile" | |
for ((h=0; h<=WINDOW_HOURS; h++)); do | |
if date -v+${h}H >/dev/null 2>&1; then hr=$(date -v+${h}H '+%H'); else hr=$(date -d "+${h} hours" '+%H'); fi | |
curl -s \ | |
-H "Accept: application/xml" \ | |
-H "DB-Client-Id: $DB_CLIENT_ID" \ | |
-H "DB-Api-Key: $DB_API_KEY" \ | |
"$BASE/plan/$eva/$date_tag/$hr" >> "$tmpfile" | |
done | |
echo '</root>' >> "$tmpfile" | |
# 4) Extract departures and arrivals entries | |
entries=$(sed 's/<?xml[^>]*>//g' "$tmpfile" \ | |
| xmlstarlet sel -T -t \ | |
-m "//s/dp" -v "concat(@pt,'|',@pp,'|',../tl/@c,'|',../tl/@n,'|',@ppth)" -n \ | |
-m "//s/ar" -v "concat(@pt,'|',@pp,'|',../tl/@c,'|',../tl/@n,'|',@ppth)" -n) | |
rm "$tmpfile" | |
# 5) Deduplicate while preserving order | |
entries=$(printf "%s\n" "$entries" | awk -F'|' '!seen[$1"|"$3"|"$4"|"$5"|"$2]++') | |
# 6) Output header | |
echo | |
echo "π Next departures from $NAME (window=${WINDOW_HOURS}h)${FILTER:+, filter $FILTER}:" | |
# 7) Iterate and display up to MAX_ENTRIES with color | |
count=0 | |
printf "%s\n" "$entries" | while IFS='|' read -r pt pp cat num path; do | |
[[ ! $pt =~ ^[0-9]{10}$ ]] && continue | |
[[ -n "$FILTER" && "$cat" != "$FILTER" ]] && continue | |
# Parse timestamp | |
year="20${pt:0:2}"; mon="${pt:2:2}"; day="${pt:4:2}"; hr="${pt:6:2}"; mn="${pt:8:2}" | |
ts_fmt="$year-$mon-$day $hr:$mn" | |
if date -j >/dev/null 2>&1; then | |
epoch=$(date -j -f "%Y-%m-%d %H:%M" "$ts_fmt" +%s) | |
else | |
epoch=$(date -d "$ts_fmt" +%s) | |
fi | |
(( epoch < t_now || epoch > t_end )) && continue | |
dest="${path##*|}" | |
# Color: time=black, train number=red, destination=yellow | |
printf "%b - %s %b%s%b to %b%s%b (Platform: %s)\n" \ | |
"${BLACK}${ts_fmt}${NC}" "$cat" \ | |
"${RED}" "$num" "${NC}" \ | |
"${YELLOW}" "$dest" "${NC}" "${pp:-N/A}" | |
((++count>=MAX_ENTRIES)) && break | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment