|
|
"""Flow API Client for VideoFX (Veo)""" |
|
|
import time |
|
|
import uuid |
|
|
import random |
|
|
import base64 |
|
|
from typing import Dict, Any, Optional, List |
|
|
from curl_cffi.requests import AsyncSession |
|
|
from ..core.logger import debug_logger |
|
|
from ..core.config import config |
|
|
|
|
|
|
|
|
class FlowClient: |
|
|
"""VideoFX API客户端""" |
|
|
|
|
|
def __init__(self, proxy_manager): |
|
|
self.proxy_manager = proxy_manager |
|
|
self.labs_base_url = config.flow_labs_base_url |
|
|
self.api_base_url = config.flow_api_base_url |
|
|
self.timeout = config.flow_timeout |
|
|
|
|
|
async def _make_request( |
|
|
self, |
|
|
method: str, |
|
|
url: str, |
|
|
headers: Optional[Dict] = None, |
|
|
json_data: Optional[Dict] = None, |
|
|
use_st: bool = False, |
|
|
st_token: Optional[str] = None, |
|
|
use_at: bool = False, |
|
|
at_token: Optional[str] = None |
|
|
) -> Dict[str, Any]: |
|
|
"""统一HTTP请求处理 |
|
|
|
|
|
Args: |
|
|
method: HTTP方法 (GET/POST) |
|
|
url: 完整URL |
|
|
headers: 请求头 |
|
|
json_data: JSON请求体 |
|
|
use_st: 是否使用ST认证 (Cookie方式) |
|
|
st_token: Session Token |
|
|
use_at: 是否使用AT认证 (Bearer方式) |
|
|
at_token: Access Token |
|
|
""" |
|
|
proxy_url = await self.proxy_manager.get_proxy_url() |
|
|
|
|
|
if headers is None: |
|
|
headers = {} |
|
|
|
|
|
|
|
|
if use_st and st_token: |
|
|
headers["Cookie"] = f"__Secure-next-auth.session-token={st_token}" |
|
|
|
|
|
|
|
|
if use_at and at_token: |
|
|
headers["Authorization"] = f"Bearer {at_token}" |
|
|
|
|
|
|
|
|
headers.update({ |
|
|
"Content-Type": "application/json", |
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" |
|
|
}) |
|
|
|
|
|
|
|
|
if config.debug_enabled: |
|
|
debug_logger.log_request( |
|
|
method=method, |
|
|
url=url, |
|
|
headers=headers, |
|
|
body=json_data, |
|
|
proxy=proxy_url |
|
|
) |
|
|
|
|
|
start_time = time.time() |
|
|
|
|
|
try: |
|
|
async with AsyncSession() as session: |
|
|
if method.upper() == "GET": |
|
|
response = await session.get( |
|
|
url, |
|
|
headers=headers, |
|
|
proxy=proxy_url, |
|
|
timeout=self.timeout, |
|
|
impersonate="chrome110" |
|
|
) |
|
|
else: |
|
|
response = await session.post( |
|
|
url, |
|
|
headers=headers, |
|
|
json=json_data, |
|
|
proxy=proxy_url, |
|
|
timeout=self.timeout, |
|
|
impersonate="chrome110" |
|
|
) |
|
|
|
|
|
duration_ms = (time.time() - start_time) * 1000 |
|
|
|
|
|
|
|
|
if config.debug_enabled: |
|
|
debug_logger.log_response( |
|
|
status_code=response.status_code, |
|
|
headers=dict(response.headers), |
|
|
body=response.text, |
|
|
duration_ms=duration_ms |
|
|
) |
|
|
|
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
except Exception as e: |
|
|
duration_ms = (time.time() - start_time) * 1000 |
|
|
error_msg = str(e) |
|
|
|
|
|
if config.debug_enabled: |
|
|
debug_logger.log_error( |
|
|
error_message=error_msg, |
|
|
status_code=getattr(e, 'status_code', None), |
|
|
response_text=getattr(e, 'response_text', None) |
|
|
) |
|
|
|
|
|
raise Exception(f"Flow API request failed: {error_msg}") |
|
|
|
|
|
|
|
|
|
|
|
async def st_to_at(self, st: str) -> dict: |
|
|
"""ST转AT |
|
|
|
|
|
Args: |
|
|
st: Session Token |
|
|
|
|
|
Returns: |
|
|
{ |
|
|
"access_token": "AT", |
|
|
"expires": "2025-11-15T04:46:04.000Z", |
|
|
"user": {...} |
|
|
} |
|
|
""" |
|
|
url = f"{self.labs_base_url}/auth/session" |
|
|
result = await self._make_request( |
|
|
method="GET", |
|
|
url=url, |
|
|
use_st=True, |
|
|
st_token=st |
|
|
) |
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
async def create_project(self, st: str, title: str) -> str: |
|
|
"""创建项目,返回project_id |
|
|
|
|
|
Args: |
|
|
st: Session Token |
|
|
title: 项目标题 |
|
|
|
|
|
Returns: |
|
|
project_id (UUID) |
|
|
""" |
|
|
url = f"{self.labs_base_url}/trpc/project.createProject" |
|
|
json_data = { |
|
|
"json": { |
|
|
"projectTitle": title, |
|
|
"toolName": "PINHOLE" |
|
|
} |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_st=True, |
|
|
st_token=st |
|
|
) |
|
|
|
|
|
|
|
|
project_id = result["result"]["data"]["json"]["result"]["projectId"] |
|
|
return project_id |
|
|
|
|
|
async def delete_project(self, st: str, project_id: str): |
|
|
"""删除项目 |
|
|
|
|
|
Args: |
|
|
st: Session Token |
|
|
project_id: 项目ID |
|
|
""" |
|
|
url = f"{self.labs_base_url}/trpc/project.deleteProject" |
|
|
json_data = { |
|
|
"json": { |
|
|
"projectToDeleteId": project_id |
|
|
} |
|
|
} |
|
|
|
|
|
await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_st=True, |
|
|
st_token=st |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
async def get_credits(self, at: str) -> dict: |
|
|
"""查询余额 |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
|
|
|
Returns: |
|
|
{ |
|
|
"credits": 920, |
|
|
"userPaygateTier": "PAYGATE_TIER_ONE" |
|
|
} |
|
|
""" |
|
|
url = f"{self.api_base_url}/credits" |
|
|
result = await self._make_request( |
|
|
method="GET", |
|
|
url=url, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
async def upload_image( |
|
|
self, |
|
|
at: str, |
|
|
image_bytes: bytes, |
|
|
aspect_ratio: str = "IMAGE_ASPECT_RATIO_LANDSCAPE" |
|
|
) -> str: |
|
|
"""上传图片,返回mediaGenerationId |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
image_bytes: 图片字节数据 |
|
|
aspect_ratio: 图片或视频宽高比(会自动转换为图片格式) |
|
|
|
|
|
Returns: |
|
|
mediaGenerationId (CAM...) |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
if aspect_ratio.startswith("VIDEO_"): |
|
|
aspect_ratio = aspect_ratio.replace("VIDEO_", "IMAGE_") |
|
|
|
|
|
|
|
|
image_base64 = base64.b64encode(image_bytes).decode('utf-8') |
|
|
|
|
|
url = f"{self.api_base_url}:uploadUserImage" |
|
|
json_data = { |
|
|
"imageInput": { |
|
|
"rawImageBytes": image_base64, |
|
|
"mimeType": "image/jpeg", |
|
|
"isUserUploaded": True, |
|
|
"aspectRatio": aspect_ratio |
|
|
}, |
|
|
"clientContext": { |
|
|
"sessionId": self._generate_session_id(), |
|
|
"tool": "ASSET_MANAGER" |
|
|
} |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
|
|
|
media_id = result["mediaGenerationId"]["mediaGenerationId"] |
|
|
return media_id |
|
|
|
|
|
|
|
|
|
|
|
async def generate_image( |
|
|
self, |
|
|
at: str, |
|
|
project_id: str, |
|
|
prompt: str, |
|
|
model_name: str, |
|
|
aspect_ratio: str, |
|
|
image_inputs: Optional[List[Dict]] = None |
|
|
) -> dict: |
|
|
"""生成图片(同步返回) |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
project_id: 项目ID |
|
|
prompt: 提示词 |
|
|
model_name: GEM_PIX, GEM_PIX_2 或 IMAGEN_3_5 |
|
|
aspect_ratio: 图片宽高比 |
|
|
image_inputs: 参考图片列表(图生图时使用) |
|
|
|
|
|
Returns: |
|
|
{ |
|
|
"media": [{ |
|
|
"image": { |
|
|
"generatedImage": { |
|
|
"fifeUrl": "图片URL", |
|
|
... |
|
|
} |
|
|
} |
|
|
}] |
|
|
} |
|
|
""" |
|
|
url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages" |
|
|
|
|
|
|
|
|
recaptcha_token = await self._get_recaptcha_token(project_id) or "" |
|
|
session_id = self._generate_session_id() |
|
|
|
|
|
|
|
|
request_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"projectId": project_id, |
|
|
"sessionId": session_id, |
|
|
"tool": "PINHOLE" |
|
|
}, |
|
|
"seed": random.randint(1, 99999), |
|
|
"imageModelName": model_name, |
|
|
"imageAspectRatio": aspect_ratio, |
|
|
"prompt": prompt, |
|
|
"imageInputs": image_inputs or [] |
|
|
} |
|
|
|
|
|
json_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"sessionId": session_id |
|
|
}, |
|
|
"requests": [request_data] |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
async def generate_video_text( |
|
|
self, |
|
|
at: str, |
|
|
project_id: str, |
|
|
prompt: str, |
|
|
model_key: str, |
|
|
aspect_ratio: str, |
|
|
user_paygate_tier: str = "PAYGATE_TIER_ONE" |
|
|
) -> dict: |
|
|
"""文生视频,返回task_id |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
project_id: 项目ID |
|
|
prompt: 提示词 |
|
|
model_key: veo_3_1_t2v_fast 等 |
|
|
aspect_ratio: 视频宽高比 |
|
|
user_paygate_tier: 用户等级 |
|
|
|
|
|
Returns: |
|
|
{ |
|
|
"operations": [{ |
|
|
"operation": {"name": "task_id"}, |
|
|
"sceneId": "uuid", |
|
|
"status": "MEDIA_GENERATION_STATUS_PENDING" |
|
|
}], |
|
|
"remainingCredits": 900 |
|
|
} |
|
|
""" |
|
|
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText" |
|
|
|
|
|
|
|
|
recaptcha_token = await self._get_recaptcha_token(project_id) or "" |
|
|
session_id = self._generate_session_id() |
|
|
scene_id = str(uuid.uuid4()) |
|
|
|
|
|
json_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"sessionId": session_id, |
|
|
"projectId": project_id, |
|
|
"tool": "PINHOLE", |
|
|
"userPaygateTier": user_paygate_tier |
|
|
}, |
|
|
"requests": [{ |
|
|
"aspectRatio": aspect_ratio, |
|
|
"seed": random.randint(1, 99999), |
|
|
"textInput": { |
|
|
"prompt": prompt |
|
|
}, |
|
|
"videoModelKey": model_key, |
|
|
"metadata": { |
|
|
"sceneId": scene_id |
|
|
} |
|
|
}] |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
async def generate_video_reference_images( |
|
|
self, |
|
|
at: str, |
|
|
project_id: str, |
|
|
prompt: str, |
|
|
model_key: str, |
|
|
aspect_ratio: str, |
|
|
reference_images: List[Dict], |
|
|
user_paygate_tier: str = "PAYGATE_TIER_ONE" |
|
|
) -> dict: |
|
|
"""图生视频,返回task_id |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
project_id: 项目ID |
|
|
prompt: 提示词 |
|
|
model_key: veo_3_0_r2v_fast |
|
|
aspect_ratio: 视频宽高比 |
|
|
reference_images: 参考图片列表 [{"imageUsageType": "IMAGE_USAGE_TYPE_ASSET", "mediaId": "..."}] |
|
|
user_paygate_tier: 用户等级 |
|
|
|
|
|
Returns: |
|
|
同 generate_video_text |
|
|
""" |
|
|
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages" |
|
|
|
|
|
|
|
|
recaptcha_token = await self._get_recaptcha_token(project_id) or "" |
|
|
session_id = self._generate_session_id() |
|
|
scene_id = str(uuid.uuid4()) |
|
|
|
|
|
json_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"sessionId": session_id, |
|
|
"projectId": project_id, |
|
|
"tool": "PINHOLE", |
|
|
"userPaygateTier": user_paygate_tier |
|
|
}, |
|
|
"requests": [{ |
|
|
"aspectRatio": aspect_ratio, |
|
|
"seed": random.randint(1, 99999), |
|
|
"textInput": { |
|
|
"prompt": prompt |
|
|
}, |
|
|
"videoModelKey": model_key, |
|
|
"referenceImages": reference_images, |
|
|
"metadata": { |
|
|
"sceneId": scene_id |
|
|
} |
|
|
}] |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
async def generate_video_start_end( |
|
|
self, |
|
|
at: str, |
|
|
project_id: str, |
|
|
prompt: str, |
|
|
model_key: str, |
|
|
aspect_ratio: str, |
|
|
start_media_id: str, |
|
|
end_media_id: str, |
|
|
user_paygate_tier: str = "PAYGATE_TIER_ONE" |
|
|
) -> dict: |
|
|
"""收尾帧生成视频,返回task_id |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
project_id: 项目ID |
|
|
prompt: 提示词 |
|
|
model_key: veo_3_1_i2v_s_fast_fl |
|
|
aspect_ratio: 视频宽高比 |
|
|
start_media_id: 起始帧mediaId |
|
|
end_media_id: 结束帧mediaId |
|
|
user_paygate_tier: 用户等级 |
|
|
|
|
|
Returns: |
|
|
同 generate_video_text |
|
|
""" |
|
|
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage" |
|
|
|
|
|
|
|
|
recaptcha_token = await self._get_recaptcha_token(project_id) or "" |
|
|
session_id = self._generate_session_id() |
|
|
scene_id = str(uuid.uuid4()) |
|
|
|
|
|
json_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"sessionId": session_id, |
|
|
"projectId": project_id, |
|
|
"tool": "PINHOLE", |
|
|
"userPaygateTier": user_paygate_tier |
|
|
}, |
|
|
"requests": [{ |
|
|
"aspectRatio": aspect_ratio, |
|
|
"seed": random.randint(1, 99999), |
|
|
"textInput": { |
|
|
"prompt": prompt |
|
|
}, |
|
|
"videoModelKey": model_key, |
|
|
"startImage": { |
|
|
"mediaId": start_media_id |
|
|
}, |
|
|
"endImage": { |
|
|
"mediaId": end_media_id |
|
|
}, |
|
|
"metadata": { |
|
|
"sceneId": scene_id |
|
|
} |
|
|
}] |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
async def generate_video_start_image( |
|
|
self, |
|
|
at: str, |
|
|
project_id: str, |
|
|
prompt: str, |
|
|
model_key: str, |
|
|
aspect_ratio: str, |
|
|
start_media_id: str, |
|
|
user_paygate_tier: str = "PAYGATE_TIER_ONE" |
|
|
) -> dict: |
|
|
"""仅首帧生成视频,返回task_id |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
project_id: 项目ID |
|
|
prompt: 提示词 |
|
|
model_key: veo_3_1_i2v_s_fast_fl等 |
|
|
aspect_ratio: 视频宽高比 |
|
|
start_media_id: 起始帧mediaId |
|
|
user_paygate_tier: 用户等级 |
|
|
|
|
|
Returns: |
|
|
同 generate_video_text |
|
|
""" |
|
|
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage" |
|
|
|
|
|
|
|
|
recaptcha_token = await self._get_recaptcha_token(project_id) or "" |
|
|
session_id = self._generate_session_id() |
|
|
scene_id = str(uuid.uuid4()) |
|
|
|
|
|
json_data = { |
|
|
"clientContext": { |
|
|
"recaptchaToken": recaptcha_token, |
|
|
"sessionId": session_id, |
|
|
"projectId": project_id, |
|
|
"tool": "PINHOLE", |
|
|
"userPaygateTier": user_paygate_tier |
|
|
}, |
|
|
"requests": [{ |
|
|
"aspectRatio": aspect_ratio, |
|
|
"seed": random.randint(1, 99999), |
|
|
"textInput": { |
|
|
"prompt": prompt |
|
|
}, |
|
|
"videoModelKey": model_key, |
|
|
"startImage": { |
|
|
"mediaId": start_media_id |
|
|
}, |
|
|
|
|
|
"metadata": { |
|
|
"sceneId": scene_id |
|
|
} |
|
|
}] |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
async def check_video_status(self, at: str, operations: List[Dict]) -> dict: |
|
|
"""查询视频生成状态 |
|
|
|
|
|
Args: |
|
|
at: Access Token |
|
|
operations: 操作列表 [{"operation": {"name": "task_id"}, "sceneId": "...", "status": "..."}] |
|
|
|
|
|
Returns: |
|
|
{ |
|
|
"operations": [{ |
|
|
"operation": { |
|
|
"name": "task_id", |
|
|
"metadata": {...} # 完成时包含视频信息 |
|
|
}, |
|
|
"status": "MEDIA_GENERATION_STATUS_SUCCESSFUL" |
|
|
}] |
|
|
} |
|
|
""" |
|
|
url = f"{self.api_base_url}/video:batchCheckAsyncVideoGenerationStatus" |
|
|
|
|
|
json_data = { |
|
|
"operations": operations |
|
|
} |
|
|
|
|
|
result = await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_at=True, |
|
|
at_token=at |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
async def delete_media(self, st: str, media_names: List[str]): |
|
|
"""删除媒体 |
|
|
|
|
|
Args: |
|
|
st: Session Token |
|
|
media_names: 媒体ID列表 |
|
|
""" |
|
|
url = f"{self.labs_base_url}/trpc/media.deleteMedia" |
|
|
json_data = { |
|
|
"json": { |
|
|
"names": media_names |
|
|
} |
|
|
} |
|
|
|
|
|
await self._make_request( |
|
|
method="POST", |
|
|
url=url, |
|
|
json_data=json_data, |
|
|
use_st=True, |
|
|
st_token=st |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def _generate_session_id(self) -> str: |
|
|
"""生成sessionId: ;timestamp""" |
|
|
return f";{int(time.time() * 1000)}" |
|
|
|
|
|
def _generate_scene_id(self) -> str: |
|
|
"""生成sceneId: UUID""" |
|
|
return str(uuid.uuid4()) |
|
|
|
|
|
async def _get_recaptcha_token(self, project_id: str) -> Optional[str]: |
|
|
"""获取reCAPTCHA token - 支持两种方式""" |
|
|
captcha_method = config.captcha_method |
|
|
|
|
|
|
|
|
if captcha_method == "personal": |
|
|
try: |
|
|
from .browser_captcha_personal import BrowserCaptchaService |
|
|
service = await BrowserCaptchaService.get_instance(self.proxy_manager) |
|
|
return await service.get_token(project_id) |
|
|
except Exception as e: |
|
|
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}") |
|
|
return None |
|
|
|
|
|
elif captcha_method == "browser": |
|
|
try: |
|
|
from .browser_captcha import BrowserCaptchaService |
|
|
service = await BrowserCaptchaService.get_instance(self.proxy_manager) |
|
|
return await service.get_token(project_id) |
|
|
except Exception as e: |
|
|
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}") |
|
|
return None |
|
|
else: |
|
|
|
|
|
client_key = config.yescaptcha_api_key |
|
|
if not client_key: |
|
|
debug_logger.log_info("[reCAPTCHA] API key not configured, skipping") |
|
|
return None |
|
|
|
|
|
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV" |
|
|
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}" |
|
|
base_url = config.yescaptcha_base_url |
|
|
page_action = "FLOW_GENERATION" |
|
|
|
|
|
try: |
|
|
async with AsyncSession() as session: |
|
|
create_url = f"{base_url}/createTask" |
|
|
create_data = { |
|
|
"clientKey": client_key, |
|
|
"task": { |
|
|
"websiteURL": website_url, |
|
|
"websiteKey": website_key, |
|
|
"type": "RecaptchaV3TaskProxylessM1", |
|
|
"pageAction": page_action |
|
|
} |
|
|
} |
|
|
|
|
|
result = await session.post(create_url, json=create_data, impersonate="chrome110") |
|
|
result_json = result.json() |
|
|
task_id = result_json.get('taskId') |
|
|
|
|
|
debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}") |
|
|
|
|
|
if not task_id: |
|
|
return None |
|
|
|
|
|
get_url = f"{base_url}/getTaskResult" |
|
|
for i in range(40): |
|
|
get_data = { |
|
|
"clientKey": client_key, |
|
|
"taskId": task_id |
|
|
} |
|
|
result = await session.post(get_url, json=get_data, impersonate="chrome110") |
|
|
result_json = result.json() |
|
|
|
|
|
debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}") |
|
|
|
|
|
solution = result_json.get('solution', {}) |
|
|
response = solution.get('gRecaptchaResponse') |
|
|
|
|
|
if response: |
|
|
return response |
|
|
|
|
|
time.sleep(3) |
|
|
|
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}") |
|
|
return None |
|
|
|