120 lines
4.9 KiB
Python
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()
|