tz-converter/server.py

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")