kuaishou.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. # -*- coding: utf-8 -*-
  2. """
  3. 快手视频发布器
  4. 参考: matrix/ks_uploader/main.py
  5. """
  6. import asyncio
  7. import os
  8. from datetime import datetime
  9. from typing import List
  10. from .base import (
  11. BasePublisher, PublishParams, PublishResult,
  12. WorkItem, WorksResult, CommentItem, CommentsResult
  13. )
  14. class KuaishouPublisher(BasePublisher):
  15. """
  16. 快手视频发布器
  17. 使用 Playwright 自动化操作快手创作者中心
  18. """
  19. platform_name = "kuaishou"
  20. login_url = "https://cp.kuaishou.com/"
  21. publish_url = "https://cp.kuaishou.com/article/publish/video"
  22. cookie_domain = ".kuaishou.com"
  23. async def set_schedule_time(self, publish_date: datetime):
  24. """设置定时发布"""
  25. if not self.page:
  26. return
  27. # 选择定时发布
  28. label_element = self.page.locator("label.radio--4Gpx6:has-text('定时发布')")
  29. await label_element.click()
  30. await asyncio.sleep(1)
  31. # 输入时间
  32. publish_date_str = publish_date.strftime("%Y-%m-%d %H:%M")
  33. await self.page.locator('.semi-input[placeholder="日期和时间"]').click()
  34. await self.page.keyboard.press("Control+KeyA")
  35. await self.page.keyboard.type(str(publish_date_str))
  36. await self.page.keyboard.press("Enter")
  37. await asyncio.sleep(1)
  38. async def upload_cover(self, cover_path: str):
  39. """上传封面图"""
  40. if not self.page or not cover_path or not os.path.exists(cover_path):
  41. return
  42. try:
  43. await self.page.get_by_role("button", name="编辑封面").click()
  44. await asyncio.sleep(1)
  45. await self.page.get_by_role("tab", name="上传封面").click()
  46. preview_div = self.page.get_by_role("tabpanel", name="上传封面").locator("div").nth(1)
  47. async with self.page.expect_file_chooser() as fc_info:
  48. await preview_div.click()
  49. preview_chooser = await fc_info.value
  50. await preview_chooser.set_files(cover_path)
  51. await self.page.get_by_role("button", name="确认").click()
  52. await asyncio.sleep(3)
  53. print(f"[{self.platform_name}] 封面上传成功")
  54. except Exception as e:
  55. print(f"[{self.platform_name}] 封面上传失败: {e}")
  56. async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
  57. """发布视频到快手"""
  58. self.report_progress(5, "正在初始化浏览器...")
  59. # 初始化浏览器
  60. await self.init_browser()
  61. # 解析并设置 cookies
  62. cookie_list = self.parse_cookies(cookies)
  63. await self.set_cookies(cookie_list)
  64. if not self.page:
  65. raise Exception("Page not initialized")
  66. # 检查视频文件
  67. if not os.path.exists(params.video_path):
  68. raise Exception(f"视频文件不存在: {params.video_path}")
  69. self.report_progress(10, "正在打开上传页面...")
  70. # 访问上传页面
  71. await self.page.goto(self.publish_url)
  72. await self.page.wait_for_url(self.publish_url, timeout=30000)
  73. self.report_progress(15, "正在选择视频文件...")
  74. # 点击上传按钮
  75. upload_btn = self.page.get_by_role("button", name="上传视频")
  76. async with self.page.expect_file_chooser() as fc_info:
  77. await upload_btn.click()
  78. file_chooser = await fc_info.value
  79. await file_chooser.set_files(params.video_path)
  80. await asyncio.sleep(1)
  81. # 关闭可能的弹窗
  82. known_btn = self.page.get_by_role("button", name="我知道了")
  83. if await known_btn.count():
  84. await known_btn.click()
  85. self.report_progress(20, "正在填充标题...")
  86. # 填写标题
  87. await asyncio.sleep(1)
  88. title_input = self.page.get_by_placeholder('添加合适的话题和描述,作品能获得更多推荐~')
  89. if await title_input.count():
  90. await title_input.click()
  91. await title_input.fill(params.title[:30])
  92. self.report_progress(30, "等待视频上传完成...")
  93. # 等待上传完成
  94. for _ in range(120):
  95. try:
  96. count = await self.page.locator('span:has-text("上传成功")').count()
  97. if count > 0:
  98. print(f"[{self.platform_name}] 视频上传完毕")
  99. break
  100. await asyncio.sleep(3)
  101. except:
  102. await asyncio.sleep(3)
  103. self.report_progress(50, "正在上传封面...")
  104. # 上传封面
  105. await self.upload_cover(params.cover_path)
  106. # 定时发布(快手暂不支持或选择器有变化)
  107. # if params.publish_date:
  108. # await self.set_schedule_time(params.publish_date)
  109. self.report_progress(80, "正在发布...")
  110. # 点击发布
  111. for _ in range(30):
  112. try:
  113. publish_btn = self.page.get_by_role('button', name="发布", exact=True)
  114. if await publish_btn.count():
  115. await publish_btn.click()
  116. await self.page.wait_for_url(
  117. "https://cp.kuaishou.com/article/manage/video*",
  118. timeout=5000
  119. )
  120. self.report_progress(100, "发布成功")
  121. return PublishResult(
  122. success=True,
  123. platform=self.platform_name,
  124. message="发布成功"
  125. )
  126. except:
  127. current_url = self.page.url
  128. if "manage/video" in current_url:
  129. self.report_progress(100, "发布成功")
  130. return PublishResult(
  131. success=True,
  132. platform=self.platform_name,
  133. message="发布成功"
  134. )
  135. await asyncio.sleep(1)
  136. raise Exception("发布超时")
  137. async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
  138. """获取快手作品列表"""
  139. print(f"\n{'='*60}")
  140. print(f"[{self.platform_name}] 获取作品列表")
  141. print(f"[{self.platform_name}] page={page}, page_size={page_size}")
  142. print(f"{'='*60}")
  143. works: List[WorkItem] = []
  144. total = 0
  145. has_more = False
  146. try:
  147. await self.init_browser()
  148. cookie_list = self.parse_cookies(cookies)
  149. await self.set_cookies(cookie_list)
  150. if not self.page:
  151. raise Exception("Page not initialized")
  152. # 访问创作者中心
  153. await self.page.goto("https://cp.kuaishou.com/")
  154. await asyncio.sleep(3)
  155. # 检查登录状态
  156. current_url = self.page.url
  157. if "passport" in current_url or "login" in current_url:
  158. raise Exception("Cookie 已过期,请重新登录")
  159. # 调用作品列表 API
  160. pcursor = "" if page == 0 else str(page)
  161. api_url = f"https://cp.kuaishou.com/rest/cp/works/v2/video/pc/photo/list?count={page_size}&pcursor={pcursor}&status=public"
  162. js_code = f"""
  163. async () => {{
  164. const resp = await fetch("{api_url}", {{
  165. credentials: 'include',
  166. headers: {{ 'Accept': 'application/json' }}
  167. }});
  168. return await resp.json();
  169. }}
  170. """
  171. response = await self.page.evaluate(js_code)
  172. if response.get('result') == 1:
  173. data = response.get('data', {})
  174. photo_list = data.get('list', [])
  175. has_more = len(photo_list) >= page_size
  176. for photo in photo_list:
  177. photo_id = photo.get('photoId', '')
  178. if not photo_id:
  179. continue
  180. # 封面
  181. cover_url = photo.get('coverUrl', '')
  182. if cover_url.startswith('http://'):
  183. cover_url = cover_url.replace('http://', 'https://')
  184. # 时长
  185. duration = photo.get('duration', 0) // 1000 # 毫秒转秒
  186. # 发布时间
  187. create_time = photo.get('timestamp', 0) // 1000
  188. publish_time = ''
  189. if create_time:
  190. from datetime import datetime
  191. publish_time = datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M:%S')
  192. works.append(WorkItem(
  193. work_id=str(photo_id),
  194. title=photo.get('caption', '') or '无标题',
  195. cover_url=cover_url,
  196. duration=duration,
  197. status='published',
  198. publish_time=publish_time,
  199. play_count=photo.get('viewCount', 0),
  200. like_count=photo.get('likeCount', 0),
  201. comment_count=photo.get('commentCount', 0),
  202. share_count=photo.get('shareCount', 0),
  203. ))
  204. print(f"[{self.platform_name}] 获取到 {len(works)} 个作品")
  205. except Exception as e:
  206. import traceback
  207. traceback.print_exc()
  208. return WorksResult(success=False, platform=self.platform_name, error=str(e))
  209. return WorksResult(success=True, platform=self.platform_name, works=works, total=total or len(works), has_more=has_more)
  210. async def get_comments(self, cookies: str, work_id: str, cursor: str = "") -> CommentsResult:
  211. """获取快手作品评论"""
  212. print(f"\n{'='*60}")
  213. print(f"[{self.platform_name}] 获取作品评论")
  214. print(f"[{self.platform_name}] work_id={work_id}")
  215. print(f"{'='*60}")
  216. comments: List[CommentItem] = []
  217. total = 0
  218. has_more = False
  219. try:
  220. await self.init_browser()
  221. cookie_list = self.parse_cookies(cookies)
  222. await self.set_cookies(cookie_list)
  223. if not self.page:
  224. raise Exception("Page not initialized")
  225. await self.page.goto("https://cp.kuaishou.com/")
  226. await asyncio.sleep(3)
  227. current_url = self.page.url
  228. if "passport" in current_url or "login" in current_url:
  229. raise Exception("Cookie 已过期,请重新登录")
  230. # 调用评论列表 API
  231. pcursor = cursor or ""
  232. api_url = f"https://cp.kuaishou.com/rest/cp/works/comment/list?photoId={work_id}&pcursor={pcursor}&count=20"
  233. js_code = f"""
  234. async () => {{
  235. const resp = await fetch("{api_url}", {{
  236. credentials: 'include',
  237. headers: {{ 'Accept': 'application/json' }}
  238. }});
  239. return await resp.json();
  240. }}
  241. """
  242. response = await self.page.evaluate(js_code)
  243. if response.get('result') == 1:
  244. data = response.get('data', {})
  245. comment_list = data.get('list', [])
  246. has_more = data.get('pcursor', '') != ''
  247. for comment in comment_list:
  248. cid = comment.get('commentId', '')
  249. if not cid:
  250. continue
  251. author = comment.get('author', {})
  252. # 解析子评论
  253. replies = []
  254. sub_list = comment.get('subComments', []) or []
  255. for sub in sub_list:
  256. sub_author = sub.get('author', {})
  257. replies.append(CommentItem(
  258. comment_id=str(sub.get('commentId', '')),
  259. work_id=work_id,
  260. content=sub.get('content', ''),
  261. author_id=str(sub_author.get('id', '')),
  262. author_name=sub_author.get('name', ''),
  263. author_avatar=sub_author.get('headurl', ''),
  264. like_count=sub.get('likeCount', 0),
  265. create_time=str(sub.get('timestamp', '')),
  266. ))
  267. comments.append(CommentItem(
  268. comment_id=str(cid),
  269. work_id=work_id,
  270. content=comment.get('content', ''),
  271. author_id=str(author.get('id', '')),
  272. author_name=author.get('name', ''),
  273. author_avatar=author.get('headurl', ''),
  274. like_count=comment.get('likeCount', 0),
  275. reply_count=comment.get('subCommentCount', 0),
  276. create_time=str(comment.get('timestamp', '')),
  277. replies=replies,
  278. ))
  279. total = len(comments)
  280. print(f"[{self.platform_name}] 获取到 {total} 条评论")
  281. except Exception as e:
  282. import traceback
  283. traceback.print_exc()
  284. return CommentsResult(success=False, platform=self.platform_name, work_id=work_id, error=str(e))
  285. return CommentsResult(success=True, platform=self.platform_name, work_id=work_id, comments=comments, total=total, has_more=has_more)