"""代理获取、白名单同步、可用性验证的统一流程。""" import threading import time from collections.abc import Callable from typing import Protocol import requests from loguru import logger from .proxy_parser import parse_proxy_response from .proxy_verifier import verify_proxies_concurrent from .proxy_whitelist import DouyuWhitelistSyncer LogFunc = Callable[[str, str], None] class WhitelistSyncer(Protocol): """代理解析流程需要的白名单能力。""" def sync_ip(self, ip: str) -> tuple[bool, str]: ... def get_local_exit_ip(self) -> str | None: ... class ProxyResolver: """从代理 API 获取并验证代理,按需同步白名单。""" def __init__( self, api_url: str, whitelist_syncer: WhitelistSyncer | None = None, log_func: LogFunc | None = None, sync_local_exit_ip: bool = False, sync_whitelist_once: bool = True, stop_event: threading.Event | None = None, ): self.api_url = api_url self.whitelist_syncer = whitelist_syncer self.log_func = log_func self.sync_local_exit_ip = sync_local_exit_ip self.sync_whitelist_once = sync_whitelist_once self.stop_event = stop_event self._last_synced_ip: str | None = None self._has_synced_whitelist = False def _is_stopped(self) -> bool: return bool(self.stop_event and self.stop_event.is_set()) def _wait_or_stopped(self, seconds: float) -> bool: """等待 seconds 秒,期间收到停止信号返回 True。""" if self.stop_event: return self.stop_event.wait(seconds) if seconds > 0: time.sleep(seconds) return False def _log(self, level: str, message: str) -> None: if self.log_func: self.log_func(level, message) return log_method = getattr( logger, level if level in ("debug", "info", "warning", "error", "success") else "info", logger.info, ) log_method(message) def _sync_ip(self, ip: str) -> tuple[bool, str]: if not self.whitelist_syncer: return False, "未配置白名单 UID/UKEY" ok, sync_msg = self.whitelist_syncer.sync_ip(ip) if ok: self._last_synced_ip = ip self._has_synced_whitelist = True return ok, sync_msg def _sync_local_exit_ip_if_needed(self, attempt: int) -> None: if not self.sync_local_exit_ip or not self.whitelist_syncer: return local_ip = self.whitelist_syncer.get_local_exit_ip() if local_ip and local_ip != self._last_synced_ip: self._log("info", f"[尝试 {attempt}] 检测出口IP: {local_ip},同步白名单...") ok, sync_msg = self._sync_ip(local_ip) if ok: self._log("info", f"白名单同步成功: {sync_msg}") else: self._log("warning", f"白名单同步失败: {sync_msg}") elif local_ip == self._last_synced_ip: self._log("debug", f"[尝试 {attempt}] 出口IP未变: {local_ip}") def fetch_verified( self, max_attempts: int = 4, return_all: bool = False, ) -> tuple[str | list[str] | None, str]: """ 获取并验证代理。 Args: max_attempts: 最大尝试次数 return_all: True 返回所有可用代理,False 返回第一个可用代理 """ last_error = "" for attempt in range(1, max_attempts + 1): if self._is_stopped(): return None, "任务已停止" if attempt > 1: delay = min(attempt - 1, 2) if self.log_func: self._log("info", f"等待 {delay}s 后重试...") if self._wait_or_stopped(delay): return None, "任务已停止" self._sync_local_exit_ip_if_needed(attempt) if self._is_stopped(): return None, "任务已停止" self._log("info", f"代理预检 {attempt}/{max_attempts}: 正在获取代理") try: response = requests.get(self.api_url, timeout=10) response.raise_for_status() text = response.text.strip() proxy_urls, whitelist_ip = parse_proxy_response(text) if proxy_urls: self._log("info", f"获取到 {len(proxy_urls)} 个代理,并发验证") available, msg = verify_proxies_concurrent( proxy_urls, return_all=return_all ) if available: if not return_all: self._log("success", f"代理预检成功: {available}") return available, msg last_error = msg self._log( "warning", f"代理预检 {attempt}/{max_attempts}: {last_error}" ) continue if whitelist_ip and self.whitelist_syncer: if self.sync_whitelist_once and self._has_synced_whitelist: last_error = ( f"白名单已同步但代理API仍返回白名单错误: {whitelist_ip}" ) self._log("warning", last_error) continue self._log( "warning", f"代理需要白名单IP: {whitelist_ip},自动同步..." ) ok, sync_msg = self._sync_ip(whitelist_ip) self._log("success" if ok else "error", f"白名单同步: {sync_msg}") if ok: self._log("info", "白名单已更新,立即重试...") continue return None, f"白名单同步失败: {sync_msg}" last_error = "代理API响应无法解析" self._log( "warning", f"代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}", ) except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底 last_error = f"代理API请求失败: {exc}" self._log("warning", f"代理预检 {attempt}/{max_attempts}: {last_error}") return None, f"代理预检失败({max_attempts}次尝试均失败): {last_error}" def resolve_working_proxy( api_url: str, whitelist_platform: str = "xiequ", whitelist_credentials: dict | None = None, max_attempts: int = 4, log_func: LogFunc | None = None, ) -> tuple[str | None, str]: """从代理 API 获取可用代理,自动处理白名单同步。""" syncer = ( DouyuWhitelistSyncer( platform=whitelist_platform or "xiequ", credentials=whitelist_credentials, ) if whitelist_credentials else None ) resolver = ProxyResolver( api_url=api_url, whitelist_syncer=syncer, log_func=log_func, sync_local_exit_ip=False, sync_whitelist_once=True, ) proxy, msg = resolver.fetch_verified(max_attempts=max_attempts, return_all=False) if isinstance(proxy, list): return (proxy[0] if proxy else None), msg return proxy, msg