MuzammilMax commited on
Commit
f755feb
ยท
verified ยท
1 Parent(s): 0ecf0ef

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -264
app.py CHANGED
@@ -1,280 +1,159 @@
1
- import os
2
  import subprocess
3
  import threading
4
  import time
5
- import shutil
6
- import zipfile
7
- import requests
8
- from datetime import datetime
9
- from fastapi import FastAPI
10
- from fastapi.responses import HTMLResponse, JSONResponse
11
- import uvicorn
12
-
13
- app = FastAPI()
14
 
15
- # Config from env
16
- SERVER_VERSION = os.getenv("SERVER_VERSION", "1.20.4")
17
- MAX_RAM = os.getenv("MAX_RAM", "10G")
18
- MIN_RAM = os.getenv("MIN_RAM", "2G")
19
- PORT = int(os.getenv("PORT", "7860"))
20
- SERVER_PORT = int(os.getenv("SERVER_PORT", "25565"))
21
- RCON_PASSWORD = os.getenv("RCON_PASSWORD", "minecraft")
22
- MC_PATH = os.getenv("MINECRAFT_PATH", "/data/minecraft")
23
- BACKUP_PATH = os.getenv("BACKUP_PATH", "/data/backups")
24
- HF_TOKEN = os.getenv("HF_TOKEN", "")
25
- HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "") # e.g. "username/mc-world-backup"
26
- SPACE_URL = os.getenv("SPACE_URL", "") # e.g. "https://yourname-spacename.hf.space"
27
 
 
28
  mc_process = None
29
- server_log = []
30
- start_time = None
31
-
32
- # โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
 
34
- def log(msg):
35
- ts = datetime.now().strftime("%H:%M:%S")
36
- entry = f"[{ts}] {msg}"
37
- server_log.append(entry)
38
- if len(server_log) > 500:
39
- server_log.pop(0)
40
- print(entry)
41
-
42
- def setup_dirs():
43
- os.makedirs(MC_PATH, exist_ok=True)
44
- os.makedirs(BACKUP_PATH, exist_ok=True)
45
-
46
- def download_paper():
47
- jar = os.path.join(MC_PATH, "server.jar")
48
- if os.path.exists(jar):
49
- log("server.jar already present, skipping download")
50
- return
51
- log(f"Downloading Paper {SERVER_VERSION}...")
52
- url = f"https://api.papermc.io/v2/projects/paper/versions/{SERVER_VERSION}/builds"
53
- try:
54
- builds = requests.get(url, timeout=30).json()["builds"]
55
- latest = builds[-1]
56
- build_num = latest["build"]
57
- jar_name = latest["downloads"]["application"]["name"]
58
- dl_url = (f"https://api.papermc.io/v2/projects/paper/versions/{SERVER_VERSION}"
59
- f"/builds/{build_num}/downloads/{jar_name}")
60
- r = requests.get(dl_url, stream=True, timeout=120)
61
- with open(jar, "wb") as f:
62
- for chunk in r.iter_content(8192):
63
- f.write(chunk)
64
- log("Paper downloaded successfully")
65
- except Exception as e:
66
- log(f"Download failed: {e}")
67
 
68
- def write_configs():
69
- # eula
70
- with open(os.path.join(MC_PATH, "eula.txt"), "w") as f:
71
- f.write("eula=true\n")
72
- # server.properties
73
- props_path = os.path.join(MC_PATH, "server.properties")
74
- if not os.path.exists(props_path):
75
- with open(props_path, "w") as f:
76
- f.write(f"""server-port={SERVER_PORT}
77
- online-mode=false
78
- difficulty=normal
79
- gamemode=survival
80
- max-players=20
81
- enable-rcon=true
82
- rcon.port=25575
83
- rcon.password={RCON_PASSWORD}
84
- view-distance=8
85
- simulation-distance=6
86
- """)
87
 
88
- # โ”€โ”€ server lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
89
 
90
- def start_server():
91
- global mc_process, start_time
92
- setup_dirs()
93
- download_paper()
94
- write_configs()
95
- jar = os.path.join(MC_PATH, "server.jar")
96
- cmd = [
97
- "java",
98
- f"-Xms{MIN_RAM}", f"-Xmx{MAX_RAM}",
99
- "-XX:+UseG1GC",
100
- "-XX:+ParallelRefProcEnabled",
101
- "-XX:MaxGCPauseMillis=200",
102
- "-jar", jar, "nogui"
103
- ]
104
- log("Starting Minecraft server...")
105
  mc_process = subprocess.Popen(
106
- cmd, cwd=MC_PATH,
107
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
108
- text=True, bufsize=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  )
110
- start_time = datetime.now()
111
- # stream stdout
112
- def read_output():
113
- for line in mc_process.stdout:
114
- server_log.append(line.rstrip())
115
- if len(server_log) > 500:
116
- server_log.pop(0)
117
- threading.Thread(target=read_output, daemon=True).start()
118
-
119
- def stop_server():
120
- global mc_process
121
- if mc_process and mc_process.poll() is None:
122
- mc_process.terminate()
123
- mc_process.wait(timeout=30)
124
- mc_process = None
125
- log("Server stopped")
126
-
127
- def is_running():
128
- return mc_process is not None and mc_process.poll() is None
129
-
130
- # โ”€โ”€ watchdog โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
131
-
132
- def watchdog():
133
- """Restart the server if it crashes."""
134
- time.sleep(60) # give initial start time
135
- while True:
136
- time.sleep(30)
137
- if not is_running() and start_time is not None:
138
- log("โš ๏ธ Server not running โ€” auto-restarting...")
139
- start_server()
140
-
141
- # โ”€โ”€ self-ping keepalive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
142
 
143
- def keepalive():
144
- """Ping our own /health every 25 min to prevent HF sleep."""
145
- time.sleep(120)
146
- while True:
147
- if SPACE_URL:
148
- try:
149
- requests.get(f"{SPACE_URL}/health", timeout=10)
150
- log("keepalive ping sent")
151
- except Exception:
152
- pass
153
- time.sleep(25 * 60)
154
 
155
- # โ”€โ”€ world backup to HF Dataset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
156
-
157
- def backup_world():
158
- """Zip world folder and push to a HF Dataset repo every 30 min."""
159
- time.sleep(300)
160
- while True:
161
- time.sleep(30 * 60)
162
- if not HF_TOKEN or not HF_DATASET_REPO:
163
- continue
164
- try:
165
- ts = datetime.now().strftime("%Y%m%d_%H%M")
166
- zip_path = f"/tmp/world_backup_{ts}.zip"
167
- world_dir = os.path.join(MC_PATH, "world")
168
- if not os.path.exists(world_dir):
169
- continue
170
- log("Creating world backup...")
171
- with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
172
- for root, _, files in os.walk(world_dir):
173
- for file in files:
174
- fp = os.path.join(root, file)
175
- z.write(fp, os.path.relpath(fp, MC_PATH))
176
- # Upload via HF API
177
- api_url = f"https://huggingface.co/api/datasets/{HF_DATASET_REPO}/upload/world_backup_{ts}.zip"
178
- with open(zip_path, "rb") as f:
179
- r = requests.put(
180
- api_url,
181
- headers={"Authorization": f"Bearer {HF_TOKEN}"},
182
- data=f,
183
- timeout=120
184
- )
185
- if r.ok:
186
- log(f"World backed up: world_backup_{ts}.zip")
187
- else:
188
- log(f"Backup upload failed: {r.status_code}")
189
- os.remove(zip_path)
190
- except Exception as e:
191
- log(f"Backup error: {e}")
192
-
193
- # โ”€โ”€ startup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
194
-
195
- @app.on_event("startup")
196
- async def on_startup():
197
- threading.Thread(target=start_server, daemon=True).start()
198
- threading.Thread(target=watchdog, daemon=True).start()
199
- threading.Thread(target=keepalive, daemon=True).start()
200
- threading.Thread(target=backup_world, daemon=True).start()
201
-
202
- # โ”€โ”€ routes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
203
-
204
- @app.get("/", response_class=HTMLResponse)
205
- async def index():
206
- status = "๐ŸŸข Running" if is_running() else "๐Ÿ”ด Stopped"
207
- uptime = ""
208
- if start_time and is_running():
209
- delta = datetime.now() - start_time
210
- h, rem = divmod(int(delta.total_seconds()), 3600)
211
- m, s = divmod(rem, 60)
212
- uptime = f"{h}h {m}m {s}s"
213
- logs_html = "\n".join(server_log[-60:])
214
- return f"""<!DOCTYPE html>
215
- <html><head>
216
- <title>Minecraft Server Manager</title>
217
- <meta http-equiv="refresh" content="15">
218
- <style>
219
- body{{font-family:monospace;background:#1a1a2e;color:#e0e0e0;margin:0;padding:20px}}
220
- h1{{color:#00d4ff}} .card{{background:#16213e;border-radius:8px;padding:16px;margin:12px 0}}
221
- .status{{font-size:1.4em;font-weight:bold}}
222
- pre{{background:#0f0f23;padding:12px;border-radius:6px;max-height:400px;overflow-y:auto;font-size:0.8em}}
223
- button{{padding:10px 24px;border:none;border-radius:6px;cursor:pointer;font-size:1em;margin:4px}}
224
- .start{{background:#00b894;color:#fff}} .stop{{background:#d63031;color:#fff}}
225
- </style>
226
- </head><body>
227
- <h1>๐ŸŽฎ Minecraft Server Manager</h1>
228
- <div class="card">
229
- <div class="status">{status}</div>
230
- {"<div>Uptime: "+uptime+"</div>" if uptime else ""}
231
- <div>Version: {SERVER_VERSION} | RAM: {MIN_RAM}โ€“{MAX_RAM}</div>
232
- </div>
233
- <div class="card">
234
- <form action="/start" method="post" style="display:inline">
235
- <button class="start" type="submit">โ–ถ Start</button>
236
- </form>
237
- <form action="/stop" method="post" style="display:inline">
238
- <button class="stop" type="submit">โน Stop</button>
239
- </form>
240
- <form action="/backup" method="post" style="display:inline">
241
- <button style="background:#6c5ce7;color:#fff" type="submit">๐Ÿ’พ Backup Now</button>
242
- </form>
243
- </div>
244
- <div class="card">
245
- <b>Server Logs</b>
246
- <pre>{logs_html}</pre>
247
- </div>
248
- </body></html>"""
249
-
250
- @app.get("/health")
251
- async def health():
252
- return JSONResponse({"status": "running" if is_running() else "stopped", "uptime_ok": True})
253
-
254
- @app.post("/start")
255
- async def api_start():
256
- if not is_running():
257
- threading.Thread(target=start_server, daemon=True).start()
258
- from fastapi.responses import RedirectResponse
259
- return RedirectResponse("/", status_code=303)
260
-
261
- @app.post("/stop")
262
- async def api_stop():
263
- stop_server()
264
- from fastapi.responses import RedirectResponse
265
- return RedirectResponse("/", status_code=303)
266
-
267
- @app.post("/backup")
268
- async def api_backup():
269
- threading.Thread(target=backup_world, daemon=True).start()
270
- from fastapi.responses import RedirectResponse
271
- return RedirectResponse("/", status_code=303)
272
-
273
- @app.get("/logs")
274
- async def api_logs():
275
- return JSONResponse({"logs": server_log[-100:]})
276
-
277
- # โ”€โ”€ main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
278
 
279
  if __name__ == "__main__":
280
- uvicorn.run(app, host="0.0.0.0", port=PORT)
 
 
 
 
 
 
 
1
  import subprocess
2
  import threading
3
  import time
4
+ import os
5
+ import socket
6
+ from flask import Flask, jsonify
 
 
 
 
 
 
7
 
8
+ app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # โ”€โ”€ Minecraft process handle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
  mc_process = None
12
+ mc_start_time = None
 
 
 
13
 
14
+ def start_minecraft():
15
+ global mc_process, mc_start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Start Tailscale daemon
18
+ subprocess.Popen(
19
+ ["tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"],
20
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
21
+ )
22
+ time.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # Authenticate Tailscale (only if authkey provided)
25
+ authkey = os.environ.get("TAILSCALE_AUTHKEY", "")
26
+ if authkey:
27
+ subprocess.run(["tailscale", "up", "--authkey", authkey],
28
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
29
 
30
+ # Launch Minecraft with Aikar's flags
31
+ mc_start_time = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  mc_process = subprocess.Popen(
33
+ [
34
+ "java",
35
+ "-Xmx4G", "-Xms4G",
36
+ "-XX:+UseG1GC",
37
+ "-XX:+ParallelRefProcEnabled",
38
+ "-XX:MaxGCPauseMillis=200",
39
+ "-XX:+UnlockExperimentalVMOptions",
40
+ "-XX:+DisableExplicitGC",
41
+ "-XX:+AlwaysPreTouch",
42
+ "-XX:G1NewSizePercent=30",
43
+ "-XX:G1MaxNewSizePercent=40",
44
+ "-XX:G1HeapRegionSize=8M",
45
+ "-XX:G1ReservePercent=20",
46
+ "-XX:G1HeapWastePercent=5",
47
+ "-XX:G1MixedGCCountTarget=4",
48
+ "-XX:InitiatingHeapOccupancyPercent=15",
49
+ "-XX:G1MixedGCLiveThresholdPercent=90",
50
+ "-XX:G1RSetUpdatingPauseTimePercent=5",
51
+ "-XX:SurvivorRatio=32",
52
+ "-XX:+PerfDisableSharedMem",
53
+ "-XX:MaxTenuringThreshold=1",
54
+ "-Dusing.aikars.flags=https://mcflags.emc.gs",
55
+ "-Daikars.new.flags=true",
56
+ "-jar", "paper.jar", "nogui"
57
+ ],
58
+ cwd="/minecraft",
59
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT
60
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ def is_minecraft_port_open():
64
+ """Check if Minecraft port 25565 is accepting connections."""
65
+ try:
66
+ with socket.create_connection(("127.0.0.1", 25565), timeout=2):
67
+ return True
68
+ except OSError:
69
+ return False
70
+
71
+
72
+ def get_uptime():
73
+ if mc_start_time is None:
74
+ return "Not started"
75
+ elapsed = int(time.time() - mc_start_time)
76
+ h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
77
+ return f"{h}h {m}m {s}s"
78
+
79
+
80
+ # โ”€โ”€ Routes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
+
82
+ @app.route("/")
83
+ def index():
84
+ mc_alive = mc_process is not None and mc_process.poll() is None
85
+ port_open = is_minecraft_port_open()
86
+ status = "๐ŸŸข Running" if mc_alive else "๐Ÿ”ด Stopped"
87
+ port_status = "๐ŸŸข Open" if port_open else "๐ŸŸก Startingโ€ฆ"
88
+
89
+ html = f"""<!DOCTYPE html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="UTF-8">
93
+ <meta http-equiv="refresh" content="30">
94
+ <meta name="viewport" content="width=device-width, initial-scale=1">
95
+ <title>Minecraft Server Status</title>
96
+ <style>
97
+ body {{
98
+ font-family: 'Segoe UI', sans-serif;
99
+ background: #1a1a2e; color: #eee;
100
+ display: flex; align-items: center; justify-content: center;
101
+ min-height: 100vh; margin: 0;
102
+ }}
103
+ .card {{
104
+ background: #16213e; border-radius: 16px;
105
+ padding: 40px 50px; text-align: center;
106
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
107
+ max-width: 480px; width: 90%;
108
+ }}
109
+ h1 {{ margin: 0 0 8px; font-size: 1.8rem; color: #e94560; }}
110
+ .subtitle {{ color: #888; margin-bottom: 28px; font-size: 0.95rem; }}
111
+ .row {{ display: flex; justify-content: space-between;
112
+ padding: 12px 0; border-bottom: 1px solid #0f3460; }}
113
+ .row:last-child {{ border-bottom: none; }}
114
+ .label {{ color: #aaa; }}
115
+ .value {{ font-weight: 600; }}
116
+ .footer {{ margin-top: 24px; font-size: 0.8rem; color: #555; }}
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <div class="card">
121
+ <h1>โ› Minecraft Server</h1>
122
+ <p class="subtitle">Paper 1.21.11 &nbsp;|&nbsp; Auto-refreshes every 30s</p>
123
+ <div class="row"><span class="label">Process</span><span class="value">{status}</span></div>
124
+ <div class="row"><span class="label">Port 25565</span><span class="value">{port_status}</span></div>
125
+ <div class="row"><span class="label">Uptime</span><span class="value">{get_uptime()}</span></div>
126
+ <div class="row"><span class="label">Version</span><span class="value">Paper 1.21.11</span></div>
127
+ <p class="footer">Connect via your Tailscale IP on port 25565</p>
128
+ </div>
129
+ </body>
130
+ </html>"""
131
+ return html
132
+
133
+
134
+ @app.route("/health")
135
+ def health():
136
+ """Hugging Face health-check endpoint โ€” always returns 200."""
137
+ return jsonify({"status": "ok"}), 200
138
+
139
+
140
+ @app.route("/api/status")
141
+ def api_status():
142
+ mc_alive = mc_process is not None and mc_process.poll() is None
143
+ return jsonify({
144
+ "minecraft_running": mc_alive,
145
+ "port_open": is_minecraft_port_open(),
146
+ "uptime": get_uptime(),
147
+ "version": "Paper 1.21.11"
148
+ })
149
+
150
+
151
+ # โ”€โ”€ Entry point โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  if __name__ == "__main__":
154
+ # Start Minecraft in a background thread so Flask starts immediately
155
+ t = threading.Thread(target=start_minecraft, daemon=True)
156
+ t.start()
157
+
158
+ # Flask must bind 0.0.0.0:7860 for Hugging Face Spaces
159
+ app.run(host="0.0.0.0", port=7860, threaded=True)