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): class RedeemedCredit(BaseModel):
movieRentalBalance: int movieRentalBalance: int = Field(default=0)
songBalance: int songBalance: int = Field(default=0)
videoBalance: int videoBalance: int = Field(default=0)
money: str money: str = Field(default="")
totalCredit: str totalCredit: str = Field(default="")
moneyRaw: float = Field(default=0) moneyRaw: float = Field(default=0)
gameBalance: int gameBalance: int = Field(default=0)
tvRentalBalance: int tvRentalBalance: int = Field(default=0)
class RedeemSuccessResponse(BaseModel): 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: def parse_xml(xml_str: str) -> dict:
root = ElementTree.fromstring(xml_str) root = fromstring(xml_str)
dict_data = {} dict_data = {}
key = "" 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": if child.tag == "key":
key = child.text key = child.text or ""
else: else:
if child.tag == "integer": if child.tag == "integer":
dict_data[key] = int(child.text) dict_data[key] = int(child.text or "0")
elif child.tag == "string": elif child.tag == "string":
dict_data[key] = child.text if child.text else "" dict_data[key] = child.text if child.text else ""
elif child.tag == "true": elif child.tag == "true":
@@ -24,12 +27,12 @@ def parse_xml(xml_str: str) -> dict:
return dict_data return dict_data
def parse_xml_tree(tree_list: list[ElementTree]): def parse_xml_tree(tree_list: list[Element]):
dict_data = {} dict_data = {}
key = "" key = ""
for child in tree_list: for child in tree_list:
if child.tag == "key": if child.tag == "key":
key = child.text key = child.text or ""
else: else:
if child.tag == "string": if child.tag == "string":
dict_data[key] = child.text if child.text else "" dict_data[key] = child.text if child.text else ""

View File

@@ -1,3 +1,4 @@
import asyncio
import base64 import base64
import hashlib import hashlib
import json import json
@@ -24,11 +25,48 @@ from core.clients.http_client import HTTPClient
logger = get_logger(__name__) 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): 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 self._client: Optional[HTTPClient] = None
async def _get_client(self) -> HTTPClient: async def _get_client(self) -> HTTPClient:
"""获取或创建HTTP客户端""" """获取或创建HTTP客户端"""
if self._client is None: if self._client is None:
@@ -38,7 +76,7 @@ class SixClient:
max_retries=3, max_retries=3,
) )
return self._client return self._client
async def close(self): async def close(self):
"""关闭客户端连接""" """关闭客户端连接"""
if self._client: if self._client:
@@ -47,10 +85,9 @@ class SixClient:
async def _do_post( async def _do_post(
self, post_data: Any, type_: str, start_now_fun: str = "0", reties: int = 3 self, post_data: Any, type_: str, start_now_fun: str = "0", reties: int = 3
) -> AppleSixResponseModel | None: ) -> AppleSixResponseModel:
if reties <= 0: if reties <= 0:
return None raise Exception("请求超时")
client = await self._get_client() client = await self._get_client()
req_count = random.randint(0, 90) + 1 req_count = random.randint(0, 90) + 1
@@ -154,14 +191,14 @@ class SixClient:
if response and response.Data: if response and response.Data:
response.Data = json.loads(response.Data) response.Data = json.loads(response.Data)
return AppleSixResponseModel[dict].model_validate(response.model_dump()) return AppleSixResponseModel[dict].model_validate(response.model_dump())
return None raise Exception("远程登录失败")
async def get_sign_sap_setup( async def get_sign_sap_setup(
self, reties: int = 3 self, reties: int = 3
) -> AppleSixResponseModel[LoginSignatureModel] | None: ) -> AppleSixResponseModel[LoginSignatureModel]:
if reties < 0: if reties < 0:
return None raise Exception("获取 sign 失败")
response = await self._do_post( response: AppleSixResponseModel[str] = await self._do_post(
json.dumps( json.dumps(
{ {
"gZip": "1", "gZip": "1",
@@ -174,20 +211,23 @@ class SixClient:
start_now_fun="9", start_now_fun="9",
) )
if not response or not response.Data: if not response or not response.Data:
return None raise Exception("获取 sign 失败")
response.Data = json.loads(decode_and_decompress(response.Data)) response_data = json.loads(decode_and_decompress(response.Data))
if response.Data.get("msg") == "请重试": if response_data.get("msg") == "请重试":
logger.info(f"重试六月登录,{response}") logger.info(f"重试六月登录,{response}")
time.sleep(1) time.sleep(1)
return await self.get_sign_sap_setup(reties - 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转码问题 # 处理signature编码问题和Base64转码问题
response.Data.signature = base64.b64decode( target_response.Data.signature = base64.b64decode(
parse.unquote_plus(response.Data.signature) parse.unquote_plus(target_response.Data.signature)
).decode() ).decode()
return response return target_response
async def get_sign_sap_setup_cert( async def get_sign_sap_setup_cert(
self, self,
@@ -195,10 +235,10 @@ class SixClient:
sign: AppleSixResponseModel[LoginSignatureModel], sign: AppleSixResponseModel[LoginSignatureModel],
sign_sap_setup: str, sign_sap_setup: str,
reties: int = 3, reties: int = 3,
) -> AppleSixResponseModel[AuthenticateModel] | None: ) -> AppleSixResponseModel[AuthenticateModel]:
if reties < 0: if reties < 0:
return None raise Exception("获取 sign 失败")
response = await self._do_post( response:AppleSixResponseModel[str] = await self._do_post(
json.dumps( json.dumps(
{ {
"signSap": parse.quote_plus( "signSap": parse.quote_plus(
@@ -220,23 +260,28 @@ class SixClient:
start_now_fun="9", start_now_fun="9",
) )
if not response or not response.Data: if not response or not response.Data:
return None raise Exception("获取 sign 失败")
try: try:
response.Data = json.loads(decode_and_decompress(response.Data)) response_data = json.loads(decode_and_decompress(response.Data))
except AttributeError as e: 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) 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}") logger.info(f"重试六月登录,{response}")
time.sleep(1) time.sleep(1)
return await self.get_sign_sap_setup_cert( return await self.get_sign_sap_setup_cert(
account, sign, sign_sap_setup, reties - 1 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.model_dump()
) )
# 解码数据 # 解码数据
response.Data.signature = parse.unquote_plus(response.Data.signature) target_response_data.Data.signature = parse.unquote_plus(target_response_data.Data.signature)
response.Data.guid = parse.unquote_plus(response.Data.guid) target_response_data.Data.guid = parse.unquote_plus(target_response_data.Data.guid)
response.Data.post = parse.unquote_plus(response.Data.post) target_response_data.Data.post = parse.unquote_plus(target_response_data.Data.post)
return response return target_response_data

View File

@@ -26,22 +26,22 @@ class LoginSessionInfo(BaseModel):
class Cookies(BaseModel): class Cookies(BaseModel):
wosid: str = Field(None, alias="wosid") wosid: str | None = Field(None, alias="wosid")
woinst: str = Field(None, alias="woinst") woinst: str | None = Field(None, alias="woinst")
ns_mzf_inst: str = Field(..., alias="ns-mzf-inst") ns_mzf_inst: str = Field(..., alias="ns-mzf-inst")
mzf_in: str = Field(None, alias="mzf_in") mzf_in: str | None = Field(None, alias="mzf_in")
mzf_dr: str = Field(None, alias="mzf_dr") mzf_dr: str | None = Field(None, alias="mzf_dr")
hsaccnt: str = Field(None, alias="hsaccnt") hsaccnt: str | None = Field(None, alias="hsaccnt")
session_store_id: str = Field(..., alias="session-store-id") session_store_id: str = Field(..., alias="session-store-id")
X_Dsid: str = Field(..., alias="X-Dsid") X_Dsid: str = Field(..., alias="X-Dsid")
mz_at0_135096725: str = Field(..., alias="mz_at0-135096725") 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_ssl_135096725: str = Field(..., alias="mz_at_ssl-135096725")
mz_at_mau_135096725: str = Field(..., alias="mz_at_mau-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") tv_pldfltcid: str = Field(..., alias="tv-pldfltcid")
wosid_lite: str = Field(..., alias="wosid-lite") wosid_lite: str = Field(..., alias="wosid-lite")
itspod: str = Field(None, alias="itspod") itspod: str | None = Field(None, alias="itspod")
class RemoteCookieModel(BaseModel): class RemoteCookieModel(BaseModel):
@@ -87,7 +87,7 @@ DataT = TypeVar("DataT")
class AppleSixResponseModel(BaseModel, Generic[DataT]): class AppleSixResponseModel(BaseModel, Generic[DataT]):
Code: str = Field(default="") Code: str = Field(default="")
Message: str = Field(default="") Message: str = Field(default="")
Data: DataT = Field(default="") Data: DataT = Field()
serverIndex: int = Field(default=0) serverIndex: int = Field(default=0)
extend: str = Field(default="") extend: str = Field(default="")
authenUserInfo: 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.timeout = timeout
self.max_retries = max_retries self.max_retries = max_retries
self.retry_delay = retry_delay self.retry_delay = retry_delay
self.retry_backoff = retry_backoff self.retry_backoff: float = retry_backoff
self.trace_enabled = trace_enabled self.trace_enabled = trace_enabled
self._client: Optional[httpx.AsyncClient] = None self._client: Optional[httpx.AsyncClient] = None

View File

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