smodusermc's picture
download
raw
9.26 kB
#!/usr/bin/env python3
"""Play the offline-games archive in a normal browser.
Browsers block web games opened straight from disk (file://): the page appears, then the loader waits forever
because the browser refuses its data requests. These games must be served over HTTP from the site's own paths.
This script does that, using only the Python standard library (Python 3.8+):
python play.py # then open http://localhost:8000/
python play.py --port 9000 --dir D:/offline-games
python play.py --offline # use only files already on disk
- Files are read from DIR (default: ./offline-games, laid out like the bucket: site/..., _vendor/..., _reports/...).
- A file that is not on disk yet is downloaded once from the public bucket and kept, so you can play any game
without downloading all of it first. With --offline nothing is downloaded.
- Responses carry the headers the original site used (cross-origin isolation, correct MIME types, byte ranges).
- Public libraries that some games load from CDNs are served from the archive's _vendor/ copies when present.
- http://localhost:PORT/ lists every captured game with a link.
"""
import argparse, html, json, mimetypes, os, re, shutil, sys, threading, time, urllib.error, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import quote, unquote, urlparse
BUCKET = 'https://huggingface.co/buckets/smodusermc/offline-games/resolve/'
SITE = b'garbsoftball.com'
VENDORS = ('www.gstatic.com', 'cdnjs.cloudflare.com', 'unpkg.com', 'cdn.jsdelivr.net', 'ajax.googleapis.com',
'maxcdn.bootstrapcdn.com', 'cdn-factory.marketjs.com', 'cdn.fbrq.io', 'www.youtube.com')
TYPES = {'.wasm': 'application/wasm', '.js': 'application/javascript; charset=utf-8', '.mjs': 'application/javascript; charset=utf-8',
'.json': 'application/json', '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.css': 'text/css',
'.svg': 'image/svg+xml', '.webp': 'image/webp', '.ogg': 'audio/ogg', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
'.m4a': 'audio/mp4', '.mp4': 'video/mp4', '.webm': 'video/webm', '.swf': 'application/x-shockwave-flash',
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', '.txt': 'text/plain; charset=utf-8',
'.xml': 'application/xml', '.ico': 'image/x-icon'}
locks, locks_guard = {}, threading.Lock()
not_in_bucket = set()
def args():
a = argparse.ArgumentParser(description='Serve the offline-games archive for play in a browser.')
a.add_argument('--port', type=int, default=8000)
a.add_argument('--dir', default='offline-games', help='local folder for the archive files (default: ./offline-games)')
a.add_argument('--offline', action='store_true', help='never download; use only files already on disk')
a.add_argument('--host', default='127.0.0.1', help='address to listen on (default 127.0.0.1)')
return a.parse_args()
def fetch(key, dest):
"""Download bucket object `key` to `dest` once (thread-safe). Returns True if the file exists afterwards."""
if dest.is_file() or OPT.offline or key in not_in_bucket: return dest.is_file()
with locks_guard: lk = locks.setdefault(key, threading.Lock())
with lk:
if dest.is_file(): return True
dest.parent.mkdir(parents=True, exist_ok=True); tmp = dest.with_name(dest.name + '.part')
for attempt in range(4):
try:
req = urllib.request.Request(BUCKET + quote(key), headers={'User-Agent': 'offline-games-player'})
with urllib.request.urlopen(req, timeout=60) as r, open(tmp, 'wb') as fh: shutil.copyfileobj(r, fh, 1 << 20)
os.replace(tmp, dest); print(' downloaded', key, flush=True); return True
except urllib.error.HTTPError as e:
if e.code in (403, 404): not_in_bucket.add(key); return False
if e.code in (429, 500, 502, 503, 504): time.sleep(2 * (attempt + 1)); continue
print(' download failed', key, e, flush=True); return False
except Exception as e:
print(' download error', key, e, flush=True); time.sleep(2 * (attempt + 1))
return False
def catalog_page():
inv_path = ROOT / '_reports' / 'inventory.json'
if not fetch('_reports/inventory.json', inv_path): return b'<p>inventory.json not available (offline and not on disk).</p>'
inv = json.loads(inv_path.read_text(encoding='utf-8'))
rows = [r for r in inv if r.get('status') in ('partial_static_capture', 'static_capture_unverified')]
rows.sort(key=lambda r: r['name'].lower())
items = ''.join(f'<li><a href="{html.escape(urlparse(r["entry"]).path)}">{html.escape(r["name"])}</a> <small>#{r["id"]}</small></li>' for r in rows)
return (f'<!doctype html><meta charset="utf-8"><title>Offline games</title><body style="font-family:sans-serif;max-width:900px;margin:2em auto">'
f'<h1>Offline games ({len(rows)})</h1><p>Files download from the archive the first time a game is opened, so the first start can take a while '
f'for large games. Afterwards they are read from disk.</p><input id=q placeholder="filter" oninput="for(const li of document.querySelectorAll(\'li\'))'
f'li.style.display=li.textContent.toLowerCase().includes(this.value.toLowerCase())?\'\':\'none\'"><ul style="columns:2">{items}</ul>').encode()
class Handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, fmt, *a): pass
def send_body(self, code, body, ctype):
self.send_response(code); self.common(ctype); self.send_header('Content-Length', str(len(body))); self.end_headers()
if self.command != 'HEAD': self.wfile.write(body)
def common(self, ctype):
self.send_header('Content-Type', ctype)
for k, v in (('Cross-Origin-Opener-Policy', 'same-origin'), ('Cross-Origin-Embedder-Policy', 'credentialless'),
('Cross-Origin-Resource-Policy', 'cross-origin'), ('Access-Control-Allow-Origin', '*'), ('Cache-Control', 'no-cache'),
('Accept-Ranges', 'bytes')):
self.send_header(k, v)
def do_HEAD(self): self.do_GET()
def do_GET(self):
path = re.sub(r'/{2,}', '/', unquote(urlparse(self.path).path))
if path in ('/', '/__games'): return self.send_body(200, catalog_page(), 'text/html; charset=utf-8')
if '..' in path.split('/'): return self.send_body(400, b'bad path', 'text/plain')
if path.startswith('/__vendor/'): key = '_vendor/' + path[len('/__vendor/'):]
else: key = 'site' + path + ('index.html' if path.endswith('/') else '')
f = ROOT / key
if not fetch(key, f):
if path.startswith('/__vendor/') and not OPT.offline: # not archived: fall back to the public CDN
self.send_response(302); self.send_header('Location', 'https://' + path[len('/__vendor/'):]); self.send_header('Content-Length', '0'); self.end_headers(); return
return self.send_body(404, b'not in the archive', 'text/plain')
ext = os.path.splitext(f.name)[1].lower()
ctype = TYPES.get(ext) or mimetypes.guess_type(f.name)[0] or 'application/octet-stream'
size = f.stat().st_size
if size < (16 << 20) and ext in ('.html', '.htm', '.js', '.mjs', '.css', '.json'):
data = f.read_bytes().replace(b'https://' + SITE, b'').replace(b'http://' + SITE, b'')
for h in VENDORS: data = data.replace(b'https://' + h.encode(), b'/__vendor/' + h.encode())
return self.send_body(200, data, ctype)
start, end, code = 0, size - 1, 200
m = re.match(r'bytes=(\d*)-(\d*)$', self.headers.get('Range', ''))
if m and size:
if m.group(1): start = int(m.group(1)); end = int(m.group(2)) if m.group(2) else size - 1
else: start = max(0, size - int(m.group(2) or 0))
end = min(end, size - 1); code = 206
if start > end: self.send_response(416); self.send_header('Content-Range', f'bytes */{size}'); self.send_header('Content-Length', '0'); self.end_headers(); return
self.send_response(code); self.common(ctype); self.send_header('Content-Length', str(end - start + 1))
if code == 206: self.send_header('Content-Range', f'bytes {start}-{end}/{size}')
self.end_headers()
if self.command == 'HEAD': return
try:
with open(f, 'rb') as fh:
fh.seek(start); left = end - start + 1
while left > 0:
chunk = fh.read(min(1 << 20, left))
if not chunk: break
self.wfile.write(chunk); left -= len(chunk)
except (BrokenPipeError, ConnectionResetError): pass
if __name__ == '__main__':
OPT = args(); ROOT = __import__('pathlib').Path(OPT.dir).resolve(); ROOT.mkdir(parents=True, exist_ok=True)
srv = ThreadingHTTPServer((OPT.host, OPT.port), Handler); srv.daemon_threads = True
print(f'Offline games: http://localhost:{OPT.port}/ (files in {ROOT}{", offline" if OPT.offline else ""}; Ctrl+C to stop)', flush=True)
try: srv.serve_forever()
except KeyboardInterrupt: print('stopped')

Xet Storage Details

Size:
9.26 kB
·
Xet hash:
d6e2baf590eca98854fee321271559d6fafdb5fdb827d176c29c835ba0bcd02c

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.