douyin.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. """
  3. 抖音视频发布器
  4. 参考: matrix/douyin_uploader/main.py
  5. """
  6. import asyncio
  7. import os
  8. from datetime import datetime
  9. from .base import BasePublisher, PublishParams, PublishResult
  10. class DouyinPublisher(BasePublisher):
  11. """
  12. 抖音视频发布器
  13. 使用 Playwright 自动化操作抖音创作者中心
  14. """
  15. platform_name = "douyin"
  16. login_url = "https://creator.douyin.com/"
  17. publish_url = "https://creator.douyin.com/creator-micro/content/upload"
  18. cookie_domain = ".douyin.com"
  19. async def set_schedule_time(self, publish_date: datetime):
  20. """设置定时发布"""
  21. if not self.page:
  22. return
  23. # 选择定时发布
  24. label_element = self.page.locator("label.radio-d4zkru:has-text('定时发布')")
  25. await label_element.click()
  26. await asyncio.sleep(1)
  27. # 输入时间
  28. publish_date_str = publish_date.strftime("%Y-%m-%d %H:%M")
  29. await self.page.locator('.semi-input[placeholder="日期和时间"]').click()
  30. await self.page.keyboard.press("Control+KeyA")
  31. await self.page.keyboard.type(str(publish_date_str))
  32. await self.page.keyboard.press("Enter")
  33. await asyncio.sleep(1)
  34. async def handle_upload_error(self, video_path: str):
  35. """处理上传错误,重新上传"""
  36. if not self.page:
  37. return
  38. print(f"[{self.platform_name}] 视频出错了,重新上传中...")
  39. await self.page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(video_path)
  40. async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
  41. """发布视频到抖音"""
  42. self.report_progress(5, "正在初始化浏览器...")
  43. # 初始化浏览器
  44. await self.init_browser()
  45. # 解析并设置 cookies
  46. cookie_list = self.parse_cookies(cookies)
  47. await self.set_cookies(cookie_list)
  48. if not self.page:
  49. raise Exception("Page not initialized")
  50. # 检查视频文件
  51. if not os.path.exists(params.video_path):
  52. raise Exception(f"视频文件不存在: {params.video_path}")
  53. self.report_progress(10, "正在打开上传页面...")
  54. # 访问上传页面
  55. await self.page.goto(self.publish_url)
  56. await self.page.wait_for_url(self.publish_url, timeout=30000)
  57. self.report_progress(15, "正在选择视频文件...")
  58. # 点击上传区域
  59. upload_div = self.page.locator("div[class*='container-drag']").first
  60. async with self.page.expect_file_chooser() as fc_info:
  61. await upload_div.click()
  62. file_chooser = await fc_info.value
  63. await file_chooser.set_files(params.video_path)
  64. # 等待跳转到发布页面
  65. self.report_progress(20, "等待进入发布页面...")
  66. for _ in range(60):
  67. try:
  68. await self.page.wait_for_url(
  69. "https://creator.douyin.com/creator-micro/content/post/video*",
  70. timeout=2000
  71. )
  72. break
  73. except:
  74. await asyncio.sleep(1)
  75. await asyncio.sleep(2)
  76. self.report_progress(30, "正在填充标题和话题...")
  77. # 填写标题
  78. title_input = self.page.get_by_text('作品标题').locator("..").locator(
  79. "xpath=following-sibling::div[1]").locator("input")
  80. if await title_input.count():
  81. await title_input.fill(params.title[:30])
  82. else:
  83. # 备用方式
  84. title_container = self.page.locator(".notranslate")
  85. await title_container.click()
  86. await self.page.keyboard.press("Control+KeyA")
  87. await self.page.keyboard.press("Delete")
  88. await self.page.keyboard.type(params.title)
  89. await self.page.keyboard.press("Enter")
  90. # 添加话题标签
  91. if params.tags:
  92. css_selector = ".zone-container"
  93. for tag in params.tags:
  94. print(f"[{self.platform_name}] 添加话题: #{tag}")
  95. await self.page.type(css_selector, "#" + tag)
  96. await self.page.press(css_selector, "Space")
  97. self.report_progress(40, "等待视频上传完成...")
  98. # 等待视频上传完成
  99. for _ in range(120):
  100. try:
  101. count = await self.page.locator("div").filter(has_text="重新上传").count()
  102. if count > 0:
  103. print(f"[{self.platform_name}] 视频上传完毕")
  104. break
  105. # 检查上传错误
  106. if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
  107. await self.handle_upload_error(params.video_path)
  108. await asyncio.sleep(3)
  109. except:
  110. await asyncio.sleep(3)
  111. self.report_progress(60, "处理视频设置...")
  112. # 关闭弹窗
  113. known_btn = self.page.get_by_role("button", name="我知道了")
  114. if await known_btn.count() > 0:
  115. await known_btn.first.click()
  116. await asyncio.sleep(2)
  117. # 设置位置
  118. try:
  119. await self.page.locator('div.semi-select span:has-text("输入地理位置")').click()
  120. await asyncio.sleep(1)
  121. await self.page.keyboard.press("Control+KeyA")
  122. await self.page.keyboard.press("Delete")
  123. await self.page.keyboard.type(params.location)
  124. await asyncio.sleep(1)
  125. await self.page.locator('div[role="listbox"] [role="option"]').first.click()
  126. except Exception as e:
  127. print(f"[{self.platform_name}] 设置位置失败: {e}")
  128. # 开启头条/西瓜同步
  129. try:
  130. third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
  131. if await self.page.locator(third_part_element).count():
  132. class_name = await self.page.eval_on_selector(
  133. third_part_element, 'div => div.className')
  134. if 'semi-switch-checked' not in class_name:
  135. await self.page.locator(third_part_element).locator(
  136. 'input.semi-switch-native-control').click()
  137. except:
  138. pass
  139. # 定时发布
  140. if params.publish_date:
  141. self.report_progress(70, "设置定时发布...")
  142. await self.set_schedule_time(params.publish_date)
  143. self.report_progress(80, "正在发布...")
  144. # 点击发布
  145. for _ in range(30):
  146. try:
  147. publish_btn = self.page.get_by_role('button', name="发布", exact=True)
  148. if await publish_btn.count():
  149. await publish_btn.click()
  150. await self.page.wait_for_url(
  151. "https://creator.douyin.com/creator-micro/content/manage",
  152. timeout=5000
  153. )
  154. self.report_progress(100, "发布成功")
  155. return PublishResult(
  156. success=True,
  157. platform=self.platform_name,
  158. message="发布成功"
  159. )
  160. except:
  161. current_url = self.page.url
  162. if "content/manage" in current_url:
  163. self.report_progress(100, "发布成功")
  164. return PublishResult(
  165. success=True,
  166. platform=self.platform_name,
  167. message="发布成功"
  168. )
  169. await asyncio.sleep(1)
  170. raise Exception("发布超时")