Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e231a4ab | |||
| a11f8c4fa0 | |||
| cf5a06af11 | |||
| e07f75432e | |||
| 1696d5c65b | |||
| c8b386f47a | |||
| 3888da352a |
@@ -1 +1 @@
|
||||
v1.1.7
|
||||
v1.1.12
|
||||
|
||||
@@ -191,10 +191,21 @@ start_vpn() {
|
||||
> /etc/resolv.conf
|
||||
for dns in $(echo "$VPN_DNS" | tr ',' ' '); do
|
||||
echo "nameserver $dns" >> /etc/resolv.conf
|
||||
# Add explicit route to DNS server through wg0 so it's found in main table
|
||||
# (suppress_prefixlength 0 ignores default routes but allows host routes)
|
||||
ip -4 route add "$dns" dev "$INTERFACE" 2>/dev/null || true
|
||||
done
|
||||
echo "[vpn] DNS set to: ${VPN_DNS}"
|
||||
fi
|
||||
|
||||
# Add explicit host route for the health-check target so it is picked up by
|
||||
# the 'lookup main suppress_prefixlength 0' rule (same as DNS servers above).
|
||||
# Without this, CHECK_HOST falls through to the VPN table default route whose
|
||||
# source-address selection can be defeated by the priority-100 'from ETH0_IP'
|
||||
# policy rule, causing pings to bypass wg0 and be dropped by the kill switch.
|
||||
ip -4 route add "${CHECK_HOST}" dev "$INTERFACE" 2>/dev/null || true
|
||||
echo "[vpn] Health-check route: ${CHECK_HOST} → ${INTERFACE}"
|
||||
|
||||
echo "[vpn] WireGuard interface ${INTERFACE} is up."
|
||||
echo "[vpn] Main routes:"
|
||||
ip route show | sed 's/^/[vpn] /'
|
||||
@@ -246,9 +257,21 @@ health_loop() {
|
||||
echo "[health] VPN recovered."
|
||||
failures=0
|
||||
fi
|
||||
# Secondary DNS check
|
||||
if ping -c 1 -W 5 "google.com" > /dev/null 2>&1; then
|
||||
: # DNS OK — silent
|
||||
else
|
||||
echo "[health] WARN google.com unreachable — possible DNS issue"
|
||||
fi
|
||||
else
|
||||
failures=$((failures + 1))
|
||||
echo "[health] Check failed ($failures/$max_failures) — ping ${CHECK_HOST} failed"
|
||||
# Secondary check: distinguish IP failure from DNS failure
|
||||
if ping -c 1 -W 5 "google.com" > /dev/null 2>&1; then
|
||||
echo "[health] INFO google.com reachable — DNS works, ${CHECK_HOST} may be filtered"
|
||||
else
|
||||
echo "[health] INFO google.com also unreachable — DNS or general routing failure"
|
||||
fi
|
||||
# Dump WireGuard stats to show if handshake is stale and how much data flows
|
||||
echo "[health] wg stats:"
|
||||
wg show "$INTERFACE" 2>/dev/null | grep -E 'latest handshake|transfer|endpoint' | sed 's/^/[health] /' || echo "[health] wg0 not found"
|
||||
@@ -349,6 +372,8 @@ check_vpn_connectivity() {
|
||||
echo "[check] FAIL DNS resolution failed"
|
||||
echo "[check] resolv.conf: $(tr '\n' ' ' < /etc/resolv.conf)"
|
||||
echo "[check] Check that DNS servers are reachable through wg0"
|
||||
echo "[check] ── End of checks ──"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[check] ── End of checks ──"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[Interface]
|
||||
PrivateKey = EPRa2f/v72LvIXAY4yqIRJifsSb+nCcYHqC2rwA94UI=
|
||||
Address = 100.64.244.78/32
|
||||
DNS = 198.18.0.1,198.18.0.2
|
||||
#DNS = 198.18.0.1,198.18.0.2
|
||||
DNS = 8.8.8.8
|
||||
|
||||
# Route zum VPN-Server direkt über dein lokales Netz
|
||||
PostUp = ip route add 91.148.236.64 via 192.168.178.1 dev wlp4s0f0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aniworld-web",
|
||||
"version": "1.1.7",
|
||||
"version": "1.1.12",
|
||||
"description": "Aniworld Anime Download Manager - Web Frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -12,6 +12,7 @@ Example:
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -63,6 +64,11 @@ class TMDBClient:
|
||||
self.max_connections = max_connections
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
self._cache: Dict[str, Any] = {}
|
||||
# TMDB allows ~40 req/s; use 30 concurrent + per-second throttle to stay safe
|
||||
self._semaphore = asyncio.Semaphore(30)
|
||||
self._rate_limit_lock = asyncio.Lock()
|
||||
self._request_timestamps: List[float] = []
|
||||
self._max_requests_per_second = 35 # Stay under TMDB's ~40/s limit
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
@@ -83,7 +89,7 @@ class TMDBClient:
|
||||
self,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
max_retries: int = 3
|
||||
max_retries: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""Make an async request to TMDB API with retries.
|
||||
|
||||
@@ -110,58 +116,82 @@ class TMDBClient:
|
||||
logger.debug("Cache hit for %s", endpoint)
|
||||
return self._cache[cache_key]
|
||||
|
||||
delay = 1
|
||||
delay = 2
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Re-ensure session before each attempt in case it was closed
|
||||
await self._ensure_session()
|
||||
|
||||
if self.session is None:
|
||||
raise TMDBAPIError("Session is not available")
|
||||
|
||||
logger.debug("TMDB API request: %s (attempt %s)", endpoint, attempt + 1)
|
||||
async with self.session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
||||
if resp.status == 401:
|
||||
raise TMDBAPIError("Invalid TMDB API key")
|
||||
elif resp.status == 404:
|
||||
raise TMDBAPIError(f"Resource not found: {endpoint}")
|
||||
elif resp.status == 429:
|
||||
# Rate limit - wait longer
|
||||
retry_after = int(resp.headers.get('Retry-After', delay * 2))
|
||||
logger.warning("Rate limited, waiting %ss", retry_after)
|
||||
await asyncio.sleep(retry_after)
|
||||
continue
|
||||
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
self._cache[cache_key] = data
|
||||
return data
|
||||
|
||||
except asyncio.TimeoutError as e:
|
||||
last_error = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning("Request timeout (attempt %s), retrying in %ss", attempt + 1, delay)
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
else:
|
||||
logger.error("Request timed out after %s attempts", max_retries)
|
||||
|
||||
except (aiohttp.ClientError, AttributeError) as e:
|
||||
last_error = e
|
||||
# If connector/session was closed, try to recreate it
|
||||
if "Connector is closed" in str(e) or isinstance(e, AttributeError):
|
||||
logger.warning("Session issue detected, recreating session: %s", e)
|
||||
self.session = None
|
||||
# Rate limiting: ensure we don't exceed ~35 requests/second
|
||||
async with self._rate_limit_lock:
|
||||
now = time.monotonic()
|
||||
# Remove timestamps older than 1 second
|
||||
self._request_timestamps = [
|
||||
ts for ts in self._request_timestamps if now - ts < 1.0
|
||||
]
|
||||
if len(self._request_timestamps) >= self._max_requests_per_second:
|
||||
sleep_time = 1.0 - (now - self._request_timestamps[0])
|
||||
if sleep_time > 0:
|
||||
logger.debug("Rate throttling: waiting %.2fs", sleep_time)
|
||||
await asyncio.sleep(sleep_time)
|
||||
self._request_timestamps.append(time.monotonic())
|
||||
|
||||
async with self._semaphore:
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Re-ensure session before each attempt in case it was closed
|
||||
await self._ensure_session()
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning("Request failed (attempt %s): %s, retrying in %ss", attempt + 1, e, delay)
|
||||
await asyncio.sleep(delay)
|
||||
delay *= 2
|
||||
else:
|
||||
logger.error("Request failed after %s attempts: %s", max_retries, e)
|
||||
|
||||
if self.session is None:
|
||||
raise TMDBAPIError("Session is not available")
|
||||
|
||||
logger.debug("TMDB API request: %s (attempt %s)", endpoint, attempt + 1)
|
||||
async with self.session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
||||
if resp.status == 401:
|
||||
raise TMDBAPIError("Invalid TMDB API key")
|
||||
elif resp.status == 404:
|
||||
raise TMDBAPIError(f"Resource not found: {endpoint}")
|
||||
elif resp.status == 429:
|
||||
# Rate limit - wait longer with exponential backoff
|
||||
retry_after = int(resp.headers.get('Retry-After', max(delay * 2, 10)))
|
||||
logger.warning("Rate limited, waiting %ss", retry_after)
|
||||
await asyncio.sleep(retry_after)
|
||||
continue
|
||||
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
self._cache[cache_key] = data
|
||||
return data
|
||||
|
||||
except asyncio.TimeoutError as e:
|
||||
last_error = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning("Request timeout (attempt %s), retrying in %ss", attempt + 1, delay)
|
||||
await asyncio.sleep(delay)
|
||||
delay = min(delay * 2, 30)
|
||||
else:
|
||||
logger.error("Request timed out after %s attempts", max_retries)
|
||||
|
||||
except (aiohttp.ClientError, AttributeError) as e:
|
||||
last_error = e
|
||||
# If connector/session was closed, try to recreate it
|
||||
if "Connector is closed" in str(e) or isinstance(e, AttributeError):
|
||||
logger.warning("Session issue detected, recreating session: %s", e)
|
||||
self.session = None
|
||||
await self._ensure_session()
|
||||
|
||||
# DNS / host-unreachable errors are not transient — abort immediately
|
||||
error_str = str(e)
|
||||
if "name resolution" in error_str.lower() or (
|
||||
isinstance(e, aiohttp.ClientConnectorError) and
|
||||
"Cannot connect to host" in error_str
|
||||
):
|
||||
logger.error("Non-transient connection error, aborting retries: %s", e)
|
||||
raise TMDBAPIError(f"Request failed after {attempt + 1} attempts: {e}") from e
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning("Request failed (attempt %s): %s, retrying in %ss", attempt + 1, e, delay)
|
||||
await asyncio.sleep(delay)
|
||||
delay = min(delay * 2, 30)
|
||||
else:
|
||||
logger.error("Request failed after %s attempts: %s", max_retries, e)
|
||||
|
||||
raise TMDBAPIError(f"Request failed after {max_retries} attempts: {last_error}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user