Hide POIs, show overlay state, remove zoom button, rotate vpn

This commit is contained in:
Andras Schmelczer 2026-07-15 21:58:05 +01:00
parent 35276d34fa
commit bce34b73de
32 changed files with 1137 additions and 186 deletions

View file

@ -27,14 +27,11 @@ import time
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse
import httpx
import gluetun
import shutdown
from constants import (
DATA_DIR,
DELAY_BETWEEN_PAGES,
GLUETUN_API_KEY,
GLUETUN_CONTROL_URL,
GLUETUN_MAX_ROTATIONS,
GLUETUN_PROXY,
MAX_BEDROOMS,
@ -472,110 +469,16 @@ def _challenge_timeout_seconds() -> int:
# ---------------------------------------------------------------------------
#
# When Cloudflare Turnstile fires mid-scrape, the cheapest unblocker is to
# swap the egress IP via Gluetun's HTTP control server. We stop and re-start
# the VPN, poll until the public IP changes, drop the stale cf_clearance
# cookies (bound to the previous IP), then reload and re-check the challenge.
def _gluetun_base_url() -> str:
return GLUETUN_CONTROL_URL.rstrip("/")
def _gluetun_api_key() -> str | None:
return GLUETUN_API_KEY
# swap the egress IP via Gluetun's HTTP control server, then drop the stale
# cf_clearance cookies (bound to the previous IP) and re-check the challenge.
# The control-server plumbing itself lives in gluetun.py, shared with
# http_client.py's egress-block detection.
def _gluetun_max_rotations() -> int:
return max(GLUETUN_MAX_ROTATIONS, 0)
def _gluetun_client() -> httpx.Client:
# Talks to the control server directly (not through the VPN proxy).
headers = {}
api_key = _gluetun_api_key()
if api_key:
headers["X-API-Key"] = api_key
return httpx.Client(headers=headers)
def _gluetun_public_ip(client: httpx.Client) -> str | None:
try:
resp = client.get(f"{_gluetun_base_url()}/v1/publicip/ip", timeout=5.0)
if resp.status_code != 200:
return None
data = resp.json()
except (httpx.HTTPError, ValueError):
return None
return data.get("public_ip") or data.get("ip")
def _gluetun_set_vpn_status(client: httpx.Client, status: str) -> bool:
"""PUT /v1/vpn/status with {'status': status}. Returns True on 2xx."""
try:
resp = client.put(
f"{_gluetun_base_url()}/v1/vpn/status",
json={"status": status},
timeout=15.0,
)
except httpx.HTTPError as exc:
log.warning("Gluetun vpn/status %s failed: %s", status, exc)
return False
if resp.status_code == 401:
log.warning(
"Gluetun vpn/status %s: 401 Unauthorized. The API key must be "
"authorised for 'PUT /v1/vpn/status' in Gluetun's auth config.toml",
status,
)
return False
if resp.status_code >= 400:
log.warning(
"Gluetun vpn/status %s returned HTTP %d: %s",
status, resp.status_code, resp.text[:200],
)
return False
return True
def _rotate_gluetun_ip(wait_seconds: int = 45) -> bool:
"""Restart Gluetun's VPN and wait for the public IP to change.
Returns True if a new IP was observed within wait_seconds."""
with _gluetun_client() as client:
old_ip = _gluetun_public_ip(client)
log.info("Requesting Gluetun IP rotation (current IP: %s)", old_ip or "unknown")
stop_attempted = False
restart_confirmed = False
try:
stop_attempted = True
if not _gluetun_set_vpn_status(client, "stopped"):
return False
time.sleep(2)
restart_confirmed = _gluetun_set_vpn_status(client, "running")
if not restart_confirmed:
return False
deadline = time.monotonic() + wait_seconds
while time.monotonic() < deadline:
time.sleep(2)
new_ip = _gluetun_public_ip(client)
if new_ip and new_ip != old_ip:
log.info("Gluetun rotated IP: %s -> %s", old_ip or "?", new_ip)
return True
finally:
if stop_attempted and not restart_confirmed:
log.warning(
"Gluetun VPN may be stopped after failed rotation; attempting recovery start"
)
if not _gluetun_set_vpn_status(client, "running"):
log.error(
"Gluetun VPN recovery start failed; manual intervention required"
)
log.warning("Gluetun IP did not change within %ds", wait_seconds)
return False
def _clear_cloudflare_cookies(page) -> None:
"""Drop cf_clearance / __cf_bm which are bound to the previous egress IP."""
try:
@ -596,7 +499,7 @@ def _rotate_and_retry_challenge(page, max_rotations: int) -> bool:
"Cloudflare Turnstile challenge, rotating Gluetun IP (attempt %d/%d)",
attempt, max_rotations,
)
if not _rotate_gluetun_ip():
if not gluetun.rotate_ip():
continue
_clear_cloudflare_cookies(page)