import threading import time import re from os import path, listdir from datetime import date from configparser import ConfigParser from urllib.request import urlopen import sass from flask import Flask, redirect, render_template, request, url_for, g, Response # ---------- ENV VARS -------------------------------------------------------- STATUS_URL = "https://builds.kolibrios.org/status.html" STATUS_FETCH_DELAY_SEC = 300 # 5 minutes # ---------- APP CONFIG ------------------------------------------------------ app = Flask(__name__) if app.debug: css = sass.compile(filename="static/style.scss") with open("static/style.css", "w", encoding="utf-8") as f: f.write(css) # ---------- LATEST COMMIT DATE (MINIMAL ADD-ON) ----------------------------- autobuild_date = "DD.MM.YYYY" autobuild_vers = "0.0.0.0+0000-0000000" def _refresh_build_date_once(): global autobuild_date, autobuild_vers try: from urllib.request import Request, urlopen import re req = Request( STATUS_URL, headers={ "User-Agent": "Mozilla/5.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }, ) with urlopen(req, timeout=10) as r: html = r.read().decode( r.headers.get_content_charset() or "utf-8", "replace" ) rows = re.findall(r"(]*>.*?)", html, flags=re.I | re.S) if not rows: return last_commit_ver = None for row in rows: cls = re.search(r'class\s*=\s*"([^"]*)"', row, flags=re.I) classes = cls.group(1).lower() if cls else "" text = re.sub(r"<[^>]+>", " ", row) if "commit" in classes: mver = re.search( r"\b(\d+\.\d+\.\d+\.\d+\+\d{3,8}-[0-9a-fA-F]{7,40})\b", row ) if mver: last_commit_ver = mver.group(1) elif "success" in classes: mts = re.search( r"\b(\d{4})\.(\d{2})\.(\d{2})\s+\d{2}:\d{2}:\d{2}\b", text ) if not mts: mds = re.search(r"\b(\d{2})\.(\d{2})\.(\d{4})\b", text) if mds: autobuild_date = f"{mds.group(1)}.{mds.group(2)}.{mds.group(3)}" else: return else: y, mo, d = mts.groups() autobuild_date = f"{d}.{mo}.{y}" if last_commit_ver: autobuild_vers = last_commit_ver return except Exception: pass def _updater_loop(): while True: _refresh_build_date_once() time.sleep(STATUS_FETCH_DELAY_SEC) _started = False _refresh_build_date_once() # Flask 3.x fix: start updater lazily on first request (since before_first_request is removed) _updater_lock = threading.Lock() @app.before_request def _ensure_updater_started(): global _started if not _started: with _updater_lock: if not _started: _started = True threading.Thread(target=_updater_loop, daemon=True).start() @app.context_processor def _inject_autobuild_vers(): return {'autobuild_vers': autobuild_vers} @app.context_processor def _inject_autobuild_date(): return {'autobuild_date': autobuild_date} # ---------- LOCALES FUNCTIONS ----------------------------------------------- def load_all_locales(): translations = {} locales_dir = "locales" locales_code_default = ('en', 'ru', 'es') locales_code_extra = [] locales_code = () for filename in listdir(locales_dir): if filename.endswith(".ini"): cp = ConfigParser() lang = path.splitext(filename)[0] with open(path.join(locales_dir, filename), encoding="utf-8") as f: cp.read_file(f) if lang not in locales_code_default: locales_code_extra.append(lang) translations[lang] = { section: dict(cp[section]) for section in cp.sections() } locales_code = locales_code_default + tuple(sorted(locales_code_extra)) locales_name = {l: translations[l]['title']['language'] for l in locales_code} return translations, locales_name, locales_code translations, locales_name, locales_code = load_all_locales() # ---------- HELPER FUNCTIONS ------------------------------------------------ def get_best_lang(): return request.accept_languages.best_match(locales_code) or "en" def render_localized_template(lang, template_name): if lang not in locales_code: return redirect(url_for("index", lang=get_best_lang())) return render_template( template_name, year=date.today().year, ) @app.before_request def before_request(): if args := request.view_args: g.locale = args.get("lang", "en") g.translations = translations.get(g.locale, get_best_lang()) g.locales_name = locales_name @app.context_processor def inject_translations(): def translate(text, **kwargs): section, key = text.split(":", 1) template = g.translations \ .get(section, {}) \ .get(key, f"${section}: {key}$") try: return template.format(**kwargs) except Exception: return template return {'_': translate} # ---------- MAIN PAGES ------------------------------------------------------ @app.route("/") def home(): return redirect(url_for("index", lang=get_best_lang())) @app.route("/") def index(lang): return render_localized_template(lang, "index.html") @app.route("//download") def download(lang): return render_localized_template(lang, "download.html") # ---------- ROBOTS.TXT + SITEMAP.XML ---------------------------------------- @app.route("/robots.txt") def robots_txt(): base_url = request.url_root.rstrip("/") content = [ "User-agent: *", "Disallow:", f"Sitemap: {base_url}/sitemap.xml", ] return Response("\n".join(content), mimetype="text/plain") @app.route("/sitemap.xml") def sitemap_xml(): base_url = request.url_root.rstrip("/") today = date.today().isoformat() urls = [] for lang in locales_code: urls.append(f"{base_url}/{lang}") urls.append(f"{base_url}/{lang}/download") xml_lines = [ '', '', ] for loc in urls: xml_lines.extend( [ f" ", f" {loc}", f" {today}", f" monthly", f" 0.8", f" ", ] ) xml_lines.append("") return Response("\n".join(xml_lines), mimetype="application/xml") # ---------- APP ENTRY ------------------------------------------------------- if __name__ == "__main__": app.run(host="0.0.0.0", debug=True)