|
|
@@ -133,7 +133,7 @@ class DouyinPublisher(BasePublisher):
|
|
|
return {'need_captcha': False, 'captcha_type': ''}
|
|
|
|
|
|
async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
|
|
|
- """发布视频到抖音"""
|
|
|
+ """发布视频到抖音 - 参考 matrix/douyin_uploader/main.py"""
|
|
|
print(f"\n{'='*60}")
|
|
|
print(f"[{self.platform_name}] 开始发布视频")
|
|
|
print(f"[{self.platform_name}] 视频路径: {params.video_path}")
|
|
|
@@ -163,114 +163,194 @@ class DouyinPublisher(BasePublisher):
|
|
|
|
|
|
self.report_progress(10, "正在打开上传页面...")
|
|
|
|
|
|
- # 访问上传页面
|
|
|
- await self.page.goto(self.publish_url)
|
|
|
- await self.page.wait_for_url(self.publish_url, timeout=30000)
|
|
|
+ # 访问上传页面 - 参考 matrix
|
|
|
+ await self.page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
|
|
+ print(f"[{self.platform_name}] 等待页面加载...")
|
|
|
|
|
|
- # 等待页面加载,检查验证码
|
|
|
- await asyncio.sleep(2)
|
|
|
+ try:
|
|
|
+ await self.page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=30000)
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ await asyncio.sleep(3)
|
|
|
+
|
|
|
+ # 检查当前 URL 和页面状态
|
|
|
+ current_url = self.page.url
|
|
|
+ print(f"[{self.platform_name}] 当前 URL: {current_url}")
|
|
|
+
|
|
|
+ # 检查是否在登录页面或需要登录
|
|
|
+ if "login" in current_url or "passport" in current_url:
|
|
|
+ screenshot_base64 = await self.capture_screenshot()
|
|
|
+ return PublishResult(
|
|
|
+ success=False,
|
|
|
+ platform=self.platform_name,
|
|
|
+ error="Cookie 已过期,需要重新登录",
|
|
|
+ need_captcha=True,
|
|
|
+ captcha_type='login',
|
|
|
+ screenshot_base64=screenshot_base64,
|
|
|
+ page_url=current_url,
|
|
|
+ status='need_captcha'
|
|
|
+ )
|
|
|
+
|
|
|
+ # 使用 AI 检测验证码
|
|
|
+ ai_captcha_result = await self.ai_check_captcha()
|
|
|
+ if ai_captcha_result['has_captcha']:
|
|
|
+ print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha_result['captcha_type']}", flush=True)
|
|
|
+ screenshot_base64 = await self.capture_screenshot()
|
|
|
+ return PublishResult(
|
|
|
+ success=False,
|
|
|
+ platform=self.platform_name,
|
|
|
+ error=f"检测到{ai_captcha_result['captcha_type']}验证码,需要使用有头浏览器完成验证",
|
|
|
+ need_captcha=True,
|
|
|
+ captcha_type=ai_captcha_result['captcha_type'],
|
|
|
+ screenshot_base64=screenshot_base64,
|
|
|
+ page_url=current_url,
|
|
|
+ status='need_captcha'
|
|
|
+ )
|
|
|
+
|
|
|
+ # 传统方式检测验证码
|
|
|
captcha_result = await self.check_captcha()
|
|
|
if captcha_result['need_captcha']:
|
|
|
- print(f"[{self.platform_name}] 检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
|
|
|
+ print(f"[{self.platform_name}] 传统方式检测到验证码: {captcha_result['captcha_type']}", flush=True)
|
|
|
screenshot_base64 = await self.capture_screenshot()
|
|
|
- page_url = await self.get_page_url()
|
|
|
return PublishResult(
|
|
|
success=False,
|
|
|
platform=self.platform_name,
|
|
|
- error=f"需要{captcha_result['captcha_type']}验证码",
|
|
|
+ error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
|
|
|
need_captcha=True,
|
|
|
captcha_type=captcha_result['captcha_type'],
|
|
|
screenshot_base64=screenshot_base64,
|
|
|
- page_url=page_url,
|
|
|
+ page_url=current_url,
|
|
|
status='need_captcha'
|
|
|
)
|
|
|
|
|
|
self.report_progress(15, "正在选择视频文件...")
|
|
|
|
|
|
- # 点击上传区域
|
|
|
- upload_div = self.page.locator("div[class*='container-drag']").first
|
|
|
- async with self.page.expect_file_chooser() as fc_info:
|
|
|
- await upload_div.click()
|
|
|
- file_chooser = await fc_info.value
|
|
|
- await file_chooser.set_files(params.video_path)
|
|
|
+ # 点击上传区域 - 参考 matrix: div.container-drag-info-Tl0RGH 或带 container-drag 的 div
|
|
|
+ upload_selectors = [
|
|
|
+ "div[class*='container-drag-info']",
|
|
|
+ "div[class*='container-drag']",
|
|
|
+ "div.upload-btn",
|
|
|
+ "div[class*='upload']",
|
|
|
+ ]
|
|
|
+
|
|
|
+ upload_success = False
|
|
|
+ for selector in upload_selectors:
|
|
|
+ try:
|
|
|
+ upload_div = self.page.locator(selector).first
|
|
|
+ if await upload_div.count() > 0:
|
|
|
+ print(f"[{self.platform_name}] 找到上传区域: {selector}")
|
|
|
+ async with self.page.expect_file_chooser(timeout=10000) as fc_info:
|
|
|
+ await upload_div.click()
|
|
|
+ file_chooser = await fc_info.value
|
|
|
+ await file_chooser.set_files(params.video_path)
|
|
|
+ upload_success = True
|
|
|
+ print(f"[{self.platform_name}] 视频文件已选择")
|
|
|
+ break
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[{self.platform_name}] 选择器 {selector} 失败: {e}")
|
|
|
+
|
|
|
+ if not upload_success:
|
|
|
+ screenshot_base64 = await self.capture_screenshot()
|
|
|
+ return PublishResult(
|
|
|
+ success=False,
|
|
|
+ platform=self.platform_name,
|
|
|
+ error="未找到上传入口",
|
|
|
+ screenshot_base64=screenshot_base64,
|
|
|
+ page_url=await self.get_page_url(),
|
|
|
+ status='failed'
|
|
|
+ )
|
|
|
|
|
|
- # 等待跳转到发布页面
|
|
|
+ # 等待跳转到发布页面 - 参考 matrix
|
|
|
self.report_progress(20, "等待进入发布页面...")
|
|
|
- for _ in range(60):
|
|
|
+ for i in range(60):
|
|
|
try:
|
|
|
+ # matrix 等待的 URL: https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page
|
|
|
await self.page.wait_for_url(
|
|
|
"https://creator.douyin.com/creator-micro/content/post/video*",
|
|
|
timeout=2000
|
|
|
)
|
|
|
+ print(f"[{self.platform_name}] 已进入发布页面")
|
|
|
break
|
|
|
except:
|
|
|
+ print(f"[{self.platform_name}] 等待进入发布页面... {i+1}/60")
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
self.report_progress(30, "正在填充标题和话题...")
|
|
|
|
|
|
- # 填写标题
|
|
|
+ # 填写标题 - 参考 matrix
|
|
|
title_input = self.page.get_by_text('作品标题').locator("..").locator(
|
|
|
"xpath=following-sibling::div[1]").locator("input")
|
|
|
if await title_input.count():
|
|
|
await title_input.fill(params.title[:30])
|
|
|
+ print(f"[{self.platform_name}] 标题已填写")
|
|
|
else:
|
|
|
- # 备用方式
|
|
|
+ # 备用方式 - 参考 matrix
|
|
|
title_container = self.page.locator(".notranslate")
|
|
|
await title_container.click()
|
|
|
+ await self.page.keyboard.press("Backspace")
|
|
|
await self.page.keyboard.press("Control+KeyA")
|
|
|
await self.page.keyboard.press("Delete")
|
|
|
await self.page.keyboard.type(params.title)
|
|
|
await self.page.keyboard.press("Enter")
|
|
|
+ print(f"[{self.platform_name}] 标题已填写(备用方式)")
|
|
|
|
|
|
- # 添加话题标签
|
|
|
+ # 添加话题标签 - 参考 matrix
|
|
|
if params.tags:
|
|
|
css_selector = ".zone-container"
|
|
|
- for tag in params.tags:
|
|
|
- print(f"[{self.platform_name}] 添加话题: #{tag}")
|
|
|
+ for index, tag in enumerate(params.tags, start=1):
|
|
|
+ print(f"[{self.platform_name}] 正在添加第{index}个话题: #{tag}")
|
|
|
await self.page.type(css_selector, "#" + tag)
|
|
|
await self.page.press(css_selector, "Space")
|
|
|
|
|
|
self.report_progress(40, "等待视频上传完成...")
|
|
|
|
|
|
- # 等待视频上传完成
|
|
|
- for _ in range(120):
|
|
|
+ # 等待视频上传完成 - 参考 matrix: 检测"重新上传"按钮
|
|
|
+ for i in range(120):
|
|
|
try:
|
|
|
count = await self.page.locator("div").filter(has_text="重新上传").count()
|
|
|
if count > 0:
|
|
|
print(f"[{self.platform_name}] 视频上传完毕")
|
|
|
break
|
|
|
-
|
|
|
- # 检查上传错误
|
|
|
- if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
|
|
|
- await self.handle_upload_error(params.video_path)
|
|
|
-
|
|
|
- await asyncio.sleep(3)
|
|
|
+ else:
|
|
|
+ print(f"[{self.platform_name}] 正在上传视频中... {i+1}/120")
|
|
|
+
|
|
|
+ # 检查上传错误
|
|
|
+ if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
|
|
|
+ print(f"[{self.platform_name}] 发现上传出错了,重新上传...")
|
|
|
+ await self.handle_upload_error(params.video_path)
|
|
|
+
|
|
|
+ await asyncio.sleep(3)
|
|
|
except:
|
|
|
+ print(f"[{self.platform_name}] 正在上传视频中...")
|
|
|
await asyncio.sleep(3)
|
|
|
|
|
|
self.report_progress(60, "处理视频设置...")
|
|
|
|
|
|
- # 关闭弹窗
|
|
|
- known_btn = self.page.get_by_role("button", name="我知道了")
|
|
|
- if await known_btn.count() > 0:
|
|
|
- await known_btn.first.click()
|
|
|
+ # 点击"我知道了"弹窗 - 参考 matrix
|
|
|
+ known_count = await self.page.get_by_role("button", name="我知道了").count()
|
|
|
+ if known_count > 0:
|
|
|
+ await self.page.get_by_role("button", name="我知道了").nth(0).click()
|
|
|
+ print(f"[{self.platform_name}] 关闭弹窗")
|
|
|
|
|
|
- await asyncio.sleep(2)
|
|
|
+ await asyncio.sleep(5)
|
|
|
|
|
|
- # 设置位置
|
|
|
+ # 设置位置 - 参考 matrix
|
|
|
try:
|
|
|
await self.page.locator('div.semi-select span:has-text("输入地理位置")').click()
|
|
|
await asyncio.sleep(1)
|
|
|
+ await self.page.keyboard.press("Backspace")
|
|
|
await self.page.keyboard.press("Control+KeyA")
|
|
|
await self.page.keyboard.press("Delete")
|
|
|
await self.page.keyboard.type(params.location)
|
|
|
await asyncio.sleep(1)
|
|
|
await self.page.locator('div[role="listbox"] [role="option"]').first.click()
|
|
|
+ print(f"[{self.platform_name}] 位置设置成功: {params.location}")
|
|
|
except Exception as e:
|
|
|
print(f"[{self.platform_name}] 设置位置失败: {e}")
|
|
|
|
|
|
- # 开启头条/西瓜同步
|
|
|
+ # 开启头条/西瓜同步 - 参考 matrix
|
|
|
try:
|
|
|
third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
|
|
|
if await self.page.locator(third_part_element).count():
|
|
|
@@ -279,6 +359,7 @@ class DouyinPublisher(BasePublisher):
|
|
|
if 'semi-switch-checked' not in class_name:
|
|
|
await self.page.locator(third_part_element).locator(
|
|
|
'input.semi-switch-native-control').click()
|
|
|
+ print(f"[{self.platform_name}] 已开启头条/西瓜同步")
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
@@ -290,61 +371,41 @@ class DouyinPublisher(BasePublisher):
|
|
|
self.report_progress(80, "正在发布...")
|
|
|
print(f"[{self.platform_name}] 查找发布按钮...")
|
|
|
|
|
|
- # 点击发布
|
|
|
- publish_clicked = False
|
|
|
+ # 点击发布 - 参考 matrix
|
|
|
for i in range(30):
|
|
|
try:
|
|
|
- # 每次循环都检查验证码
|
|
|
- captcha_result = await self.check_captcha()
|
|
|
- if captcha_result['need_captcha']:
|
|
|
- print(f"[{self.platform_name}] 发布过程中检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
|
|
|
- # 获取截图供 AI 分析
|
|
|
- screenshot_base64 = await self.capture_screenshot()
|
|
|
- page_url = await self.get_page_url()
|
|
|
- return PublishResult(
|
|
|
- success=False,
|
|
|
- platform=self.platform_name,
|
|
|
- error=f"发布过程中需要{captcha_result['captcha_type']}验证码",
|
|
|
- need_captcha=True,
|
|
|
- captcha_type=captcha_result['captcha_type'],
|
|
|
- screenshot_base64=screenshot_base64,
|
|
|
- page_url=page_url,
|
|
|
- status='need_captcha'
|
|
|
- )
|
|
|
-
|
|
|
- publish_btn = self.page.get_by_role('button', name="发布", exact=True)
|
|
|
- btn_count = await publish_btn.count()
|
|
|
- print(f"[{self.platform_name}] 发布按钮数量: {btn_count}")
|
|
|
-
|
|
|
- if btn_count > 0:
|
|
|
- print(f"[{self.platform_name}] 点击发布按钮...")
|
|
|
- await publish_btn.click()
|
|
|
- publish_clicked = True
|
|
|
-
|
|
|
- # 点击后等待并检查验证码
|
|
|
- await asyncio.sleep(2)
|
|
|
- captcha_result = await self.check_captcha()
|
|
|
- if captcha_result['need_captcha']:
|
|
|
- print(f"[{self.platform_name}] 点击发布后需要验证码: {captcha_result['captcha_type']}", flush=True)
|
|
|
+ # 检查验证码(不要在每次循环都调 AI,太慢)
|
|
|
+ if i % 5 == 0:
|
|
|
+ ai_captcha = await self.ai_check_captcha()
|
|
|
+ if ai_captcha['has_captcha']:
|
|
|
+ print(f"[{self.platform_name}] AI检测到发布过程中需要验证码: {ai_captcha['captcha_type']}", flush=True)
|
|
|
screenshot_base64 = await self.capture_screenshot()
|
|
|
page_url = await self.get_page_url()
|
|
|
return PublishResult(
|
|
|
success=False,
|
|
|
platform=self.platform_name,
|
|
|
- error=f"发布需要{captcha_result['captcha_type']}验证码",
|
|
|
+ error=f"发布过程中需要{ai_captcha['captcha_type']}验证码,请使用有头浏览器完成验证",
|
|
|
need_captcha=True,
|
|
|
- captcha_type=captcha_result['captcha_type'],
|
|
|
+ captcha_type=ai_captcha['captcha_type'],
|
|
|
screenshot_base64=screenshot_base64,
|
|
|
page_url=page_url,
|
|
|
status='need_captcha'
|
|
|
)
|
|
|
|
|
|
+ publish_btn = self.page.get_by_role('button', name="发布", exact=True)
|
|
|
+ btn_count = await publish_btn.count()
|
|
|
+
|
|
|
+ if btn_count > 0:
|
|
|
+ print(f"[{self.platform_name}] 点击发布按钮...")
|
|
|
+ await publish_btn.click()
|
|
|
+
|
|
|
+ # 等待跳转到内容管理页面 - 参考 matrix
|
|
|
await self.page.wait_for_url(
|
|
|
"https://creator.douyin.com/creator-micro/content/manage",
|
|
|
timeout=5000
|
|
|
)
|
|
|
self.report_progress(100, "发布成功")
|
|
|
- print(f"[{self.platform_name}] 发布成功! 已跳转到内容管理页面")
|
|
|
+ print(f"[{self.platform_name}] 视频发布成功!")
|
|
|
screenshot_base64 = await self.capture_screenshot()
|
|
|
page_url = await self.get_page_url()
|
|
|
return PublishResult(
|
|
|
@@ -357,11 +418,10 @@ class DouyinPublisher(BasePublisher):
|
|
|
)
|
|
|
except Exception as e:
|
|
|
current_url = self.page.url
|
|
|
- print(f"[{self.platform_name}] 尝试 {i+1}/30, 当前URL: {current_url}")
|
|
|
-
|
|
|
- if "content/manage" in current_url:
|
|
|
+ # 检查是否已经在管理页面
|
|
|
+ if "https://creator.douyin.com/creator-micro/content/manage" in current_url:
|
|
|
self.report_progress(100, "发布成功")
|
|
|
- print(f"[{self.platform_name}] 发布成功! 已在内容管理页面")
|
|
|
+ print(f"[{self.platform_name}] 视频发布成功!")
|
|
|
screenshot_base64 = await self.capture_screenshot()
|
|
|
return PublishResult(
|
|
|
success=True,
|
|
|
@@ -371,22 +431,12 @@ class DouyinPublisher(BasePublisher):
|
|
|
page_url=current_url,
|
|
|
status='success'
|
|
|
)
|
|
|
-
|
|
|
- # 检查是否有错误提示
|
|
|
- try:
|
|
|
- error_toast = self.page.locator('[class*="toast"][class*="error"], [class*="error-tip"]')
|
|
|
- if await error_toast.count() > 0:
|
|
|
- error_text = await error_toast.first.text_content()
|
|
|
- if error_text:
|
|
|
- print(f"[{self.platform_name}] 检测到错误提示: {error_text}")
|
|
|
- raise Exception(f"发布失败: {error_text}")
|
|
|
- except:
|
|
|
- pass
|
|
|
-
|
|
|
- await asyncio.sleep(1)
|
|
|
+ else:
|
|
|
+ print(f"[{self.platform_name}] 视频正在发布中... {i+1}/30, URL: {current_url}")
|
|
|
+ await asyncio.sleep(1)
|
|
|
|
|
|
- # 发布超时,返回截图供 AI 分析
|
|
|
- print(f"[{self.platform_name}] 发布超时,获取截图供 AI 分析...")
|
|
|
+ # 发布超时
|
|
|
+ print(f"[{self.platform_name}] 发布超时,获取截图...")
|
|
|
screenshot_base64 = await self.capture_screenshot()
|
|
|
page_url = await self.get_page_url()
|
|
|
|