"""Run: python3 serve.py --port 8081. Supports video seeking and storefront fallbacks.""" import argparse import functools import re import ssl import urllib.error import urllib.parse import urllib.request from pathlib import Path from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer IMAGE_CDN = "https://assets.joqhl8w6.on-eva.io/blob/" TGJU_AJAX = "https://call1.tgju.org/ajax.json" class Handler(SimpleHTTPRequestHandler): def translate_path(self, path): clean = urllib.parse.unquote(path.split("?", 1)[0]) if clean.startswith("/products/") and clean.rstrip("/") != "/products": rel = clean.lstrip("/") candidate = Path(self.directory) / rel if not candidate.is_file() and not (candidate / "index.html").is_file(): return str(Path(self.directory) / "products" / "index.html") return super().translate_path(path) def do_GET(self): clean = urllib.parse.unquote(self.path.split("?", 1)[0]) if clean == "/api/fx/eur": try: req = urllib.request.Request( TGJU_AJAX, headers={ "User-Agent": "KikomikoFX/1.0", "Accept": "application/json", }, ) ctx = ssl._create_unverified_context() with urllib.request.urlopen(req, timeout=20, context=ctx) as resp: body = resp.read() self.send_response(200) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Cache-Control", "public, max-age=300") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) except Exception as exc: # noqa: BLE001 msg = str(exc).encode("utf-8", "ignore") self.send_response(502) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(msg) + 20)) self.end_headers() self.wfile.write(b'{"error":"' + msg.replace(b'"', b"'") + b'"}') return if clean.startswith("/i/"): image_id = clean[3:].strip("/") if image_id and re.fullmatch(r"[0-9a-fA-F-]+", image_id): # Always use KIKO Eva CDN so every product photo loads correctly. url = IMAGE_CDN + urllib.parse.quote(image_id, safe="-") self.send_response(302) self.send_header("Location", url) self.send_header("Cache-Control", "public, max-age=604800") self.end_headers() return return super().do_GET() def send_head(self): self.remaining = None file = Path(self.translate_path(self.path)) value = self.headers.get('Range') if not value or not file.is_file(): return super().send_head() size = file.stat().st_size match = re.fullmatch(r'bytes=(\d*)-(\d*)', value.strip()) if not match or not any(match.groups()): self.send_error(400, 'Invalid byte range') return None first, last = match.groups() if first: start = int(first) end = min(int(last), size - 1) if last else size - 1 else: start, end = max(0, size - int(last)), size - 1 if start > end or start >= size: self.send_response(416) self.send_header('Content-Range', f'bytes */{size}') self.end_headers() return None stream = file.open('rb') stream.seek(start) self.remaining = end - start + 1 self.send_response(206) self.send_header('Content-Type', self.guess_type(str(file))) self.send_header('Content-Length', str(self.remaining)) self.send_header('Content-Range', f'bytes {start}-{end}/{size}') self.end_headers() return stream def end_headers(self): self.send_header('Accept-Ranges', 'bytes') super().end_headers() def copyfile(self, source, output): if self.remaining is None: return super().copyfile(source, output) while self.remaining > 0: chunk = source.read(min(65536, self.remaining)) if not chunk: break output.write(chunk) self.remaining -= len(chunk) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--port', type=int, default=8081) args = parser.parse_args() root = Path(__file__).resolve().parent / 'dist' handler = functools.partial(Handler, directory=str(root)) print(f'Open http://127.0.0.1:{args.port}/', flush=True) ThreadingHTTPServer(('127.0.0.1', args.port), handler).serve_forever()