first deployment of kikomiko with url photos
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8d98304137
commit
ae3ed7c7b7
@@ -0,0 +1,119 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user