feat: 增加桌面端登录、视频清单和封面缓存
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from urllib.request import getproxies
|
||||
|
||||
import httpx
|
||||
|
||||
from server.services.device import DeviceProfile
|
||||
|
||||
|
||||
class HttpClient:
|
||||
def __init__(self) -> None:
|
||||
self._proxy = {
|
||||
"mode": "off",
|
||||
"scheme": "http",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
}
|
||||
self._client: httpx.Client | None = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def configure_proxy(self, proxy: dict) -> None:
|
||||
data = proxy or {}
|
||||
mode = data.get("mode") or "off"
|
||||
if mode not in ("off", "system", "custom"):
|
||||
raise RuntimeError("代理方式无效")
|
||||
scheme = data.get("scheme") or "http"
|
||||
if scheme not in ("http", "socks5"):
|
||||
raise RuntimeError("代理协议无效")
|
||||
host = (data.get("host") or "").strip()
|
||||
port = str(data.get("port") or "").strip()
|
||||
if mode == "custom":
|
||||
if not host:
|
||||
raise RuntimeError("请填写代理地址")
|
||||
if not port.isdigit() or not 1 <= int(port) <= 65535:
|
||||
raise RuntimeError("请填写 1 到 65535 的端口")
|
||||
with self._lock:
|
||||
self._proxy = {
|
||||
"mode": mode,
|
||||
"scheme": scheme,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"username": (data.get("username") or "").strip(),
|
||||
"password": data.get("password") or "",
|
||||
}
|
||||
self._close_locked()
|
||||
|
||||
def request_json(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None) -> dict:
|
||||
data = self.request_data(method, url, token, device, body)
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("接口没有返回数据")
|
||||
return data
|
||||
|
||||
def request_data(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None):
|
||||
headers = device.headers()
|
||||
if token:
|
||||
headers["apasstk"] = token
|
||||
response = self._request(method, url, headers=headers, json=body)
|
||||
payload = response.json()
|
||||
if payload.get("code") != 200:
|
||||
raise RuntimeError(f"{method} {url} -> {payload.get('code')} {payload.get('msg')}")
|
||||
if "data" not in payload:
|
||||
raise RuntimeError("接口没有返回数据")
|
||||
return payload.get("data")
|
||||
|
||||
def _request(self, method: str, url: str, headers: dict, json: dict | None) -> httpx.Response:
|
||||
last_error = None
|
||||
for attempt in range(6):
|
||||
try:
|
||||
return self._client_locked().request(method, url, headers=headers, json=json)
|
||||
except (httpx.HTTPError, TimeoutError, OSError) as exc:
|
||||
last_error = exc
|
||||
time.sleep(min(10, 1.5 * (attempt + 1)))
|
||||
raise RuntimeError(f"请求失败: {last_error}")
|
||||
|
||||
def _client_locked(self) -> httpx.Client:
|
||||
with self._lock:
|
||||
if self._client is None or self._client.is_closed:
|
||||
self._client = httpx.Client(**self._client_kwargs())
|
||||
return self._client
|
||||
|
||||
def get_bytes(self, url: str) -> tuple[bytes, str]:
|
||||
response = self._client_locked().get(url, headers={"User-Agent": "RaptDrama"})
|
||||
response.raise_for_status()
|
||||
media = response.headers.get("content-type", "image/jpeg").split(";", 1)[0].strip() or "image/jpeg"
|
||||
if not media.startswith("image/"):
|
||||
raise RuntimeError("不是图片")
|
||||
return response.content, media
|
||||
|
||||
def _close_locked(self) -> None:
|
||||
if self._client is not None:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
|
||||
def _client_kwargs(self) -> dict:
|
||||
kwargs = {
|
||||
"trust_env": False,
|
||||
"follow_redirects": True,
|
||||
"timeout": httpx.Timeout(25.0),
|
||||
}
|
||||
mode = self._proxy["mode"]
|
||||
if mode == "off":
|
||||
kwargs["proxy"] = None
|
||||
return kwargs
|
||||
if mode == "custom":
|
||||
kwargs["proxy"] = self._proxy_url()
|
||||
return kwargs
|
||||
http_proxy, https_proxy = self._system_proxies()
|
||||
if http_proxy and https_proxy and http_proxy != https_proxy:
|
||||
kwargs["mounts"] = {
|
||||
"http://": httpx.HTTPTransport(proxy=http_proxy),
|
||||
"https://": httpx.HTTPTransport(proxy=https_proxy),
|
||||
}
|
||||
return kwargs
|
||||
kwargs["proxy"] = https_proxy or http_proxy or None
|
||||
return kwargs
|
||||
|
||||
def _proxy_url(self) -> str:
|
||||
scheme = "socks5h" if self._proxy["scheme"] == "socks5" else "http"
|
||||
auth = ""
|
||||
username = self._proxy["username"]
|
||||
if username:
|
||||
password = quote(self._proxy["password"] or "", safe="")
|
||||
auth = f"{quote(username, safe='')}:{password}@"
|
||||
return f"{scheme}://{auth}{self._proxy['host']}:{self._proxy['port']}"
|
||||
|
||||
@staticmethod
|
||||
def _system_proxies() -> tuple[str, str]:
|
||||
found = getproxies()
|
||||
return _with_scheme(found.get("http")), _with_scheme(found.get("https"))
|
||||
|
||||
|
||||
def _with_scheme(value: str | None) -> str:
|
||||
text = (value or "").strip()
|
||||
if text and "://" not in text:
|
||||
return "http://" + text
|
||||
return text
|
||||
|
||||
|
||||
http = HttpClient()
|
||||
Reference in New Issue
Block a user