#!/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/") 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/", 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)