| 1 | # -*- coding: utf-8 -*- |
| 2 | """虎扑视频上传 + 手动登录保存 cookie。 |
| 3 | |
| 4 | 功能: |
| 5 | - hupu_cookie_gen: 打开浏览器让用户手动登录(QQ/手机号等),保存 storage_state |
| 6 | - cookie_auth: 验证 cookie 是否有效 |
| 7 | - hupu_setup: 统一入口(检查/触发登录) |
| 8 | - HuPuVideo: 视频上传类 |
| 9 | |
| 10 | 基于 playwright codegen 录制脚本改写。 |
| 11 | 发布页:https://bbs.hupu.com/newpost?tabkey=2(视频发布标签页) |
| 12 | 专区:固定选择「步行街 → 步行街主干道」 |
| 13 | |
| 14 | 注意:虎扑有 headless 检测,需配合反检测参数才能正常操作。 |
| 15 | """ |
| 16 | from __future__ import annotations |
| 17 | |
| 18 | import asyncio |
| 19 | import inspect |
| 20 | import os |
| 21 | import re |
| 22 | import time |
| 23 | from pathlib import Path |
| 24 | |
| 25 | from playwright.async_api import BrowserContext, Page, Playwright, async_playwright |
| 26 | |
| 27 | from conf import BASE_DIR, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH |
| 28 | from uploader.base_video import BaseVideoUploader |
| 29 | from utils.log import hupu_logger |
| 30 | |
| 31 | |
| 32 | HUPU_HOME_URL = "https://www.hupu.com/" |
| 33 | HUPU_LOGIN_URL = "https://passport.hupu.com/v2/login?pcPhone=1&jumpurl=https://www.hupu.com&from=https://www.hupu.com" |
| 34 | HUPU_PUBLISH_URL = "https://bbs.hupu.com/newpost?tabkey=2" |
| 35 | |
| 36 | # 虎扑发布成功后跳转的帖子 URL 模式 |
| 37 | HUPU_POST_URL_PATTERN = re.compile(r"bbs\.hupu\.com/\d+\.html") |
| 38 | |
| 39 | # 反检测 UA |
| 40 | _CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" |
| 41 | |
| 42 | # 反 webdriver 检测脚本 |
| 43 | _STEALTH_SCRIPT = """ |
| 44 | Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); |
| 45 | """ |
| 46 | |
| 47 | |
| 48 | def _msg(emoji: str, text: str) -> str: |
| 49 | return f"{emoji} {text}" |
| 50 | |
| 51 | |
| 52 | def _build_login_result(success: bool, status: str, message: str, account_file: str, current_url: str = "") -> dict: |
| 53 | return { |
| 54 | "success": success, |
| 55 | "status": status, |
| 56 | "message": message, |
| 57 | "account_file": str(account_file), |
| 58 | "current_url": current_url, |
| 59 | } |
| 60 | |
| 61 | |
| 62 | async def _emit_qrcode_callback(qrcode_callback, payload: dict): |
| 63 | if not qrcode_callback: |
| 64 | return |
| 65 | callback_result = qrcode_callback(payload) |
| 66 | if inspect.isawaitable(callback_result): |
| 67 | await callback_result |
| 68 | |
| 69 | |
| 70 | def _build_launch_kwargs(headless: bool) -> dict: |
| 71 | launch_kwargs = { |
| 72 | "headless": headless, |
| 73 | "args": ["--disable-blink-features=AutomationControlled"], |
| 74 | } |
| 75 | if LOCAL_CHROME_PATH: |
| 76 | launch_kwargs["executable_path"] = LOCAL_CHROME_PATH |
| 77 | return launch_kwargs |
| 78 | |
| 79 | |
| 80 | def _resolve_account_file(account_file: str | Path) -> str: |
| 81 | path = Path(account_file).expanduser() |
| 82 | if path.is_absolute(): |
| 83 | return str(path) |
| 84 | if len(path.parts) == 1: |
| 85 | return str((Path(BASE_DIR) / "cookies" / path).resolve()) |
| 86 | return str(path.resolve()) |
| 87 | |
| 88 | |
| 89 | async def _create_stealth_context(browser, account_file: str | None = None) -> BrowserContext: |
| 90 | """创建带反检测的 context。""" |
| 91 | kwargs = { |
| 92 | "user_agent": _CHROME_UA, |
| 93 | "viewport": {"width": 1920, "height": 1080}, |
| 94 | } |
| 95 | if account_file and os.path.exists(account_file): |
| 96 | kwargs["storage_state"] = account_file |
| 97 | context = await browser.new_context(**kwargs) |
| 98 | return context |
| 99 | |
| 100 | |
| 101 | async def _new_stealth_page(context: BrowserContext) -> Page: |
| 102 | """创建带反检测 init script 的 page。""" |
| 103 | page = await context.new_page() |
| 104 | await page.add_init_script(_STEALTH_SCRIPT) |
| 105 | return page |
| 106 | |
| 107 | |
| 108 | async def hupu_cookie_gen(account_file, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 120, headless: bool = False): |
| 109 | """QQ 扫码登录虎扑,保存 cookie。 |
| 110 | |
| 111 | 流程:打开虎扑登录页 → 点击 QQ 登录 → 截取 QQ 二维码 → 等待扫码完成 → 保存 storage_state。 |
| 112 | 支持 headless 模式(终端显示二维码)。 |
| 113 | """ |
| 114 | account_file = _resolve_account_file(account_file) |
| 115 | Path(account_file).parent.mkdir(parents=True, exist_ok=True) |
| 116 | result = _build_login_result(False, "failed", "虎扑登录失败", account_file) |
| 117 | |
| 118 | async with async_playwright() as playwright: |
| 119 | browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=headless)) |
| 120 | context = await _create_stealth_context(browser) |
| 121 | try: |
| 122 | page = await _new_stealth_page(context) |
| 123 | await page.goto(HUPU_LOGIN_URL, timeout=60000, wait_until="load") |
| 124 | await page.wait_for_timeout(3000) |
| 125 | |
| 126 | # 点击 QQ 登录按钮 |
| 127 | qq_btn = page.get_by_role("button", name="qq QQ登录") |
| 128 | if not await qq_btn.count(): |
| 129 | hupu_logger.error(_msg("😢", "未找到 QQ 登录按钮")) |
| 130 | result = _build_login_result(False, "failed", "未找到 QQ 登录按钮", account_file, page.url) |
| 131 | return result |
| 132 | |
| 133 | await qq_btn.click() |
| 134 | await page.wait_for_timeout(5000) |
| 135 | hupu_logger.info(_msg("🏃", "已跳转到 QQ 登录页")) |
| 136 | |
| 137 | # 从 QQ iframe 中获取二维码 |
| 138 | qrcode_info = await _grab_qq_qrcode(page, context, account_file) |
| 139 | |
| 140 | if qrcode_info: |
| 141 | await _emit_qrcode_callback(qrcode_callback, qrcode_info) |
| 142 | hupu_logger.info(_msg("🧍", "请用 QQ 手机版扫码登录")) |
| 143 | else: |
| 144 | hupu_logger.warning(_msg("⚠️", "未能获取 QQ 二维码,请在浏览器中手动扫码")) |
| 145 | |
| 146 | # 轮询等待登录完成 |
| 147 | for _ in range(max_checks): |
| 148 | current_url = page.url |
| 149 | # QQ 授权成功后会跳回虎扑首页 |
| 150 | if "www.hupu.com" in current_url and "passport" not in current_url and "graph.qq.com" not in current_url: |
| 151 | hupu_logger.info(_msg("🥳", f"登录成功,跳转到: {current_url}")) |
| 152 | result = _build_login_result(True, "success", "虎扑 QQ 扫码登录成功", account_file, current_url) |
| 153 | break |
| 154 | # 检查是否实际跳转到虎扑 passport 回调页(不是 redirect_uri 参数中包含) |
| 155 | if current_url.startswith("https://passport.hupu.com/pc/qqcallback"): |
| 156 | await page.wait_for_timeout(5000) |
| 157 | current_url = page.url |
| 158 | hupu_logger.info(_msg("🥳", f"QQ 回调成功,当前: {current_url}")) |
| 159 | result = _build_login_result(True, "success", "虎扑 QQ 扫码登录成功", account_file, current_url) |
| 160 | break |
| 161 | # 检查 cookies 中是否出现 u(核心登录态) |
| 162 | cookies = await context.cookies() |
| 163 | if any(c.get("name") == "u" and c.get("value") for c in cookies): |
| 164 | hupu_logger.info(_msg("🥳", f"登录成功(检测到 u cookie),当前: {current_url}")) |
| 165 | result = _build_login_result(True, "success", "虎扑 QQ 扫码登录成功", account_file, current_url) |
| 166 | break |
| 167 | await page.wait_for_timeout(poll_interval * 1000) |
| 168 | else: |
| 169 | result = _build_login_result(False, "timeout", "等待 QQ 扫码登录超时", account_file, page.url) |
| 170 | |
| 171 | if result["success"]: |
| 172 | await asyncio.sleep(2) |
| 173 | # 确保跳转到首页加载完 cookie |
| 174 | if "www.hupu.com" not in page.url: |
| 175 | await page.goto(HUPU_HOME_URL, timeout=30000, wait_until="domcontentloaded") |
| 176 | await page.wait_for_timeout(3000) |
| 177 | await context.storage_state(path=account_file) |
| 178 | hupu_logger.success(_msg("🥳", f"cookie 已保存: {account_file}")) |
| 179 | except Exception as exc: |
| 180 | result = _build_login_result(False, "failed", str(exc), account_file, current_url=page.url if "page" in locals() else "") |
| 181 | finally: |
| 182 | # 清理临时二维码文件 |
| 183 | qr_path = Path(account_file).parent / f"{Path(account_file).stem}_qq_qrcode.png" |
| 184 | if qr_path.exists(): |
| 185 | qr_path.unlink() |
| 186 | if not result["success"]: |
| 187 | hupu_logger.error(_msg("😢", f"登录失败: {result['message']}")) |
| 188 | await context.close() |
| 189 | await browser.close() |
| 190 | return result |
| 191 | |
| 192 | |
| 193 | async def _grab_qq_qrcode(page: Page, context: BrowserContext, account_file: str) -> dict | None: |
| 194 | """从 QQ 登录 iframe 中获取二维码。""" |
| 195 | from utils.login_qrcode import decode_qrcode_from_path, print_terminal_qrcode |
| 196 | |
| 197 | # 等待 QQ iframe 加载(最多 15 秒) |
| 198 | qq_frame = None |
| 199 | for _ in range(5): |
| 200 | for frame in page.frames: |
| 201 | if "xui.ptlogin2.qq.com" in frame.url or "ptlogin2.qq.com" in frame.url: |
| 202 | qq_frame = frame |
| 203 | break |
| 204 | if qq_frame: |
| 205 | break |
| 206 | await asyncio.sleep(3) |
| 207 | |
| 208 | if not qq_frame: |
| 209 | return None |
| 210 | |
| 211 | await asyncio.sleep(3) |
| 212 | |
| 213 | # 获取二维码图片(id="qrlogin_img") |
| 214 | qr_selectors = ["#qrlogin_img", 'img[src*="ptqrshow"]', 'img[id*="qr"]'] |
| 215 | for sel in qr_selectors: |
| 216 | qr_loc = qq_frame.locator(sel).first |
| 217 | if await qr_loc.count(): |
| 218 | src = await qr_loc.get_attribute("src") |
| 219 | if src and src.startswith("http"): |
| 220 | # 下载二维码图片 |
| 221 | try: |
| 222 | resp = await context.request.get(src) |
| 223 | qr_path = Path(account_file).parent / f"{Path(account_file).stem}_qq_qrcode.png" |
| 224 | qr_path.parent.mkdir(parents=True, exist_ok=True) |
| 225 | qr_path.write_bytes(await resp.body()) |
| 226 | |
| 227 | # 尝试解码并在终端显示 |
| 228 | qrcode_content = decode_qrcode_from_path(qr_path) |
| 229 | if qrcode_content: |
| 230 | print_terminal_qrcode(qrcode_content, qr_path, "QQ手机版") |
| 231 | else: |
| 232 | hupu_logger.warning(_msg("😵", f"终端无法显示二维码,请打开 {qr_path} 扫码")) |
| 233 | |
| 234 | return {"image_path": str(qr_path), "image_data_url": ""} |
| 235 | except Exception as exc: |
| 236 | hupu_logger.warning(_msg("⚠️", f"下载 QQ 二维码失败: {exc}")) |
| 237 | continue |
| 238 | |
| 239 | return None |
| 240 | |
| 241 | |
| 242 | async def cookie_auth(account_file): |
| 243 | """验证虎扑 cookie 是否有效。访问发布页,检测是否能正常加载。""" |
| 244 | account_file = _resolve_account_file(account_file) |
| 245 | async with async_playwright() as playwright: |
| 246 | browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=True)) |
| 247 | try: |
| 248 | context = await _create_stealth_context(browser, account_file) |
| 249 | page = await _new_stealth_page(context) |
| 250 | await page.goto(HUPU_PUBLISH_URL, timeout=60000, wait_until="domcontentloaded") |
| 251 | await page.wait_for_timeout(5000) |
| 252 | |
| 253 | # 检查是否被跳转到登录页 |
| 254 | if "passport" in page.url or "login" in page.url: |
| 255 | hupu_logger.info(_msg("🥹", "cookie 已失效(跳转到登录页)")) |
| 256 | return False |
| 257 | |
| 258 | # 检查发布页是否出现「上传视频」按钮(登录后才有) |
| 259 | upload_btn = page.get_by_role("button", name="上传视频") |
| 260 | if await upload_btn.count(): |
| 261 | hupu_logger.success(_msg("🥳", "cookie 有效")) |
| 262 | return True |
| 263 | |
| 264 | # 兜底:检查 cookie 中是否有 u 字段 |
| 265 | cookies = await context.cookies() |
| 266 | if any(c.get("name") == "u" and c.get("value") for c in cookies): |
| 267 | hupu_logger.success(_msg("🥳", "cookie 有效(u cookie 存在)")) |
| 268 | return True |
| 269 | |
| 270 | hupu_logger.info(_msg("🥹", "cookie 已失效(未检测到登录态)")) |
| 271 | return False |
| 272 | except Exception as exc: |
| 273 | hupu_logger.warning(_msg("😵", f"cookie 校验出错,按失效处理: {exc}")) |
| 274 | return False |
| 275 | finally: |
| 276 | await browser.close() |
| 277 | |
| 278 | |
| 279 | async def hupu_setup(account_file, handle=False, return_detail=False, qrcode_callback=None, headless: bool = False): |
| 280 | """统一入口:检查 cookie → 如无效且 handle=True 则触发手动登录。""" |
| 281 | account_file = _resolve_account_file(account_file) |
| 282 | if not os.path.exists(account_file) or not await cookie_auth(account_file): |
| 283 | if not handle: |
| 284 | result = _build_login_result(False, "cookie_invalid", "cookie 文件不存在或已失效", account_file) |
| 285 | return result if return_detail else False |
| 286 | hupu_logger.info(_msg("🥹", "cookie 文件不存在或已失效,打开浏览器请手动登录")) |
| 287 | result = await hupu_cookie_gen(account_file, qrcode_callback=qrcode_callback, headless=headless) |
| 288 | return result if return_detail else result["success"] |
| 289 | |
| 290 | result = _build_login_result(True, "cookie_valid", "cookie 有效", account_file) |
| 291 | return result if return_detail else True |
| 292 | |
| 293 | |
| 294 | class HuPuVideo(BaseVideoUploader): |
| 295 | """虎扑视频上传。 |
| 296 | |
| 297 | 流程:直接跳转发布页 → 上传视频文件 → 填标题 → 填简介 → |
| 298 | 上传封面 → 选专区(步行街主干道)→ 选原创/二创 → 选 AI 声明 → |
| 299 | 点击「确定发布」→ 等待跳转到帖子页面。 |
| 300 | """ |
| 301 | |
| 302 | def __init__( |
| 303 | self, |
| 304 | title, |
| 305 | file_path, |
| 306 | tags, |
| 307 | account_file, |
| 308 | publish_date=0, |
| 309 | desc: str | None = None, |
| 310 | thumbnail_path: str | None = None, |
| 311 | debug: bool = True, |
| 312 | headless: bool = LOCAL_CHROME_HEADLESS, |
| 313 | ): |
| 314 | self.title = title |
| 315 | self.file_path = file_path |
| 316 | self.tags = tags or [] |
| 317 | self.account_file = _resolve_account_file(account_file) |
| 318 | self.publish_date = publish_date |
| 319 | self.desc = desc or "" |
| 320 | self.thumbnail_path = thumbnail_path |
| 321 | self.debug = debug |
| 322 | self.headless = headless |
| 323 | self.local_executable_path = LOCAL_CHROME_PATH |
| 324 | self.max_title_length = 40 |
| 325 | self.min_title_length = 4 |
| 326 | |
| 327 | async def validate_upload_args(self): |
| 328 | if not os.path.exists(self.account_file): |
| 329 | raise RuntimeError(f"cookie文件不存在,请先完成虎扑登录: {self.account_file}") |
| 330 | if not await cookie_auth(self.account_file): |
| 331 | raise RuntimeError(f"cookie文件已失效,请先完成虎扑登录: {self.account_file}") |
| 332 | if not self.title or not str(self.title).strip(): |
| 333 | raise ValueError("视频标题不能为空") |
| 334 | if len(self.title) < self.min_title_length: |
| 335 | raise ValueError(f"视频标题至少{self.min_title_length}个字") |
| 336 | self.file_path = str(self.validate_video_file(self.file_path)) |
| 337 | if self.thumbnail_path: |
| 338 | self.thumbnail_path = str(self.validate_image_file(self.thumbnail_path)) |
| 339 | |
| 340 | async def upload(self, playwright: Playwright) -> None: |
| 341 | hupu_logger.info(_msg("🧍", "先检查 cookie 和视频文件")) |
| 342 | await self.validate_upload_args() |
| 343 | hupu_logger.info(_msg("🥳", "上传前检查通过")) |
| 344 | |
| 345 | browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=self.headless)) |
| 346 | context = await _create_stealth_context(browser, self.account_file) |
| 347 | |
| 348 | try: |
| 349 | page = await _new_stealth_page(context) |
| 350 | # 直接跳转到视频发布页(绕过首页点击) |
| 351 | await page.goto(HUPU_PUBLISH_URL, timeout=60000, wait_until="load") |
| 352 | await page.wait_for_timeout(5000) |
| 353 | hupu_logger.info(_msg("🏃", f"开始上传视频: {self.title}")) |
| 354 | |
| 355 | # 1) 上传视频文件 |
| 356 | await self._upload_video_file(page) |
| 357 | |
| 358 | # 2) 填写标题 |
| 359 | await self._fill_title(page) |
| 360 | |
| 361 | # 3) 填写简介 |
| 362 | await self._fill_description(page) |
| 363 | |
| 364 | # 4) 上传封面(如有) |
| 365 | if self.thumbnail_path: |
| 366 | await self._upload_thumbnail(page) |
| 367 | |
| 368 | # 5) 选择专区(步行街 → 步行街主干道) |
| 369 | await self._select_zone(page) |
| 370 | |
| 371 | # 6) 选择原创/二创 + AI 声明 |
| 372 | await self._check_declarations(page) |
| 373 | |
| 374 | # 7) 点击发布 |
| 375 | await self._submit_publish(page) |
| 376 | |
| 377 | # 保存 cookie |
| 378 | await context.storage_state(path=self.account_file) |
| 379 | hupu_logger.success(_msg("🥳", "cookie 更新完毕")) |
| 380 | finally: |
| 381 | await context.close() |
| 382 | await browser.close() |
| 383 | |
| 384 | async def _upload_video_file(self, page: Page) -> None: |
| 385 | """点击「上传视频」按钮并设置文件。""" |
| 386 | upload_btn = page.get_by_role("button", name="上传视频") |
| 387 | await upload_btn.wait_for(state="visible", timeout=15000) |
| 388 | |
| 389 | # 通过 file chooser 设置文件 |
| 390 | async with page.expect_file_chooser(timeout=10000) as fc_info: |
| 391 | await upload_btn.click() |
| 392 | file_chooser = await fc_info.value |
| 393 | await file_chooser.set_files(self.file_path) |
| 394 | hupu_logger.info(_msg("🏃", f"已选择视频文件: {self.file_path}")) |
| 395 | |
| 396 | # 等待视频上传就绪(标题输入框出现即可填写) |
| 397 | title_field = page.get_by_placeholder("请输入标题(最少4个字,最多40个字)") |
| 398 | await title_field.wait_for(state="visible", timeout=300000) |
| 399 | hupu_logger.info(_msg("🥳", "视频已就绪")) |
| 400 | |
| 401 | async def _fill_title(self, page: Page) -> None: |
| 402 | """填写标题(4-40字)。""" |
| 403 | title_field = page.get_by_placeholder("请输入标题(最少4个字,最多40个字)") |
| 404 | await title_field.wait_for(state="visible", timeout=15000) |
| 405 | title = self.title[:self.max_title_length] |
| 406 | await title_field.click() |
| 407 | await title_field.fill(title) |
| 408 | hupu_logger.info(_msg("🏷️", f"标题已填写: {title}")) |
| 409 | |
| 410 | async def _fill_description(self, page: Page) -> None: |
| 411 | """填写简介。""" |
| 412 | desc_field = page.get_by_placeholder("请输入简介") |
| 413 | if not await desc_field.count(): |
| 414 | hupu_logger.warning(_msg("⚠️", "未找到简介输入框")) |
| 415 | return |
| 416 | |
| 417 | # 组装描述:正文 + 标签 |
| 418 | content = self.desc |
| 419 | if self.tags: |
| 420 | tag_str = " ".join(f"#{t}#" for t in self.tags) |
| 421 | content = f"{content}\n{tag_str}" if content else tag_str |
| 422 | |
| 423 | if content: |
| 424 | await desc_field.click() |
| 425 | await desc_field.fill(content) |
| 426 | hupu_logger.info(_msg("📝", f"简介已填写({len(content)}字)")) |
| 427 | |
| 428 | async def _upload_thumbnail(self, page: Page) -> None: |
| 429 | """上传封面:点击「更换封面」→ 设置文件。""" |
| 430 | try: |
| 431 | cover_span = page.locator("span").filter(has_text="更换封面") |
| 432 | await cover_span.wait_for(state="visible", timeout=10000) |
| 433 | |
| 434 | # 通过 file chooser 设置封面图片 |
| 435 | async with page.expect_file_chooser(timeout=10000) as fc_info: |
| 436 | await cover_span.click() |
| 437 | file_chooser = await fc_info.value |
| 438 | await file_chooser.set_files(self.thumbnail_path) |
| 439 | hupu_logger.info(_msg("🏃", f"已选择封面图片: {self.thumbnail_path}")) |
| 440 | await page.wait_for_timeout(3000) |
| 441 | hupu_logger.success(_msg("🖼️", "封面已上传")) |
| 442 | except Exception as exc: |
| 443 | hupu_logger.warning(_msg("⚠️", f"封面上传失败: {exc},继续发布(使用默认封面)")) |
| 444 | |
| 445 | async def _select_zone(self, page: Page) -> None: |
| 446 | """选择专区:步行街 → 步行街主干道。""" |
| 447 | try: |
| 448 | # 录制脚本:page.get_by_label("发视频").get_by_text("添加专区").click() |
| 449 | add_zone_btn = page.get_by_label("发视频").get_by_text("添加专区") |
| 450 | await add_zone_btn.wait_for(state="visible", timeout=10000) |
| 451 | await add_zone_btn.click() |
| 452 | await page.wait_for_timeout(1500) |
| 453 | |
| 454 | # 选择「步行街」分类 |
| 455 | zone_dialog = page.get_by_label("添加专区") |
| 456 | step_street = zone_dialog.locator("div").filter(has_text=re.compile(r"^步行街$")) |
| 457 | await step_street.click() |
| 458 | await page.wait_for_timeout(1000) |
| 459 | |
| 460 | # 选择「步行街主干道」子分类 |
| 461 | main_road = page.get_by_text("步行街主干道") |
| 462 | await main_road.click() |
| 463 | await page.wait_for_timeout(500) |
| 464 | |
| 465 | # 点击确定 |
| 466 | confirm_btn = page.get_by_role("button", name="确 定") |
| 467 | await confirm_btn.click() |
| 468 | await page.wait_for_timeout(1000) |
| 469 | hupu_logger.info(_msg("🏷️", "已选择专区:步行街主干道")) |
| 470 | except Exception as exc: |
| 471 | hupu_logger.warning(_msg("⚠️", f"选择专区失败: {exc}")) |
| 472 | |
| 473 | async def _check_declarations(self, page: Page) -> None: |
| 474 | """选择原创/二创声明 + AI 声明。""" |
| 475 | try: |
| 476 | # 1) 点击「原创/二创」按钮 |
| 477 | declaration_btn = page.get_by_role("button", name="原创/二创") |
| 478 | if await declaration_btn.count(): |
| 479 | await declaration_btn.click(timeout=5000) |
| 480 | await page.wait_for_timeout(1000) |
| 481 | hupu_logger.info(_msg("🏷️", "已点击「原创/二创」")) |
| 482 | except Exception as exc: |
| 483 | hupu_logger.warning(_msg("⚠️", f"点击原创/二创失败: {exc}")) |
| 484 | |
| 485 | try: |
| 486 | # 2) 选择「含AI生成内容」 |
| 487 | combobox = page.get_by_role("combobox") |
| 488 | if await combobox.count(): |
| 489 | await combobox.click(timeout=5000) |
| 490 | await page.wait_for_timeout(1000) |
| 491 | |
| 492 | ai_option = page.get_by_text("含AI生成内容") |
| 493 | if await ai_option.count(): |
| 494 | await ai_option.click(timeout=5000) |
| 495 | await page.wait_for_timeout(500) |
| 496 | hupu_logger.info(_msg("🏷️", "已选择「含AI生成内容」")) |
| 497 | except Exception as exc: |
| 498 | hupu_logger.warning(_msg("⚠️", f"选择 AI 声明失败: {exc}")) |
| 499 | |
| 500 | async def _submit_publish(self, page: Page) -> None: |
| 501 | """点击「确定发布」并等待跳转到帖子页面。""" |
| 502 | # 录制脚本:page.get_by_label("发视频").get_by_text("确定发布").click() |
| 503 | publish_btn = page.get_by_label("发视频").get_by_text("确定发布") |
| 504 | await publish_btn.wait_for(state="visible", timeout=15000) |
| 505 | await publish_btn.click() |
| 506 | hupu_logger.info(_msg("🏃", "已点击「确定发布」")) |
| 507 | |
| 508 | # 等待跳转到帖子详情页(URL 匹配 bbs.hupu.com/{数字}.html) |
| 509 | start = time.monotonic() |
| 510 | while time.monotonic() - start < 60: |
| 511 | current_url = page.url |
| 512 | if HUPU_POST_URL_PATTERN.search(current_url): |
| 513 | hupu_logger.success(_msg("🥳", f"视频发布成功: {current_url}")) |
| 514 | return |
| 515 | |
| 516 | await page.wait_for_timeout(2000) |
| 517 | |
| 518 | # 超时 - 可能已成功但检测不到 |
| 519 | hupu_logger.warning(_msg("⚠️", f"发布后 60s 未检测到帖子页面跳转,当前 URL: {page.url}")) |
| 520 | |
| 521 | async def main(self): |
| 522 | async with async_playwright() as playwright: |
| 523 | await self.upload(playwright) |
| 524 |