resolve ip
This commit is contained in:
@@ -347,6 +347,7 @@ def save_preferences(data: dict, user_id: int | None = None):
|
||||
footer_items_json = data.get("footer_items_json")
|
||||
title_speed_enabled = data.get("title_speed_enabled")
|
||||
tracker_favicons_enabled = data.get("tracker_favicons_enabled")
|
||||
reverse_dns_enabled = data.get("reverse_dns_enabled")
|
||||
automation_toasts_enabled = data.get("automation_toasts_enabled")
|
||||
smart_queue_toasts_enabled = data.get("smart_queue_toasts_enabled")
|
||||
disk_monitor_paths_json = data.get("disk_monitor_paths_json")
|
||||
@@ -387,6 +388,9 @@ def save_preferences(data: dict, user_id: int | None = None):
|
||||
conn.execute("UPDATE user_preferences SET title_speed_enabled=?, updated_at=? WHERE user_id=?", (1 if title_speed_enabled else 0, now, user_id))
|
||||
if tracker_favicons_enabled is not None:
|
||||
conn.execute("UPDATE user_preferences SET tracker_favicons_enabled=?, updated_at=? WHERE user_id=?", (1 if tracker_favicons_enabled else 0, now, user_id))
|
||||
if reverse_dns_enabled is not None:
|
||||
# Note: Reverse DNS is optional because peer PTR lookups can add latency on busy swarms.
|
||||
conn.execute("UPDATE user_preferences SET reverse_dns_enabled=?, updated_at=? WHERE user_id=?", (1 if reverse_dns_enabled else 0, now, user_id))
|
||||
if automation_toasts_enabled is not None:
|
||||
# Note: Lets users silence automation-created toast noise without hiding job/history data.
|
||||
conn.execute("UPDATE user_preferences SET automation_toasts_enabled=?, updated_at=? WHERE user_id=?", (1 if automation_toasts_enabled else 0, now, user_id))
|
||||
|
||||
99
pytorrent/services/reverse_dns.py
Normal file
99
pytorrent/services/reverse_dns.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
_CACHE_TTL_SECONDS = 24 * 60 * 60
|
||||
_NEGATIVE_TTL_SECONDS = 60 * 60
|
||||
_CACHE_LIMIT = 2048
|
||||
_LOOKUP_LIMIT_PER_REQUEST = 24
|
||||
_LOOKUP_TIMEOUT_SECONDS = 0.8
|
||||
|
||||
_cache: dict[str, tuple[str, float]] = {}
|
||||
_pending: dict[str, Any] = {}
|
||||
_lock = Lock()
|
||||
_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="reverse-dns")
|
||||
|
||||
|
||||
def _is_resolvable_ip(value: str) -> bool:
|
||||
try:
|
||||
ipaddress.ip_address(str(value or "").strip())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _lookup_host(ip: str) -> str:
|
||||
try:
|
||||
host = socket.gethostbyaddr(ip)[0]
|
||||
return str(host or "").rstrip(".")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _trim_cache(now: float) -> None:
|
||||
expired = [ip for ip, (_, expires_at) in _cache.items() if expires_at <= now]
|
||||
for ip in expired:
|
||||
_cache.pop(ip, None)
|
||||
if len(_cache) <= _CACHE_LIMIT:
|
||||
return
|
||||
for ip, _ in sorted(_cache.items(), key=lambda item: item[1][1])[: len(_cache) - _CACHE_LIMIT]:
|
||||
_cache.pop(ip, None)
|
||||
|
||||
|
||||
def _store(ip: str, host: str, now: float | None = None) -> None:
|
||||
now = now or time.monotonic()
|
||||
ttl = _CACHE_TTL_SECONDS if host else _NEGATIVE_TTL_SECONDS
|
||||
_cache[ip] = (host, now + ttl)
|
||||
|
||||
|
||||
def attach_reverse_dns(peers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Attach cached or newly resolved PTR hostnames to peer rows with a small request budget."""
|
||||
now = time.monotonic()
|
||||
missing: list[str] = []
|
||||
with _lock:
|
||||
_trim_cache(now)
|
||||
for peer in peers:
|
||||
ip = str(peer.get("ip") or "").strip()
|
||||
if not ip or not _is_resolvable_ip(ip):
|
||||
peer["host"] = ""
|
||||
continue
|
||||
cached = _cache.get(ip)
|
||||
if cached and cached[1] > now:
|
||||
peer["host"] = cached[0]
|
||||
continue
|
||||
peer["host"] = ""
|
||||
if ip not in _pending and ip not in missing and len(missing) < _LOOKUP_LIMIT_PER_REQUEST:
|
||||
missing.append(ip)
|
||||
for ip in missing:
|
||||
_pending[ip] = _executor.submit(_lookup_host, ip)
|
||||
futures = list(_pending.items())
|
||||
|
||||
if futures:
|
||||
wait([future for _, future in futures], timeout=_LOOKUP_TIMEOUT_SECONDS)
|
||||
|
||||
done_hosts: dict[str, str] = {}
|
||||
with _lock:
|
||||
now = time.monotonic()
|
||||
for ip, future in list(_pending.items()):
|
||||
if not future.done():
|
||||
continue
|
||||
try:
|
||||
host = str(future.result() or "")
|
||||
except Exception:
|
||||
host = ""
|
||||
_store(ip, host, now)
|
||||
done_hosts[ip] = host
|
||||
_pending.pop(ip, None)
|
||||
|
||||
for peer in peers:
|
||||
ip = str(peer.get("ip") or "").strip()
|
||||
if ip in done_hosts:
|
||||
peer["host"] = done_hosts[ip]
|
||||
elif not peer.get("host") and ip in _pending:
|
||||
peer["host_pending"] = True
|
||||
return peers
|
||||
Reference in New Issue
Block a user