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