zapret-kvn/xray_fluent/application/auto_switch_service.py
2026-04-07 20:49:48 +03:00

119 lines
4 KiB
Python

from __future__ import annotations
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..app_controller import AppController
from ..models import Node
AUTO_SWITCH_HIGH_TICKS_REQUIRED = 10
AUTO_SWITCH_IDLE_BPS = 1024.0
def check_auto_switch(controller: AppController, down_bps: float) -> None:
settings = controller.state.settings
if not settings.auto_switch_enabled:
return
if not controller.connected or controller._switching or controller._reconnecting:
return
if len(controller.state.nodes) < 2:
return
if controller._auto_switch_exhausted:
return
now = time.monotonic()
threshold_bps = settings.auto_switch_threshold_kbps * 1024.0
if down_bps >= threshold_bps:
controller._auto_switch_high_ticks += 1
if controller._auto_switch_high_ticks >= AUTO_SWITCH_HIGH_TICKS_REQUIRED:
controller._auto_switch_active_download = True
controller._auto_switch_low_since = 0.0
return
if not controller._auto_switch_active_download:
controller._auto_switch_high_ticks = 0
return
if down_bps < AUTO_SWITCH_IDLE_BPS:
controller._auto_switch_low_since = 0.0
controller._auto_switch_high_ticks = 0
controller._auto_switch_active_download = False
return
controller._auto_switch_high_ticks = 0
if controller._auto_switch_low_since == 0.0:
controller._auto_switch_low_since = now
return
low_duration = now - controller._auto_switch_low_since
if low_duration < settings.auto_switch_delay_sec:
return
if now - controller._auto_switch_last_switch < settings.auto_switch_cooldown_sec:
return
max_attempts = max(1, len(controller.state.nodes) - 1)
if controller._auto_switch_cycle_attempts >= max_attempts:
controller._auto_switch_exhausted = True
controller._auto_switch_low_since = 0.0
controller._auto_switch_active_download = False
controller.status.emit("warning", "Автопереключение остановлено: все серверы уже проверены")
controller._log("[auto-switch] exhausted all nodes for current session")
return
controller._auto_switch_low_since = 0.0
controller._auto_switch_last_switch = now
controller._auto_switch_active_download = False
next_node = get_next_node_for_auto_switch(controller)
if not next_node:
return
controller._auto_switch_cycle_attempts += 1
controller._auto_switch_transitioning = True
controller._log(
f"[auto-switch] speed {down_bps / 1024:.0f} KB/s < {settings.auto_switch_threshold_kbps} KB/s "
f"for {low_duration:.0f}s → switching to {next_node.name}"
)
controller.auto_switch_triggered.emit(next_node.name)
controller.state.selected_node_id = next_node.id
controller.selection_changed.emit(next_node)
controller.save()
controller._desired_connected = True
controller._request_transition("auto-switch: speed drop")
def get_next_node_for_auto_switch(controller: AppController) -> Node | None:
current_id = controller.state.selected_node_id
nodes = controller.state.nodes
if not nodes:
return None
candidates = [
node
for node in nodes
if node.id != current_id and node.is_alive is True and node.speed_mbps is not None and node.speed_mbps > 0
]
if candidates:
return max(candidates, key=lambda node: node.speed_mbps)
candidates = [node for node in nodes if node.id != current_id and node.is_alive is True]
if candidates:
return min(candidates, key=lambda node: node.ping_ms if node.ping_ms is not None else float("inf"))
current_idx: int | None = None
for idx, node in enumerate(nodes):
if node.id == current_id:
current_idx = idx
break
if current_idx is None:
return nodes[0]
next_idx = (current_idx + 1) % len(nodes)
if nodes[next_idx].id == current_id:
return None
return nodes[next_idx]