primo commit

This commit is contained in:
2026-09-23 18:59:27 +02:00
parent 029e9f67fb
commit bd900099a0
451 changed files with 143273 additions and 2 deletions
+560
View File
@@ -0,0 +1,560 @@
#!/usr/bin/env python3
"""OGame Galaxy Viewer — backend Flask.
Servizi:
- vista galassia/sistema (dati da world.json, aggiornato 1x/giorno)
- import rapporti di spionaggio via API string (sr-...) con salvataggio JSON
- consultazione rapporti per pianeta
Avvio: python3 app.py (sviluppo)
Servizio systemd: ogame-galaxy.service
"""
import json
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from flask import Flask, jsonify, request, send_from_directory
import report_store
import update_data
from technames import category_name, item_name
BASE_DIR = Path(__file__).resolve().parent
STATIC = BASE_DIR / "static"
LATEST = BASE_DIR / "data" / "latest"
app = Flask(__name__, static_folder=str(STATIC), static_url_path="/static")
@app.after_request
def _no_cache(resp):
# tool personale: niente cache, sempre file freschi
if request.path.startswith("/static/") or request.path in ("/", "/rankings"):
resp.headers["Cache-Control"] = "no-store"
return resp
# semplice rate-limit di cortesia verso il proxy (10 req/min => max 5/min qui)
_report_lock = threading.Lock()
def load_config():
with open(BASE_DIR / "config.json", encoding="utf-8") as f:
return json.load(f)
def load_world():
p = LATEST / "world.json"
if not p.exists():
return None
with open(p, encoding="utf-8") as f:
world = json.load(f)
return _augment_world_with_reports(world)
def _augment_world_with_reports(world):
"""Aggiunge i pianeti noti *solo* dai rapporti di spionaggio.
L'API pubblica `universe.xml` viene rigenerata di rado: i pianeti dei player
più recenti possono mancare per giorni, pur essendo visibili nei rapporti.
Se esiste un rapporto su una coordinata assente, la aggiungiamo (marcata
`report_only`), associando il giocatore per nome univoco, così galassia,
finestra giocatore e classifica restano utilizzabili.
"""
try:
idx = report_store._load_index()
except Exception:
return world
reports = idx.get("reports", {})
planets = world.setdefault("planets", {})
players = world.get("players", {})
avatar = world.setdefault("avatar", {})
by_name = {}
for pid, p in players.items():
n = (p.get("name") or "").strip().lower()
if n:
by_name.setdefault(n, []).append(pid)
def resolve(rec):
pids = by_name.get((rec.get("defender_name") or "").strip().lower())
return pids[0] if pids and len(pids) == 1 else None
# 1) pianeti (type != 3): aggiungi quelli mancanti
for rec in reports.values():
coords = rec.get("coords")
if not coords or rec.get("planet_type") == 3 or coords in planets:
continue
pid = resolve(rec)
if not pid:
continue
planets[coords] = {
"id": f"report-{coords}",
"player": pid,
"name": rec.get("defender_planet_name") or "(da rapporto)",
"coords": coords,
"report_only": True,
}
avatar.setdefault(pid, coords)
# 2) lune (type == 3): aggancia al pianeta se presente e senza luna nota
for rec in reports.values():
if rec.get("planet_type") != 3:
continue
base = planets.get(rec.get("coords") or "")
if base is not None and not base.get("moon"):
base["moon"] = {
"id": f"report-{base['coords']}#moon",
"name": rec.get("defender_planet_name") or "Luna",
"size": 0,
}
return world
def fmt_ts(epoch):
if not epoch:
return None
return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
# ---------------------------------------------------------------- pagine
@app.route("/")
def index():
return send_from_directory(STATIC, "index.html")
# ---------------------------------------------------------------- API dati
@app.route("/api/bootstrap")
def api_bootstrap():
cfg = load_config()
world = load_world()
if not world:
return jsonify({"ok": False, "error": "Dati non ancora scaricati."}), 503
rep = report_store.stats(cfg)
return jsonify({
"ok": True,
"server": world["server"],
"myPlayer": {
"id": cfg.get("my_player_id"),
"name": cfg.get("my_player_name"),
},
"fetched": {k: fmt_ts(v) for k, v in world.get("fetched", {}).items()},
"reportStats": rep,
})
@app.route("/api/galaxy")
def api_galaxy():
world = load_world()
if not world:
return jsonify({"ok": False, "error": "Dati non disponibili."}), 503
try:
g = int(request.args.get("g", 1))
s = int(request.args.get("s", 1))
except ValueError:
return jsonify({"ok": False, "error": "Parametri g/s non validi"}), 400
sd = world["server"]
if not (1 <= g <= sd["galaxies"]):
return jsonify({"ok": False, "error": f"Galassia fuori range (1-{sd['galaxies']})"}), 400
if not (1 <= s <= sd["systems"]):
return jsonify({"ok": False, "error": f"Sistema fuori range (1-{sd['systems']})"}), 400
players = world["players"]
planets = world["planets"]
cells = []
idx = report_store._load_index()
reports_idx = idx.get("reports", {})
for pos in range(1, 16):
cell = {"pos": pos}
key = f"{g}:{s}:{pos}"
planet = planets.get(key)
if planet:
pid = planet["player"]
pl = players.get(pid, {})
# eventuali rapporti su questo pianeta
rep_ids = [r for r in idx.get("by_coords", {}).get(key, [])]
rep_ids += [r for r in idx.get("by_coords", {}).get(key + "#moon", [])]
reps = []
for rid in rep_ids:
rec = reports_idx.get(rid)
if rec:
reps.append({
"sr_id": rid,
"moon": key + "#moon" in idx.get("by_coords", {}).get(key + "#moon", []),
"event_time": rec.get("event_time"),
"activity": rec.get("activity"),
})
reps.sort(key=lambda r: r.get("event_time") or "", reverse=True)
cell["planet"] = {
"id": planet["id"],
"name": planet["name"],
"coords": planet["coords"],
"report_only": planet.get("report_only", False),
"mine": str(pid) == str(load_config().get("my_player_id")),
}
cell["player"] = {
"id": pid,
"name": pl.get("name", ""),
"status": pl.get("status", ""),
"alliance_tag": pl.get("alliance_tag", ""),
}
if planet.get("moon"):
cell["moon"] = planet["moon"]
cell["reports"] = reps
# posizioni in classifica (generale/armamenti) del giocatore in cella
if cell.get("player"):
rk = (world.get("ranks") or {}).get(cell["player"]["id"], {})
cell["player"]["rank_total"] = rk.get("total")
cell["player"]["rank_military"] = rk.get("military")
cells.append(cell)
return jsonify({"ok": True, "galaxy": g, "system": s,
"galaxies": sd["galaxies"], "systems": sd["systems"],
"cells": cells})
@app.route("/api/planet")
def api_planet():
"""Dettaglio pianeta: info + tutti i rapporti di spionaggio salvati."""
world = load_world()
g = request.args.get("g")
s = request.args.get("s")
p = request.args.get("p")
if not (g and s and p):
return jsonify({"ok": False, "error": "Parametri g/s/p richiesti"}), 400
key = f"{g}:{s}:{p}"
planets = world["planets"]
planet = planets.get(key)
if not planet:
return jsonify({"ok": False, "error": "Pianeta non trovato in questo sistema."}), 404
player = world["players"].get(planet["player"], {})
rk = (world.get("ranks") or {}).get(planet["player"], {})
player = {**player, "rank_total": rk.get("total"), "rank_military": rk.get("military")}
ally = {}
if player.get("alliance"):
ally = world["alliances"].get(player["alliance"], {})
# altri pianeti dello stesso giocatore
others = [pl for k, pl in planets.items()
if pl["player"] == planet["player"] and k != key]
others.sort(key=lambda x: x["coords"])
# rapporti
reports = [
_report_view(rec, report_store.get_report(rec["sr_id"]))
for rec in report_store.list_by_coords(key, scope="planet")
]
moon_reports = [
_report_view(rec, report_store.get_report(rec["sr_id"]))
for rec in report_store.list_by_coords(key, scope="moon")
]
return jsonify({
"ok": True,
"planet": planet,
"player": {**player, "alliance_name": ally.get("name", "")},
"other_planets": others,
"reports": reports,
"moon_reports": moon_reports,
})
@app.route("/api/player")
def api_player():
"""Dettaglio giocatore: tutti i suoi pianeti + TUTTI i rapporti (pianeti e lune)."""
world = load_world()
if not world:
return jsonify({"ok": False, "error": "Dati non disponibili."}), 503
pid = request.args.get("id")
if not pid:
return jsonify({"ok": False, "error": "Parametro id richiesto"}), 400
player = world["players"].get(pid)
if not player:
return jsonify({"ok": False, "error": "Giocatore non trovato."}), 404
rk = (world.get("ranks") or {}).get(pid, {})
player = {**player, "rank_total": rk.get("total"), "rank_military": rk.get("military")}
ally = world["alliances"].get(player.get("alliance"), {}) if player.get("alliance") else {}
planets = [pl for pl in world["planets"].values() if pl["player"] == pid]
planets.sort(key=lambda x: [int(n) for n in x["coords"].split(":")])
# tutti i rapporti del giocatore, su qualunque suo pianeta o luna
idx = report_store._load_index()
by = idx.get("by_coords", {})
reports_meta = idx.get("reports", {})
seen = set()
reports = []
for pl in planets:
c = pl["coords"]
for key in (c, f"{c}#moon"):
for rid in by.get(key, []):
if rid in seen:
continue
rec = reports_meta.get(rid)
if rec:
seen.add(rid)
reports.append(_report_view(rec, report_store.get_report(rid)))
reports.sort(key=lambda r: r.get("event_timestamp") or 0, reverse=True)
return jsonify({
"ok": True,
"player": {**player, "alliance_name": ally.get("name", "")},
"avatar": (world.get("avatar") or {}).get(pid),
"planets": planets,
"reports": reports,
})
def _report_view(rec, full):
"""Vista sintetica (senza dati grezzi pesanti) per la lista rapporti."""
rep = (full or {}).get("report", {})
gen = rep.get("generic", {})
det = rep.get("details", {})
res = det.get("resources") or {}
return {
"sr_id": rec["sr_id"],
"saved_at": rec.get("saved_at"),
"event_time": rec.get("event_time"),
"event_timestamp": rec.get("event_timestamp"),
"attacker_name": rec.get("attacker_name"),
"defender_name": rec.get("defender_name"),
"defender_planet_name": rec.get("defender_planet_name"),
"coords": rec.get("coords"),
"activity": rec.get("activity"),
"loot_percentage": rec.get("loot_percentage"),
"spy_fail_chance": gen.get("spy_fail_chance"),
"total_ship_count": gen.get("total_ship_count"),
"total_defense_count": gen.get("total_defense_count"),
"resources": res,
"failed": {k: gen.get(k) for k in
("failed_ships", "failed_defense", "failed_buildings", "failed_research")},
"categories": {
"buildings": len(det.get("buildings") or []),
"lfbuildings": len(det.get("lfbuildings") or []),
"research": len(det.get("research") or []),
"ships": len(det.get("ships") or []),
"defense": len(det.get("defense") or []),
},
"moon": rec.get("planet_type") == 3,
}
# ---------------------------------------------------------------- rapporti
@app.route("/api/reports", methods=["POST"])
def api_add_report():
cfg = load_config()
body = request.get_json(silent=True) or {}
token = (body.get("token") or "").strip()
if not token:
return jsonify({"ok": False, "error": "Incolla l'API string del rapporto (es. sr-ar-170-...)."}), 400
with _report_lock:
try:
rec, created = report_store.import_report(token, cfg)
except ValueError as e:
return jsonify({"ok": False, "error": str(e)}), 400
except RuntimeError as e:
return jsonify({"ok": False, "error": str(e)}), 502
sr_id = rec["meta"]["sr_id"]
full = report_store.get_report(sr_id)
view = _report_view(rec["meta"], full)
return jsonify({"ok": True, "created": created, "report": view,
"coords": rec["meta"].get("coords"),
"planet_type": rec["meta"].get("planet_type")})
@app.route("/api/reports/<sr_id>")
def api_get_report(sr_id):
full = report_store.get_report(sr_id)
if not full:
return jsonify({"ok": False, "error": "Rapporto non trovato."}), 404
idx = report_store._load_index()
rec = idx["reports"].get(sr_id)
return jsonify({"ok": True, "meta": rec, "data": full["report"]})
@app.route("/api/reports/<sr_id>", methods=["DELETE"])
def api_delete_report(sr_id):
"""Elimina un rapporto di spionaggio (file + indice)."""
if not report_store.delete_report(sr_id):
return jsonify({"ok": False, "error": "Rapporto non trovato."}), 404
return jsonify({"ok": True, "deleted": sr_id})
@app.route("/api/refresh", methods=["POST"])
def api_refresh():
def _run():
try:
update_data.main()
except Exception as e: # noqa
print("refresh fallito:", e)
threading.Thread(target=_run, daemon=True).start()
return jsonify({"ok": True, "message": "Aggiornamento dati avviato in background."})
@app.route("/rankings")
def rankings_page():
return send_from_directory(STATIC, "rankings.html")
# mappa stato API -> gruppo di ricerca
_STATUS_GROUPS = {
"all": lambda st: True,
"active": lambda st: st == "",
"inactive": lambda st: st in ("i", "I"),
"vacation": lambda st: st == "v",
"vacation_inactive": lambda st: st in ("vi", "vI"),
"admin": lambda st: st == "a",
}
@app.route("/api/rankings")
def api_rankings():
"""Classifiche con filtri: type=total|military|fleet, range posizione, stato, testo."""
world = load_world()
if not world:
return jsonify({"ok": False, "error": "Dati non disponibili."}), 503
rtype = request.args.get("type", "total")
if rtype not in ("total", "military", "fleet"):
return jsonify({"ok": False, "error": "type deve essere total, military o fleet"}), 400
try:
start = int(request.args.get("start", 1) or 1)
end = int(request.args.get("end", 0) or 0)
except ValueError:
return jsonify({"ok": False, "error": "range non valido"}), 400
status = request.args.get("status", "all")
if status not in _STATUS_GROUPS:
return jsonify({"ok": False, "error": "stato non valido"}), 400
q = (request.args.get("q") or "").strip().lower()
players = world["players"]
ranks = world.get("ranks", {})
avatar = world.get("avatar", {})
pcount = {}
for pl in world["planets"].values():
pcount[pl["player"]] = pcount.get(pl["player"], 0) + 1
# rapporti di spionaggio per giocatore bersaglio (pianeta + luna) e
# coordinata del rapporto più recente (per il salto diretto dal badge)
rcount = {}
rcoord = {}
ridx = report_store._load_index()
reports_meta = ridx.get("reports", {})
for ckey, rids in ridx.get("by_coords", {}).items():
planet = world["planets"].get(ckey.split("#")[0])
if not planet:
continue
pid = planet["player"]
rcount[pid] = rcount.get(pid, 0) + len(rids)
for rid in rids:
ts = (reports_meta.get(rid) or {}).get("event_timestamp") or 0
if pid not in rcoord or ts > rcoord[pid][1]:
rcoord[pid] = (planet["coords"], ts)
# flotte conosciute (tab "Top Flotte"): per ogni coordinata (pianeta o luna)
# si prende il rapporto più recente, poi si sommano le navi per giocatore.
latest_by_coord = {}
for ckey, rids in ridx.get("by_coords", {}).items():
best = None
for rid in rids:
ts = (reports_meta.get(rid) or {}).get("event_timestamp") or 0
if best is None or ts > best[0]:
best = (ts, rid)
if best:
latest_by_coord[ckey] = best[1]
pships = {}
for ckey, rid in latest_by_coord.items():
planet = world["planets"].get(ckey.split("#")[0])
if not planet:
continue
pid = planet["player"]
ships = (reports_meta.get(rid) or {}).get("total_ship_count") or 0
pships[pid] = pships.get(pid, 0) + int(ships)
ok_status = _STATUS_GROUPS[status]
# --- tab "Top Flotte": solo giocatori con rapporti, ordinati per navi ---
if rtype == "fleet":
rows = []
mine_id = str(load_config().get("my_player_id"))
for pid, ships in pships.items():
p = players.get(pid)
if not p or not ok_status(p.get("status", "")):
continue
name = p.get("name", "")
tag = p.get("alliance_tag", "")
ally_name = p.get("alliance_name", "")
if q and not (q in name.lower() or q in tag.lower() or q in ally_name.lower()):
continue
rows.append({
"id": pid, "name": name, "status": p.get("status", ""),
"alliance_tag": tag, "alliance_name": ally_name,
"rank": None, "score": int(ships),
"avatar": avatar.get(pid), "planets": pcount.get(pid, 0),
"reports": rcount.get(pid, 0),
"report_coords": rcoord.get(pid, (None,))[0],
"mine": str(pid) == mine_id,
})
rows.sort(key=lambda r: r["score"], reverse=True)
for i, r in enumerate(rows, 1):
r["rank"] = i
if start > 1 or end:
rows = [r for r in rows
if r["rank"] >= start and (not end or r["rank"] <= end)]
return jsonify({"ok": True, "type": "fleet", "count": len(rows), "rows": rows})
rows = []
for pid, p in players.items():
if not ok_status(p.get("status", "")):
continue
name = p.get("name", "")
tag = p.get("alliance_tag", "")
ally_name = p.get("alliance_name", "")
if q and not (q in name.lower() or q in tag.lower() or q in ally_name.lower()):
continue
rk = ranks.get(pid, {})
rank = rk.get(rtype)
score = rk.get(rtype + "_score")
# filtro range posizione (solo per chi ha una posizione)
if (start > 1 or end):
if rank is None or rank < start or (end and rank > end):
continue
rows.append({
"id": pid,
"name": name,
"status": p.get("status", ""),
"alliance_tag": tag,
"alliance_name": ally_name,
"rank": rank,
"score": int(score) if score is not None else None,
"avatar": avatar.get(pid),
"planets": pcount.get(pid, 0),
"reports": rcount.get(pid, 0),
"report_coords": rcoord.get(pid, (None,))[0],
"mine": str(pid) == str(load_config().get("my_player_id")),
})
rows.sort(key=lambda r: (r["rank"] is None, r["rank"] or 10 ** 9))
return jsonify({"ok": True, "type": rtype, "count": len(rows), "rows": rows})
def _ensure_fresh_data():
"""All'avvio: aggiorna se l'ultimo fetch dei players è più vecchio di refresh_hours."""
cfg = load_config()
world = load_world()
stale = True
if world:
ft = world.get("fetched", {}).get("players") or 0
stale = (time.time() - ft) > cfg.get("refresh_hours", 24) * 3600
if stale:
print("Dati datati: aggiornamento all'avvio...")
try:
update_data.main()
except Exception as e:
print("Aggiornamento iniziale fallito:", e)
if __name__ == "__main__":
_ensure_fresh_data()
app.run(host="0.0.0.0", port=8899, debug=False)