tz-converter/server.py

231 lines
6.4 KiB
Python

from functools import cache
import json
from flask import (
Flask,
make_response,
redirect,
request,
jsonify,
render_template,
send_from_directory,
send_file,
)
import os
import json
import requests
from datetime import datetime, timedelta
import dotenv
from pytz import timezone
import pytz
import re
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():
return render_template("index.html")
# 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)
return pytz.timezone(full_tz) if isinstance(full_tz, str) else 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 %Z"),
}
)
@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)
offset = to_tz_time.strftime('%z')
offset_formatted = f"UTC{offset[:3]}:{offset[3:]}" if offset else ""
tz_name = to_tz_time.tzname() if to_tz_time.tzname() else offset_formatted
results[original_tz] = f"{to_tz_time.strftime('%Y-%m-%d %H:%M:%S')} {tz_name}"
except Exception as e:
return jsonify({"error": str(e)}), 400
return jsonify(results)
@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")