增加斗鱼账号检测功能
This commit is contained in:
+179
-36
@@ -30,7 +30,16 @@ _geetest_semaphore = threading.Semaphore(2)
|
||||
|
||||
class CredentialError(ValueError):
|
||||
"""账号或密码错误,不应重试。"""
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
code: str = "credential_error",
|
||||
status_message: str = "",
|
||||
):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.status_message = status_message or message
|
||||
|
||||
|
||||
class AccountLike(Protocol):
|
||||
@@ -62,6 +71,8 @@ class DouyuLogin:
|
||||
SEND_EMAIL_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/sendemail"
|
||||
VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify"
|
||||
WEBLOGIN_API = "https://msg.douyu.com/webLogin"
|
||||
CP_RPC_API = "https://www.douyu.com/member/cp/cp_rpc_ajax"
|
||||
CP_REFERER = "https://www.douyu.com/member/cp"
|
||||
LOGIN_REFERER = (
|
||||
"https://passport.douyu.com/index/login?"
|
||||
"passport_reg_callback=PASSPORT_REG_SUCCESS_CALLBACK&"
|
||||
@@ -242,6 +253,64 @@ class DouyuLogin:
|
||||
preview = body[:200].replace("\n", "\\n")
|
||||
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
|
||||
|
||||
@staticmethod
|
||||
def _classify_credential_payload(payload: dict) -> tuple[str, str] | None:
|
||||
"""识别登录接口返回的账号类终态。"""
|
||||
error_code = payload.get('error')
|
||||
error_msg = str(payload.get('msg') or '')
|
||||
|
||||
if error_code == 110022 or '账号不存在' in error_msg:
|
||||
return "account_cancelled", "账号已注销"
|
||||
|
||||
password_keywords = ['密码错误', '账号或密码', '账号或者密码']
|
||||
if error_code == 110018 or any(kw in error_msg for kw in password_keywords):
|
||||
return "password_wrong", "账号密码错误"
|
||||
|
||||
return None
|
||||
|
||||
def _credential_error_from_payload(self, stage: str, payload: dict) -> CredentialError | None:
|
||||
"""把账号类错误转换为不重试的 CredentialError。"""
|
||||
classified = self._classify_credential_payload(payload)
|
||||
if not classified:
|
||||
return None
|
||||
code, status_message = classified
|
||||
return CredentialError(
|
||||
f"{stage}失败: {status_message}",
|
||||
code=code,
|
||||
status_message=status_message,
|
||||
)
|
||||
|
||||
def _run_login_steps(self, deadline: float = 0) -> str:
|
||||
"""执行一次完整登录链路,成功后返回 Cookie。"""
|
||||
# 1️⃣ 第一次登录(获取极验参数)
|
||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||
gt, challenge, code_token, _ = self._first_login()
|
||||
|
||||
# 2️⃣ 极验 fullpage 验证
|
||||
logger.info("步骤2: 极验 fullpage 验证...")
|
||||
validate, seccode = self._solve_geetest(gt, challenge, deadline=deadline)
|
||||
|
||||
# 3️⃣ 第二次登录(带极验)
|
||||
logger.info("步骤3: 第二次登录(带极验验证)...")
|
||||
remote_code = self._second_login(gt, challenge, validate, seccode, code_token)
|
||||
|
||||
# 4️⃣ 发送邮箱验证
|
||||
logger.info("步骤4: 发送邮箱验证...")
|
||||
email_sent_at = time.time()
|
||||
self._send_email_verify(remote_code)
|
||||
|
||||
# 5️⃣ IMAP获取验证码
|
||||
logger.info("步骤5: 获取邮箱验证码...")
|
||||
verify_code = self._get_email_code(after_timestamp=email_sent_at)
|
||||
|
||||
# 6️⃣ 提交验证码
|
||||
logger.info("步骤6: 提交验证码...")
|
||||
login_url = self._submit_verify_code(remote_code, verify_code)
|
||||
|
||||
# 7️⃣ 完成登录获取Cookie
|
||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||
return self._complete_login(login_url)
|
||||
|
||||
def login(self) -> LoginResult:
|
||||
"""
|
||||
完整登录流程(带整体重试)。
|
||||
@@ -280,34 +349,7 @@ class DouyuLogin:
|
||||
)
|
||||
|
||||
try:
|
||||
# 1️⃣ 第一次登录(获取极验参数)
|
||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||
gt, challenge, code_token, _ = self._first_login()
|
||||
|
||||
# 2️⃣ 极验 fullpage 验证
|
||||
logger.info("步骤2: 极验 fullpage 验证...")
|
||||
validate, seccode = self._solve_geetest(gt, challenge, deadline=deadline)
|
||||
|
||||
# 3️⃣ 第二次登录(带极验)
|
||||
logger.info("步骤3: 第二次登录(带极验验证)...")
|
||||
remote_code = self._second_login(gt, challenge, validate, seccode, code_token)
|
||||
|
||||
# 4️⃣ 发送邮箱验证
|
||||
logger.info("步骤4: 发送邮箱验证...")
|
||||
email_sent_at = time.time()
|
||||
self._send_email_verify(remote_code)
|
||||
|
||||
# 5️⃣ IMAP获取验证码
|
||||
logger.info("步骤5: 获取邮箱验证码...")
|
||||
verify_code = self._get_email_code(after_timestamp=email_sent_at)
|
||||
|
||||
# 6️⃣ 提交验证码
|
||||
logger.info("步骤6: 提交验证码...")
|
||||
login_url = self._submit_verify_code(remote_code, verify_code)
|
||||
|
||||
# 7️⃣ 完成登录获取Cookie
|
||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||
cookie = self._complete_login(login_url)
|
||||
cookie = self._run_login_steps(deadline=deadline)
|
||||
message = "登录成功"
|
||||
if self._cookie_enrich_error:
|
||||
message = f"登录成功,补CK失败: {self._cookie_enrich_error}"
|
||||
@@ -320,7 +362,7 @@ class DouyuLogin:
|
||||
return LoginResult(success=False, message=str(e))
|
||||
except CredentialError as e:
|
||||
logger.error(f"登录失败(凭据错误,不再重试): {e}")
|
||||
return LoginResult(success=False, message=str(e), code="credential_error")
|
||||
return LoginResult(success=False, message=str(e), code=e.code)
|
||||
except EmailLoginError as e:
|
||||
logger.error(f"登录失败(邮箱登录失败,不再重试): {e}")
|
||||
return LoginResult(success=False, message=str(e), code="email_login_failed")
|
||||
@@ -339,6 +381,107 @@ class DouyuLogin:
|
||||
# 所有重试耗尽或超时
|
||||
return LoginResult(success=False, message=str(e))
|
||||
|
||||
def check_account(self) -> LoginResult:
|
||||
"""
|
||||
检测账号状态。
|
||||
|
||||
可直接识别注销/密码错误;登录成功后请求个人中心接口判断实名状态。
|
||||
"""
|
||||
logger.info(f"开始检测账号: {self.account.username}")
|
||||
start_time = time.monotonic()
|
||||
deadline = start_time + self.max_total_time if self.max_total_time > 0 else 0
|
||||
|
||||
attempt = 0
|
||||
while True:
|
||||
attempt += 1
|
||||
if self._is_stopped():
|
||||
logger.warning("账号检测任务已停止")
|
||||
return LoginResult(success=False, message="任务已停止")
|
||||
elapsed = time.monotonic() - start_time
|
||||
if self.max_total_time > 0 and elapsed > self.max_total_time:
|
||||
logger.warning(f"账号检测总耗时 {elapsed:.0f}s 超过上限 {self.max_total_time}s,放弃")
|
||||
return LoginResult(success=False, message=f"账号检测超时({elapsed:.0f}s > {self.max_total_time}s)")
|
||||
|
||||
if attempt > 1:
|
||||
if self.max_login_retries > 0:
|
||||
logger.info(f"账号检测整体重试 {attempt}/{self.max_login_retries},换新代理从头开始")
|
||||
else:
|
||||
logger.info(f"账号检测整体重试 {attempt} (无限重试),换新代理从头开始")
|
||||
if not self._prepare_retry():
|
||||
return LoginResult(
|
||||
success=False,
|
||||
message=f"静态代理连续失败 {self.MAX_STATIC_RETRY} 次,无法切换代理",
|
||||
)
|
||||
|
||||
try:
|
||||
self._run_login_steps(deadline=deadline)
|
||||
return self._check_certification_status()
|
||||
|
||||
except InterruptedError as e:
|
||||
logger.warning(f"账号检测任务已停止: {e}")
|
||||
return LoginResult(success=False, message=str(e))
|
||||
except CredentialError as e:
|
||||
logger.info(f"账号检测完成: {e.status_message}")
|
||||
return LoginResult(success=True, message=e.status_message, code=e.code)
|
||||
except EmailLoginError as e:
|
||||
logger.error(f"账号检测失败(邮箱登录失败,不再重试): {e}")
|
||||
return LoginResult(success=False, message=str(e), code="email_login_failed")
|
||||
except Exception as e:
|
||||
elapsed = time.monotonic() - start_time
|
||||
if self.max_login_retries > 0:
|
||||
logger.error(f"账号检测失败(尝试 {attempt}/{self.max_login_retries},已耗时 {elapsed:.0f}s): {e}")
|
||||
else:
|
||||
logger.error(f"账号检测失败(尝试 {attempt},已耗时 {elapsed:.0f}s): {e}")
|
||||
|
||||
has_retry = self.max_login_retries <= 0 or attempt < self.max_login_retries
|
||||
has_time = self.max_total_time <= 0 or elapsed < self.max_total_time
|
||||
if has_retry and has_time:
|
||||
self._sleep_interruptible(1)
|
||||
continue
|
||||
return LoginResult(success=False, message=str(e))
|
||||
|
||||
def _check_certification_status(self) -> LoginResult:
|
||||
"""请求个人中心接口并判断实名状态。"""
|
||||
headers = {
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,fr;q=0.8,de;q=0.7,en;q=0.6',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Priority': 'u=1, i',
|
||||
'Referer': self.CP_REFERER,
|
||||
'Sec-CH-UA': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
|
||||
'Sec-CH-UA-Mobile': '?0',
|
||||
'Sec-CH-UA-Platform': '"macOS"',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Origin': None,
|
||||
'Content-Type': None,
|
||||
}
|
||||
payload = self._request_json(
|
||||
'get',
|
||||
self.CP_RPC_API,
|
||||
'账号认证状态接口',
|
||||
headers=headers,
|
||||
timeout=(5, 10),
|
||||
)
|
||||
info = payload.get('info') or {}
|
||||
ident_status = str(info.get('ident_status', ''))
|
||||
ident_type = str(info.get('ident_type', ''))
|
||||
|
||||
if ident_status == '0' and ident_type == '0':
|
||||
logger.success("账号检测完成: 账号未认证")
|
||||
return LoginResult(success=True, message="账号未认证", code="account_unverified")
|
||||
|
||||
if ident_status == '2' and ident_type == '2':
|
||||
logger.success("账号检测完成: 账号已认证")
|
||||
return LoginResult(success=True, message="账号已认证", code="account_verified")
|
||||
|
||||
message = f"账号认证状态未知: ident_status={ident_status or '-'}, ident_type={ident_type or '-'}"
|
||||
logger.warning(message)
|
||||
return LoginResult(success=True, message=message, code="account_auth_unknown")
|
||||
|
||||
def _prepare_retry(self) -> bool:
|
||||
"""重试前准备:取新代理、重置 session cookies。
|
||||
|
||||
@@ -389,9 +532,9 @@ class DouyuLogin:
|
||||
|
||||
if payload.get('error') != 81:
|
||||
error_msg = payload.get('msg', '未知错误')
|
||||
# 密码错误等凭据问题不应重试
|
||||
if any(kw in error_msg for kw in ['密码错误', '账号或密码', '账号或者密码', '账号不存在']):
|
||||
raise CredentialError(f"第一次登录失败: {error_msg}")
|
||||
credential_error = self._credential_error_from_payload("第一次登录", payload)
|
||||
if credential_error:
|
||||
raise credential_error
|
||||
raise ValueError(f"第一次登录失败: {error_msg}")
|
||||
|
||||
# 提取极验参数
|
||||
@@ -534,9 +677,9 @@ class DouyuLogin:
|
||||
|
||||
if payload.get('error') != 130014:
|
||||
error_msg = payload.get('msg', '未知错误')
|
||||
# 密码错误等凭据问题不应重试
|
||||
if any(kw in error_msg for kw in ['密码错误', '账号或密码', '账号或者密码', '账号不存在']):
|
||||
raise CredentialError(f"第二次登录失败: {error_msg}")
|
||||
credential_error = self._credential_error_from_payload("第二次登录", payload)
|
||||
if credential_error:
|
||||
raise credential_error
|
||||
raise ValueError(f"第二次登录失败: {error_msg}")
|
||||
|
||||
# 提取remote_code
|
||||
|
||||
Reference in New Issue
Block a user