-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_server.py
More file actions
155 lines (130 loc) · 5.38 KB
/
api_server.py
File metadata and controls
155 lines (130 loc) · 5.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Pure-Python JSON API server for the game catalogue.
Endpoints:
GET /api/games — Returns all games as JSON.
GET /api/games?search=X — Returns games whose title or slug contain X (case-insensitive).
Also serves the output/ directory for static HTML game files.
Usage:
python api_server.py [--port PORT] [--output-dir DIR]
"""
import argparse
import json
import os
import re
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
# Matches filenames produced by process_game: {source}_{slug}.html
_FILENAME_RE = re.compile(r"^(?P<source>crazygames|poki)_(?P<slug>.+)\.html$")
def _build_game_index(output_dir: Path) -> list[dict]:
"""Scan output_dir for .html files and return game metadata list."""
games: list[dict] = []
if not output_dir.is_dir():
return games
for entry in sorted(output_dir.iterdir()):
if not entry.is_file() or entry.suffix != ".html":
continue
m = _FILENAME_RE.match(entry.name)
if not m:
continue
source = m.group("source")
slug = m.group("slug")
title = slug.replace("-", " ").replace("_", " ").title()
games.append({
"title": title,
"slug": slug,
"source": source,
"filename": entry.name,
"size": entry.stat().st_size,
})
return games
def _search_games(games: list[dict], query: str) -> list[dict]:
"""Filter games by title or slug containing query (case-insensitive)."""
q = query.lower()
return [
g for g in games
if q in g["title"].lower() or q in g["slug"].lower()
]
class GameAPIHandler(BaseHTTPRequestHandler):
output_dir: Path
game_index: list[dict]
def _send_json(self, data: object, status: int = 200) -> None:
body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def _send_file(self, path: Path, content_type: str = "text/html") -> None:
try:
data = path.read_bytes()
except OSError:
self._send_json({"error": "File not found"}, 404)
return
self.send_response(200)
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(data)
def do_GET(self) -> None: # noqa: N802
parsed = urllib.parse.urlparse(self.path)
path = parsed.path.rstrip("/")
qs = urllib.parse.parse_qs(parsed.query)
if path == "/api/games":
search = qs.get("search", [None])[0]
if search:
results = _search_games(self.game_index, search)
else:
results = self.game_index
self._send_json(results)
return
# Serve static game files from output_dir
if path.startswith("/games/"):
filename = os.path.basename(path)
file_path = self.output_dir / filename
# Prevent path traversal
if file_path.resolve().parent != self.output_dir.resolve():
self._send_json({"error": "Forbidden"}, 403)
return
if file_path.is_file():
self._send_file(file_path)
return
self._send_json({"error": "Not found"}, 404)
return
# Root: simple info page
if path in ("", "/"):
total = len(self.game_index)
self._send_json({
"service": "civil2-game-api",
"total_games": total,
"endpoints": {
"GET /api/games": "List all games (JSON)",
"GET /api/games?search=QUERY": "Search games by title/slug",
"GET /games/{filename}": "Serve a game HTML file",
},
})
return
self._send_json({"error": "Not found"}, 404)
def log_message(self, fmt: str, *args: object) -> None:
print(f" [{self.log_date_time_string()}] {fmt % args}")
def run_server(port: int = 8080, output_dir: str = "output") -> None:
out_path = Path(output_dir).resolve()
game_index = _build_game_index(out_path)
print(f"Indexed {len(game_index)} games from {out_path}")
# Dynamically bind class attributes so the handler can access them
GameAPIHandler.output_dir = out_path
GameAPIHandler.game_index = game_index
server = HTTPServer(("0.0.0.0", port), GameAPIHandler)
print(f"API server listening on http://0.0.0.0:{port}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down.")
server.server_close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Game catalogue JSON API server")
parser.add_argument("--port", type=int, default=8080, help="Port to listen on (default: 8080)")
parser.add_argument("--output-dir", default="output", help="Directory with game HTML files (default: output)")
args = parser.parse_args()
run_server(port=args.port, output_dir=args.output_dir)