Dotfiles/desktopenvs/hyprlua/astal-menu/backend/geolocate.py

78 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""IP geolocation with a provider fallback chain, cached with a TTL.
Prints JSON: {lat, lon, city, country, ip, source}. Diagnostics go to stderr,
non-zero exit on total failure. Used by services/location.py (and runnable
standalone for testing).
"""
from __future__ import annotations
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
CACHE = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "astal-menu" / "location.json"
TTL = 1800 # seconds
PROVIDERS = [
("ip-api", "http://ip-api.com/json/?fields=lat,lon,city,country,query",
lambda d: {"lat": d["lat"], "lon": d["lon"], "city": d.get("city"),
"country": d.get("country"), "ip": d.get("query")}),
("ipapi.co", "https://ipapi.co/json/",
lambda d: {"lat": d["latitude"], "lon": d["longitude"], "city": d.get("city"),
"country": d.get("country_name"), "ip": d.get("ip")}),
("ipinfo", "https://ipinfo.io/json",
lambda d: {"lat": float(d["loc"].split(",")[0]), "lon": float(d["loc"].split(",")[1]),
"city": d.get("city"), "country": d.get("country"), "ip": d.get("ip")}),
]
def _cached() -> dict | None:
try:
blob = json.loads(CACHE.read_text())
if time.time() - blob.get("_ts", 0) < TTL:
return blob["data"]
except (FileNotFoundError, json.JSONDecodeError, KeyError):
pass
return None
def _store(data: dict) -> None:
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps({"_ts": time.time(), "data": data}))
def _fetch(url: str) -> dict:
req = urllib.request.Request(url, headers={"User-Agent": "astal-menu/1.0"})
with urllib.request.urlopen(req, timeout=8) as resp:
return json.loads(resp.read().decode())
def main() -> int:
if "--no-cache" not in sys.argv:
hit = _cached()
if hit:
print(json.dumps(hit))
return 0
for name, url, parse in PROVIDERS:
try:
data = parse(_fetch(url))
data["source"] = name
if data.get("lat") is None or data.get("lon") is None:
raise ValueError("no coordinates")
_store(data)
print(json.dumps(data))
return 0
except Exception as exc: # noqa: BLE001 — try the next provider
print(f"{name}: {exc}", file=sys.stderr)
print("all geolocation providers failed", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())