Files

120 lines
4.9 KiB
Python

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