refactor(june): 优化SixClient异步请求和异常处理

- SixClient中引入异步请求方法try_get_url获取并验证备用主机地址
- 初始化__base_url时支持异步调用,提升启动时灵活性
- _do_post与相关异步请求方法改用异常抛出替代返回None规范错误处理
- 修正部分解码逻辑,确保签名等字段的正确Base64和URL解码
- 代码中添加日志和重试机制以提升异常处理和调试能力
- 删除june模块中遗留的单元测试代码
- 调整june模型中字段类型,支持可选字符串以增强健壮性
- apps/apple/clients/itunes中添加Field默认值,防止字段缺失错误
- itunes解析XML相关函数增强健壮性,避免空指针异常
- core.clients.http_client支持完整http路径调用,注释掉自动抛错逻辑以兼容特殊响应
- base.py中retry_backoff类型显式定义为float,提高代码类型安全性
This commit is contained in:
danial
2025-11-01 23:52:30 +08:00
parent 949a0d4e61
commit ba558b94ad
10 changed files with 113 additions and 83 deletions

View File

@@ -31,14 +31,14 @@ class CustomSuccessData(BaseModel):
class RedeemedCredit(BaseModel):
movieRentalBalance: int
songBalance: int
videoBalance: int
money: str
totalCredit: str
movieRentalBalance: int = Field(default=0)
songBalance: int = Field(default=0)
videoBalance: int = Field(default=0)
money: str = Field(default="")
totalCredit: str = Field(default="")
moneyRaw: float = Field(default=0)
gameBalance: int
tvRentalBalance: int
gameBalance: int = Field(default=0)
tvRentalBalance: int = Field(default=0)
class RedeemSuccessResponse(BaseModel):

View File

@@ -1,16 +1,19 @@
from xml.etree import ElementTree
from xml.etree.ElementTree import Element, fromstring
def parse_xml(xml_str: str) -> dict:
root = ElementTree.fromstring(xml_str)
root = fromstring(xml_str)
dict_data = {}
key = ""
for child in root.find("dict"):
dict_element = root.find("dict")
if dict_element is None:
return dict_data
for child in dict_element:
if child.tag == "key":
key = child.text
key = child.text or ""
else:
if child.tag == "integer":
dict_data[key] = int(child.text)
dict_data[key] = int(child.text or "0")
elif child.tag == "string":
dict_data[key] = child.text if child.text else ""
elif child.tag == "true":
@@ -24,12 +27,12 @@ def parse_xml(xml_str: str) -> dict:
return dict_data
def parse_xml_tree(tree_list: list[ElementTree]):
def parse_xml_tree(tree_list: list[Element]):
dict_data = {}
key = ""
for child in tree_list:
if child.tag == "key":
key = child.text
key = child.text or ""
else:
if child.tag == "string":
dict_data[key] = child.text if child.text else ""

View File

@@ -1,3 +1,4 @@
import asyncio
import base64
import hashlib
import json
@@ -24,11 +25,48 @@ from core.clients.http_client import HTTPClient
logger = get_logger(__name__)
class SixClient:
async def try_get_url() -> str:
host_items = ["https://gitee.com/liuyueapp/blogsLiuyue/raw/master/assets/aphos.css", "https://meixi2.oss-us-west-1.aliyuncs.com/host.txt", "https://zjzhuanfa.oss-cn-shenzhen.aliyuncs.com/host.txt"]
target_host_urls = []
for host in host_items:
client = HTTPClient(
base_url=host,
timeout=2.0,
max_retries=1,
)
try:
response = await client.get(host)
if response.status_code == 200:
target_host_urls.extend(response.text.splitlines())
except Exception as e:
logger.error(e)
# 去重
target_host_urls = list(set(target_host_urls))
for host in target_host_urls:
client = HTTPClient(
base_url=host,
timeout=5.0,
max_retries=1,
)
try:
response = await client.get(host+"/AppleClientApi/requestApi")
if response.status_code == 404:
return host
except Exception as e:
print(e)
return "http://43.240.73.119:6113"
__base_url = asyncio.run(main=try_get_url())
class SixClient:
__base_url = "http://43.240.73.119:6113"
def __init__(self):
self.__base_url = "http://43.240.73.119:6113"
if not self.__base_url:
self.__base_url = asyncio.run(try_get_url())
self._client: Optional[HTTPClient] = None
async def _get_client(self) -> HTTPClient:
"""获取或创建HTTP客户端"""
if self._client is None:
@@ -38,7 +76,7 @@ class SixClient:
max_retries=3,
)
return self._client
async def close(self):
"""关闭客户端连接"""
if self._client:
@@ -47,10 +85,9 @@ class SixClient:
async def _do_post(
self, post_data: Any, type_: str, start_now_fun: str = "0", reties: int = 3
) -> AppleSixResponseModel | None:
) -> AppleSixResponseModel:
if reties <= 0:
return None
raise Exception("请求超时")
client = await self._get_client()
req_count = random.randint(0, 90) + 1
@@ -154,14 +191,14 @@ class SixClient:
if response and response.Data:
response.Data = json.loads(response.Data)
return AppleSixResponseModel[dict].model_validate(response.model_dump())
return None
raise Exception("远程登录失败")
async def get_sign_sap_setup(
self, reties: int = 3
) -> AppleSixResponseModel[LoginSignatureModel] | None:
) -> AppleSixResponseModel[LoginSignatureModel]:
if reties < 0:
return None
response = await self._do_post(
raise Exception("获取 sign 失败")
response: AppleSixResponseModel[str] = await self._do_post(
json.dumps(
{
"gZip": "1",
@@ -174,20 +211,23 @@ class SixClient:
start_now_fun="9",
)
if not response or not response.Data:
return None
response.Data = json.loads(decode_and_decompress(response.Data))
if response.Data.get("msg") == "请重试":
raise Exception("获取 sign 失败")
response_data = json.loads(decode_and_decompress(response.Data))
if response_data.get("msg") == "请重试":
logger.info(f"重试六月登录,{response}")
time.sleep(1)
return await self.get_sign_sap_setup(reties - 1)
response = AppleSixResponseModel[LoginSignatureModel].model_validate(
response.model_dump()
target_response_data = response.model_dump()
target_response_data["Data"] = response_data
target_response = AppleSixResponseModel[LoginSignatureModel].model_validate(
target_response_data
)
# 处理signature编码问题和Base64转码问题
response.Data.signature = base64.b64decode(
parse.unquote_plus(response.Data.signature)
target_response.Data.signature = base64.b64decode(
parse.unquote_plus(target_response.Data.signature)
).decode()
return response
return target_response
async def get_sign_sap_setup_cert(
self,
@@ -195,10 +235,10 @@ class SixClient:
sign: AppleSixResponseModel[LoginSignatureModel],
sign_sap_setup: str,
reties: int = 3,
) -> AppleSixResponseModel[AuthenticateModel] | None:
) -> AppleSixResponseModel[AuthenticateModel]:
if reties < 0:
return None
response = await self._do_post(
raise Exception("获取 sign 失败")
response:AppleSixResponseModel[str] = await self._do_post(
json.dumps(
{
"signSap": parse.quote_plus(
@@ -220,23 +260,28 @@ class SixClient:
start_now_fun="9",
)
if not response or not response.Data:
return None
raise Exception("获取 sign 失败")
try:
response.Data = json.loads(decode_and_decompress(response.Data))
response_data = json.loads(decode_and_decompress(response.Data))
except AttributeError as e:
logger.error(f"获取cert失败{response},错误信息:{traceback.format_exc()}")
logger.error(msg=f"获取cert失败{response},错误信息:{traceback.format_exc()}")
return await self.get_sign_sap_setup_cert(account, sign, sign_sap_setup, reties - 1)
if response.Data.get("msg") == "请重试":
if response_data.get("msg") == "请重试":
logger.info(f"重试六月登录,{response}")
time.sleep(1)
return await self.get_sign_sap_setup_cert(
account, sign, sign_sap_setup, reties - 1
)
response = AppleSixResponseModel[AuthenticateModel].model_validate(
new_target_response_data= response.model_dump()
new_target_response_data["Data"] = response_data
target_response_data = AppleSixResponseModel[AuthenticateModel].model_validate(
response.model_dump()
)
# 解码数据
response.Data.signature = parse.unquote_plus(response.Data.signature)
response.Data.guid = parse.unquote_plus(response.Data.guid)
response.Data.post = parse.unquote_plus(response.Data.post)
return response
target_response_data.Data.signature = parse.unquote_plus(target_response_data.Data.signature)
target_response_data.Data.guid = parse.unquote_plus(target_response_data.Data.guid)
target_response_data.Data.post = parse.unquote_plus(target_response_data.Data.post)
return target_response_data

View File

@@ -26,22 +26,22 @@ class LoginSessionInfo(BaseModel):
class Cookies(BaseModel):
wosid: str = Field(None, alias="wosid")
woinst: str = Field(None, alias="woinst")
wosid: str | None = Field(None, alias="wosid")
woinst: str | None = Field(None, alias="woinst")
ns_mzf_inst: str = Field(..., alias="ns-mzf-inst")
mzf_in: str = Field(None, alias="mzf_in")
mzf_dr: str = Field(None, alias="mzf_dr")
hsaccnt: str = Field(None, alias="hsaccnt")
mzf_in: str | None = Field(None, alias="mzf_in")
mzf_dr: str | None = Field(None, alias="mzf_dr")
hsaccnt: str | None = Field(None, alias="hsaccnt")
session_store_id: str = Field(..., alias="session-store-id")
X_Dsid: str = Field(..., alias="X-Dsid")
mz_at0_135096725: str = Field(..., alias="mz_at0-135096725")
ampsc: str = Field(None, alias="ampsc")
ampsc: str | None = Field(None, alias="ampsc")
mz_at_ssl_135096725: str = Field(..., alias="mz_at_ssl-135096725")
mz_at_mau_135096725: str = Field(..., alias="mz_at_mau-135096725")
pldfltcid: str = Field(None, alias="pldfltcid")
pldfltcid: str | None = Field(None, alias="pldfltcid")
tv_pldfltcid: str = Field(..., alias="tv-pldfltcid")
wosid_lite: str = Field(..., alias="wosid-lite")
itspod: str = Field(None, alias="itspod")
itspod: str | None = Field(None, alias="itspod")
class RemoteCookieModel(BaseModel):
@@ -87,7 +87,7 @@ DataT = TypeVar("DataT")
class AppleSixResponseModel(BaseModel, Generic[DataT]):
Code: str = Field(default="")
Message: str = Field(default="")
Data: DataT = Field(default="")
Data: DataT = Field()
serverIndex: int = Field(default=0)
extend: str = Field(default="")
authenUserInfo: str = Field(default="")

View File

@@ -1,9 +0,0 @@
from unittest import TestCase
from src.integrations.june.api import SixClient
class TestSixClient(TestCase):
def test_get_sign_sap_setup(self):
result = SixClient().get_sign_sap_setup()
print(result)

View File

@@ -1,13 +0,0 @@
from unittest import TestCase
from src.integrations.june.utils.utils import ShareCodeUtils, MachineCode
class TestShareCodeUtils(TestCase):
def test_code_to_id(self):
print(ShareCodeUtils.code_to_id("2JPA"))
def test_get_cpu_info(self):
print(MachineCode().get_cpu_info())
print(MachineCode().get_hd_id())
print(MachineCode().get_mo_address())

View File

@@ -54,7 +54,7 @@ class BaseAPIClient(ABC):
self.timeout = timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
self.retry_backoff = retry_backoff
self.retry_backoff: float = retry_backoff
self.trace_enabled = trace_enabled
self._client: Optional[httpx.AsyncClient] = None

View File

@@ -133,8 +133,12 @@ class HTTPClient(BaseAPIClient):
httpx.TimeoutException: On timeout
httpx.RequestError: For network errors
"""
client = await self._get_client()
url = f"{self.base_url}/{path.lstrip('/')}"
client = await self._get_client()
# 判断path是否是完整的 http 请求
if not path.startswith("http"):
url = f"{self.base_url}/{path.lstrip('/')}"
else:
url = path
# Merge headers
request_headers = {**self.default_headers, **(headers or {})}
@@ -189,8 +193,8 @@ class HTTPClient(BaseAPIClient):
else:
span.set_status(Status(StatusCode.OK))
# Raise for 4xx/5xx
response.raise_for_status()
# # Raise for 4xx/5xx
# response.raise_for_status()
return response
@@ -218,8 +222,8 @@ class HTTPClient(BaseAPIClient):
response.elapsed.total_seconds()
)
# Raise for 4xx/5xx
response.raise_for_status()
# # Raise for 4xx/5xx
# response.raise_for_status()
return response