generated from nathanwoodburn/python-webserver-template
322 lines
9.0 KiB
Python
322 lines
9.0 KiB
Python
from functools import cache
|
|
import json
|
|
from flask import (
|
|
Flask,
|
|
make_response,
|
|
redirect,
|
|
request,
|
|
jsonify,
|
|
render_template,
|
|
send_from_directory,
|
|
send_file,
|
|
Response
|
|
)
|
|
import os
|
|
import json
|
|
import requests
|
|
from datetime import datetime, timedelta
|
|
import dotenv
|
|
from pytz import timezone
|
|
import pytz
|
|
import re
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import io
|
|
import urllib.parse
|
|
|
|
|
|
dotenv.load_dotenv()
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
def find(name, path):
|
|
for root, dirs, files in os.walk(path):
|
|
if name in files:
|
|
return os.path.join(root, name)
|
|
|
|
# Assets routes
|
|
@app.route("/assets/<path:path>")
|
|
def send_assets(path):
|
|
if path.endswith(".json"):
|
|
return send_from_directory(
|
|
"templates/assets", path, mimetype="application/json"
|
|
)
|
|
|
|
if os.path.isfile("templates/assets/" + path):
|
|
return send_from_directory("templates/assets", path)
|
|
|
|
# Try looking in one of the directories
|
|
filename: str = path.split("/")[-1]
|
|
if (
|
|
filename.endswith(".png")
|
|
or filename.endswith(".jpg")
|
|
or filename.endswith(".jpeg")
|
|
or filename.endswith(".svg")
|
|
):
|
|
if os.path.isfile("templates/assets/img/" + filename):
|
|
return send_from_directory("templates/assets/img", filename)
|
|
if os.path.isfile("templates/assets/img/favicon/" + filename):
|
|
return send_from_directory("templates/assets/img/favicon", filename)
|
|
|
|
return render_template("404.html"), 404
|
|
|
|
|
|
# region Special routes
|
|
@app.route("/favicon.png")
|
|
def faviconPNG():
|
|
return send_from_directory("templates/assets/img", "favicon.png")
|
|
|
|
|
|
@app.route("/.well-known/<path:path>")
|
|
def wellknown(path):
|
|
# Try to proxy to https://nathan.woodburn.au/.well-known/
|
|
req = requests.get(f"https://nathan.woodburn.au/.well-known/{path}")
|
|
return make_response(
|
|
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
)
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region Main routes
|
|
@app.route("/")
|
|
def index():
|
|
from_tz = request.args.get('from_tz', '')
|
|
time = request.args.get('time', '')
|
|
to_tzs = request.args.get('to_tzs', '')
|
|
|
|
# Generate the OG image URL based on the parameters
|
|
# Make sure to encode the parameters
|
|
from_tz = urllib.parse.quote(from_tz)
|
|
time = urllib.parse.quote(time)
|
|
to_tzs = urllib.parse.quote(to_tzs)
|
|
|
|
|
|
og_image = f"og_image?from_tz={from_tz}&time={time}&to_tzs={to_tzs}"
|
|
|
|
return render_template("index.html", og_image=og_image)
|
|
|
|
# Mapping of short timezone names to full names
|
|
SHORT_TZ_MAP = {
|
|
"GMT": "Etc/GMT",
|
|
"PST": "America/Los_Angeles",
|
|
"PDT": "America/Los_Angeles",
|
|
"MST": "America/Denver",
|
|
"MDT": "America/Denver",
|
|
"CST": "America/Chicago",
|
|
"CDT": "America/Chicago",
|
|
"EST": "America/New_York",
|
|
"EDT": "America/New_York",
|
|
"AKST": "America/Anchorage",
|
|
"AKDT": "America/Anchorage",
|
|
"HST": "Pacific/Honolulu",
|
|
"HDT": "Pacific/Honolulu",
|
|
"AST": "America/Puerto_Rico",
|
|
"NST": "America/St_Johns",
|
|
"BST": "Europe/London",
|
|
"CET": "Europe/Paris",
|
|
"CEST": "Europe/Paris",
|
|
"EET": "Europe/Athens",
|
|
"EEST": "Europe/Athens",
|
|
"MSK": "Europe/Moscow",
|
|
"MSD": "Europe/Moscow",
|
|
"IST": "Asia/Kolkata",
|
|
"PKT": "Asia/Karachi",
|
|
"WIB": "Asia/Jakarta",
|
|
"WITA": "Asia/Makassar",
|
|
"WIT": "Asia/Jayapura",
|
|
"CST": "Asia/Shanghai",
|
|
"JST": "Asia/Tokyo",
|
|
"KST": "Asia/Seoul",
|
|
"AEDT": "Australia/Sydney",
|
|
"AEST": "Australia/Sydney",
|
|
"ACST": "Australia/Adelaide",
|
|
"ACDT": "Australia/Adelaide",
|
|
"AWST": "Australia/Perth",
|
|
}
|
|
|
|
def get_full_timezone(tz):
|
|
match = re.match(r"^UTC([+-]\d{1,2}):?(\d{2})?$", tz)
|
|
if match:
|
|
hours = int(match.group(1))
|
|
minutes = int(match.group(2)) if match.group(2) else 0
|
|
return pytz.FixedOffset(hours * 60 + minutes)
|
|
full_tz = SHORT_TZ_MAP.get(tz, tz)
|
|
try:
|
|
return pytz.timezone(full_tz) if isinstance(full_tz, str) else None
|
|
except pytz.UnknownTimeZoneError:
|
|
return None
|
|
|
|
@app.route("/api/v1/convert", methods=["POST"])
|
|
def convert():
|
|
data = request.json
|
|
from_tz = get_full_timezone(data.get("from_tz").replace("_", "/"))
|
|
to_tz = get_full_timezone(data.get("to_tz").replace("_", "/"))
|
|
time = data.get("time")
|
|
|
|
if from_tz is None or to_tz is None:
|
|
return jsonify({"error": "Invalid timezone"}), 400
|
|
|
|
# Parse the input time
|
|
try:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
|
|
except ValueError:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M")
|
|
|
|
try:
|
|
# Get the timezone
|
|
from_tz_time = from_tz.localize(input_time)
|
|
|
|
# Convert the time
|
|
to_tz_time = from_tz_time.astimezone(to_tz)
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
return jsonify(
|
|
{
|
|
"from": from_tz_time.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
"to": to_tz_time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
)
|
|
|
|
@app.route("/api/v1/convert_multiple", methods=["POST"])
|
|
def convert_multiple():
|
|
data = request.json
|
|
from_tz = get_full_timezone(data.get("from_tz").replace("_", "/"))
|
|
time = data.get("time")
|
|
to_tz_list = [(tz, get_full_timezone(tz.replace("_", "/"))) for tz in data.get("to_tzs") if len(tz) > 0]
|
|
|
|
if from_tz is None or any(tz[1] is None for tz in to_tz_list):
|
|
return jsonify({"error": "Invalid timezone"}), 400
|
|
|
|
# Parse the input time
|
|
try:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
|
|
except ValueError:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M")
|
|
|
|
try:
|
|
# Get the timezone
|
|
from_tz_time = from_tz.localize(input_time)
|
|
|
|
# Convert the time to each timezone in the list
|
|
results = {}
|
|
for original_tz, tz in to_tz_list:
|
|
to_tz_time = from_tz_time.astimezone(tz)
|
|
results[original_tz] = f"{to_tz_time.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
return jsonify(results)
|
|
|
|
@app.route("/og_image")
|
|
def og_image():
|
|
from_tz = request.args.get('from_tz', 'Unknown')
|
|
time = request.args.get('time', 'Unknown')
|
|
to_tzs = request.args.get('to_tzs', 'Unknown')
|
|
|
|
# Load the background image
|
|
img_path = "templates/assets/img/og_image.webp"
|
|
img = Image.open(img_path).convert("RGBA")
|
|
d = ImageDraw.Draw(img)
|
|
|
|
# Convert time format to 12-hour format and add AM/PM
|
|
try:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
|
|
except ValueError:
|
|
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M")
|
|
time = input_time.strftime("%I:%M %p")
|
|
|
|
|
|
|
|
# Prepare text
|
|
text = f"{time} {from_tz.replace("_","/")}"
|
|
|
|
from_tz = get_full_timezone(from_tz.replace("_", "/"))
|
|
|
|
# Check if multiple timezones are provided
|
|
if "," in to_tzs:
|
|
to_tzs = to_tzs.split(",")
|
|
to_tz_list = [(tz, get_full_timezone(tz.replace("_", "/"))) for tz in to_tzs if len(tz) > 0]
|
|
else:
|
|
to_tz_list = [(to_tzs, get_full_timezone(to_tzs.replace("_", "/")))]
|
|
|
|
if from_tz is None or any(tz[1] is None for tz in to_tz_list):
|
|
return send_from_directory("templates/assets/img", "og_image.webp")
|
|
|
|
|
|
|
|
for tz in to_tz_list:
|
|
to_tz = tz[1]
|
|
to_tz_time = from_tz.localize(input_time).astimezone(to_tz)
|
|
text += f"\n{to_tz_time.strftime('%I:%M %p')} {tz[0].replace('_','/')}"
|
|
|
|
|
|
# Use font from assets
|
|
font_path = "templates/assets/fonts/Orbitron-VariableFont_wght.ttf"
|
|
font_size = 180
|
|
# Shrink font size if text is too long
|
|
if (max(len(line) for line in text.split("\n")) > 13):
|
|
font_size = 150
|
|
|
|
font_size -= (len(text.split("\n")) - 4) * 10
|
|
|
|
|
|
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
|
|
text_bbox = d.textbbox((0, 0), text, font=font)
|
|
text_width = text_bbox[2] - text_bbox[0]
|
|
text_height = text_bbox[3] - text_bbox[1]
|
|
|
|
# Calculate text position
|
|
x = (img.width - text_width) / 2
|
|
y = (img.height - text_height) / 2
|
|
|
|
# Add text to the image
|
|
d.text((x, y), text, fill=(255, 255, 255), font=font, align="center")
|
|
|
|
# Save the image to a BytesIO object in WebP format
|
|
img_io = io.BytesIO()
|
|
img.save(img_io, 'WEBP')
|
|
img_io.seek(0)
|
|
|
|
return Response(img_io, mimetype='image/webp')
|
|
|
|
@app.route("/<path:path>")
|
|
def catch_all(path: str):
|
|
if os.path.isfile("templates/" + path):
|
|
return render_template(path)
|
|
|
|
# Try with .html
|
|
if os.path.isfile("templates/" + path + ".html"):
|
|
return render_template(path + ".html")
|
|
|
|
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
|
return render_template(path.strip("/") + ".html")
|
|
|
|
# Try to find a file matching
|
|
if path.count("/") < 1:
|
|
# Try to find a file matching
|
|
filename = find(path, "templates")
|
|
if filename:
|
|
return send_file(filename)
|
|
|
|
return render_template("404.html"), 404
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
# region Error Catching
|
|
# 404 catch all
|
|
@app.errorhandler(404)
|
|
def not_found(e):
|
|
return render_template("404.html"), 404
|
|
|
|
|
|
# endregion
|
|
if __name__ == "__main__":
|
|
app.run(debug=True, port=5000, host="0.0.0.0")
|