dreammis commited on
Commit
db0ac89
·
2 Parent(s): aab3ac6f7ffa8c

Merge pull request #135 from leordsong/feature/douyin-productlink

Browse files
myUtils/postVideo.py CHANGED
@@ -29,7 +29,8 @@ def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.L
29
  asyncio.run(app.main(), debug=False)
30
 
31
 
32
- def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0):
 
33
  # 生成文件的完整路径
34
  account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
35
  files = [Path(BASE_DIR / "videoFile" / file) for file in files]
@@ -44,7 +45,7 @@ def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LI
44
  print(f"视频文件名:{file}")
45
  print(f"标题:{title}")
46
  print(f"Hashtag:{tags}")
47
- app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, category)
48
  asyncio.run(app.main(), debug=False)
49
 
50
 
 
29
  asyncio.run(app.main(), debug=False)
30
 
31
 
32
+ def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0,
33
+ productLink = '', productTitle = ''):
34
  # 生成文件的完整路径
35
  account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
36
  files = [Path(BASE_DIR / "videoFile" / file) for file in files]
 
45
  print(f"视频文件名:{file}")
46
  print(f"标题:{title}")
47
  print(f"Hashtag:{tags}")
48
+ app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, category, productLink, productTitle)
49
  asyncio.run(app.main(), debug=False)
50
 
51
 
sau_backend.py CHANGED
@@ -335,6 +335,8 @@ def postVideo():
335
  enableTimer = data.get('enableTimer')
336
  if category == 0:
337
  category = None
 
 
338
 
339
  videos_per_day = data.get('videosPerDay')
340
  daily_times = data.get('dailyTimes')
@@ -351,7 +353,7 @@ def postVideo():
351
  start_days)
352
  case 3:
353
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
354
- start_days)
355
  case 4:
356
  post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
357
  start_days)
@@ -418,6 +420,8 @@ def postVideoBatch():
418
  enableTimer = data.get('enableTimer')
419
  if category == 0:
420
  category = None
 
 
421
 
422
  videos_per_day = data.get('videosPerDay')
423
  daily_times = data.get('dailyTimes')
@@ -433,7 +437,7 @@ def postVideoBatch():
433
  start_days)
434
  case 3:
435
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
436
- start_days)
437
  case 4:
438
  post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
439
  start_days)
 
335
  enableTimer = data.get('enableTimer')
336
  if category == 0:
337
  category = None
338
+ productLink = data.get('productLink', '')
339
+ productTitle = data.get('productTitle', '')
340
 
341
  videos_per_day = data.get('videosPerDay')
342
  daily_times = data.get('dailyTimes')
 
353
  start_days)
354
  case 3:
355
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
356
+ start_days, productLink, productTitle)
357
  case 4:
358
  post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
359
  start_days)
 
420
  enableTimer = data.get('enableTimer')
421
  if category == 0:
422
  category = None
423
+ productLink = data.get('productLink', '')
424
+ productTitle = data.get('productTitle', '')
425
 
426
  videos_per_day = data.get('videosPerDay')
427
  daily_times = data.get('dailyTimes')
 
437
  start_days)
438
  case 3:
439
  post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
440
+ start_days, productLink, productTitle)
441
  case 4:
442
  post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
443
  start_days)
sau_frontend/src/views/PublishCenter.vue CHANGED
@@ -381,6 +381,27 @@
381
  </template>
382
  </el-dialog>
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  <!-- 定时发布 -->
385
  <div class="schedule-section">
386
  <h3>定时发布</h3>
@@ -490,23 +511,27 @@ const platforms = [
490
  { key: 1, name: '小红书' }
491
  ]
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  // tab页数据 - 默认只有一个tab
494
  const tabs = reactive([
495
- {
496
- name: 'tab1',
497
- label: '发布1',
498
- fileList: [], // 后端返回的文件名列表
499
- displayFileList: [], // 用于显示的文件列表
500
- selectedAccounts: [], // 选中的账号ID列表
501
- selectedPlatform: 1, // 选中的平台(单选)
502
- title: '',
503
- selectedTopics: [], // 话题列表(不带#号)
504
- scheduleEnabled: false, // 定时发布开关
505
- videosPerDay: 1, // 每天发布视频数量
506
- dailyTimes: ['10:00'], // 每天发布时间点列表
507
- startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
508
- publishStatus: null // 发布状态,包含message和type
509
- }
510
  ])
511
 
512
  // 账号相关状态
@@ -543,21 +568,9 @@ const recommendedTopics = [
543
  // 添加新tab
544
  const addTab = () => {
545
  tabCounter++
546
- const newTab = {
547
- name: `tab${tabCounter}`,
548
- label: `发布${tabCounter}`,
549
- fileList: [],
550
- displayFileList: [],
551
- selectedAccounts: [],
552
- selectedPlatform: 1,
553
- title: '',
554
- selectedTopics: [],
555
- scheduleEnabled: false,
556
- videosPerDay: 1,
557
- dailyTimes: ['10:00'],
558
- startDays: 0,
559
- publishStatus: null
560
- }
561
  tabs.push(newTab)
562
  activeTab.value = newTab.name
563
  }
@@ -747,7 +760,9 @@ const confirmPublish = async (tab) => {
747
  videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55
748
  dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点
749
  startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
750
- category: 0 //表示非原创
 
 
751
  }
752
 
753
  // 调用后端发布API
@@ -1131,10 +1146,18 @@ const batchPublish = async () => {
1131
  .account-section,
1132
  .platform-section,
1133
  .title-section,
 
1134
  .topic-section,
1135
  .schedule-section {
1136
  margin-bottom: 30px;
1137
  }
 
 
 
 
 
 
 
1138
 
1139
  .video-upload {
1140
  width: 100%;
 
381
  </template>
382
  </el-dialog>
383
 
384
+ <!-- 标签 (仅在抖音可见) -->
385
+ <div v-if="tab.selectedPlatform === 3" class="product-section">
386
+ <h3>商品链接</h3>
387
+ <el-input
388
+ v-model="tab.productTitle"
389
+ type="text"
390
+ :rows="1"
391
+ placeholder="请输入商品名称"
392
+ maxlength="200"
393
+ class="product-name-input"
394
+ />
395
+ <el-input
396
+ v-model="tab.productLink"
397
+ type="text"
398
+ :rows="1"
399
+ placeholder="请输入商品链接"
400
+ maxlength="200"
401
+ class="product-link-input"
402
+ />
403
+ </div>
404
+
405
  <!-- 定时发布 -->
406
  <div class="schedule-section">
407
  <h3>定时发布</h3>
 
511
  { key: 1, name: '小红书' }
512
  ]
513
 
514
+ const defaultTabInit = {
515
+ name: 'tab1',
516
+ label: '发布1',
517
+ fileList: [], // 后端返回的文件名列表
518
+ displayFileList: [], // 用于显示的文件列表
519
+ selectedAccounts: [], // 选中的账号ID列表
520
+ selectedPlatform: 1, // 选中的平台(单选)
521
+ title: '',
522
+ productLink: '', // 商品链接
523
+ productTitle: '', // 商品名称
524
+ selectedTopics: [], // 话题列表(不带#号)
525
+ scheduleEnabled: false, // 定时发布开关
526
+ videosPerDay: 1, // 每天发布视频数量
527
+ dailyTimes: ['10:00'], // 每天发布时间点列表
528
+ startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
529
+ publishStatus: null // 发布状态,包含message和type
530
+ }
531
+
532
  // tab页数据 - 默认只有一个tab
533
  const tabs = reactive([
534
+ defaultTabInit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  ])
536
 
537
  // 账号相关状态
 
568
  // 添加新tab
569
  const addTab = () => {
570
  tabCounter++
571
+ const newTab = defaultTabInit
572
+ newTab['name'] = `tab${tabCounter}`
573
+ newTab['label'] = `发布${tabCounter}`
 
 
 
 
 
 
 
 
 
 
 
 
574
  tabs.push(newTab)
575
  activeTab.value = newTab.name
576
  }
 
760
  videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55
761
  dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点
762
  startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
763
+ category: 0, //表示非原创
764
+ productLink: tab.productLink.trim() || '', // 商品链接
765
+ productTitle: tab.productTitle.trim() || '' // 商品名称
766
  }
767
 
768
  // 调用后端发布API
 
1146
  .account-section,
1147
  .platform-section,
1148
  .title-section,
1149
+ .product-section,
1150
  .topic-section,
1151
  .schedule-section {
1152
  margin-bottom: 30px;
1153
  }
1154
+
1155
+ .product-section {
1156
+ .product-name-input,
1157
+ .product-link-input {
1158
+ margin-bottom: 5px;
1159
+ }
1160
+ }
1161
 
1162
  .video-upload {
1163
  width: 100%;
uploader/douyin_uploader/main.py CHANGED
@@ -64,7 +64,7 @@ async def douyin_cookie_gen(account_file):
64
 
65
 
66
  class DouYinVideo(object):
67
- def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None):
68
  self.title = title # 视频标题
69
  self.file_path = file_path
70
  self.tags = tags
@@ -73,6 +73,8 @@ class DouYinVideo(object):
73
  self.date_format = '%Y年%m月%d日 %H:%M'
74
  self.local_executable_path = LOCAL_CHROME_PATH
75
  self.thumbnail_path = thumbnail_path
 
 
76
 
77
  async def set_schedule_time_douyin(self, page, publish_date):
78
  # 选择包含特定文本内容的 label 元素
@@ -157,6 +159,12 @@ class DouYinVideo(object):
157
  await page.press(css_selector, "Space")
158
  douyin_logger.info(f'总共添加{len(self.tags)}个话题')
159
 
 
 
 
 
 
 
160
  while True:
161
  # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待
162
  try:
@@ -180,7 +188,8 @@ class DouYinVideo(object):
180
  await self.set_thumbnail(page, self.thumbnail_path)
181
 
182
  # 更换可见元素
183
- # await self.set_location(page, "杭州市")
 
184
 
185
  # 頭條/西瓜
186
  third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
@@ -231,7 +240,9 @@ class DouYinVideo(object):
231
  # await finish_confirm_element.click()
232
  # await page.locator("div[class^='footer'] button:has-text('完成')").click()
233
 
234
- async def set_location(self, page: Page, location: str = "杭州市"):
 
 
235
  # todo supoort location later
236
  # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator(
237
  # "div.semi-select-single").nth(0).click()
@@ -242,6 +253,86 @@ class DouYinVideo(object):
242
  await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000)
243
  await page.locator('div[role="listbox"] [role="option"]').first.click()
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  async def main(self):
246
  async with async_playwright() as playwright:
247
  await self.upload(playwright)
 
64
 
65
 
66
  class DouYinVideo(object):
67
+ def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None, productLink='', productTitle=''):
68
  self.title = title # 视频标题
69
  self.file_path = file_path
70
  self.tags = tags
 
73
  self.date_format = '%Y年%m月%d日 %H:%M'
74
  self.local_executable_path = LOCAL_CHROME_PATH
75
  self.thumbnail_path = thumbnail_path
76
+ self.productLink = productLink
77
+ self.productTitle = productTitle
78
 
79
  async def set_schedule_time_douyin(self, page, publish_date):
80
  # 选择包含特定文本内容的 label 元素
 
159
  await page.press(css_selector, "Space")
160
  douyin_logger.info(f'总共添加{len(self.tags)}个话题')
161
 
162
+ if self.productLink and self.productTitle:
163
+ douyin_logger.info(f' [-] 正在设置商品链接...')
164
+ await asyncio.sleep(1)
165
+ await self.set_product_link(page, self.productLink, self.productTitle)
166
+ douyin_logger.info(f' [+] 完成设置商品链接...')
167
+
168
  while True:
169
  # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待
170
  try:
 
188
  await self.set_thumbnail(page, self.thumbnail_path)
189
 
190
  # 更换可见元素
191
+ await self.set_location(page, "")
192
+
193
 
194
  # 頭條/西瓜
195
  third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
 
240
  # await finish_confirm_element.click()
241
  # await page.locator("div[class^='footer'] button:has-text('完成')").click()
242
 
243
+ async def set_location(self, page: Page, location: str = ""):
244
+ if not location:
245
+ return
246
  # todo supoort location later
247
  # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator(
248
  # "div.semi-select-single").nth(0).click()
 
253
  await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000)
254
  await page.locator('div[role="listbox"] [role="option"]').first.click()
255
 
256
+ async def handle_product_dialog(self, page: Page, product_title: str):
257
+ """处理商品编辑弹窗"""
258
+
259
+ await page.wait_for_timeout(2000)
260
+ short_title_input = page.locator('input[placeholder="请输入商品短标题"]')
261
+ if not await short_title_input.count():
262
+ douyin_logger.error("[-] 未找到商品短标题输入框")
263
+ return False
264
+ product_title = product_title[:10]
265
+ await short_title_input.fill(product_title)
266
+ # 等待一下让界面响应
267
+ await page.wait_for_timeout(1000)
268
+
269
+ finish_button = page.locator('button:has-text("完成编辑")')
270
+ if 'disabled' not in await finish_button.get_attribute('class'):
271
+ await finish_button.click()
272
+ douyin_logger.debug("[+] 成功点击'完成编辑'按钮")
273
+
274
+ # 等待对话框关闭
275
+ await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000)
276
+ return True
277
+ else:
278
+ douyin_logger.error("[-] '完成编辑'按钮处于禁用状态,尝试直接关闭对话框")
279
+ # 如果按钮禁用,尝试点击取消或关闭按钮
280
+ cancel_button = page.locator('button:has-text("取消")')
281
+ if await cancel_button.count():
282
+ await cancel_button.click()
283
+ else:
284
+ # 点击右上角的关闭按钮
285
+ close_button = page.locator('.semi-modal-close')
286
+ await close_button.click()
287
+
288
+ await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000)
289
+ return False
290
+
291
+ async def set_product_link(self, page: Page, product_link: str, product_title: str):
292
+ """设置商品链接功能"""
293
+ try:
294
+ # 定位"添加标签"文本,然后向上导航到容器,再找到下拉框
295
+ dropdown = page.get_by_text('添加标签').locator("..").locator("..").locator("..").locator(".semi-select").first
296
+ if not await dropdown.count():
297
+ douyin_logger.error("[-] 未找到标签下拉框")
298
+ return False
299
+ douyin_logger.debug("[-] 找到标签下拉框,准备选择'购物车'")
300
+ await dropdown.click()
301
+ ## 等待下拉选项出现
302
+ await page.wait_for_selector('[role="listbox"]', timeout=5000)
303
+ ## 选择"购物车"选项
304
+ await page.locator('[role="option"]:has-text("购物车")').click()
305
+ douyin_logger.debug("[+] 成功选择'购物车'")
306
+
307
+ # 输入商品链接
308
+ ## 等待商品链接输入框出现
309
+ await page.wait_for_selector('input[placeholder="粘贴商品链接"]', timeout=5000)
310
+ # 输入
311
+ input_field = page.locator('input[placeholder="粘贴商品链接"]')
312
+ await input_field.fill(product_link)
313
+ douyin_logger.debug(f"[+] 已输入商品链接: {product_link}")
314
+
315
+ # 点击"添加链接"按钮
316
+ add_button = page.locator('span:has-text("添加链接")')
317
+ ## 检查按钮是否可用(没有disable类)
318
+ button_class = await add_button.get_attribute('class')
319
+ if 'disable' in button_class:
320
+ douyin_logger.error("[-] '添加链接'按钮不可用")
321
+ return False
322
+ await add_button.click()
323
+ douyin_logger.debug("[+] 成功点击'添加链接'按钮")
324
+
325
+ # 填写商品短标题
326
+ if not await self.handle_product_dialog(page, product_title):
327
+ return False
328
+
329
+ # 等待链接添加完成
330
+ douyin_logger.debug("[+] 成功设置商品链接")
331
+ return True
332
+ except Exception as e:
333
+ douyin_logger.error(f"[-] 设置商品链接时出错: {str(e)}")
334
+ return False
335
+
336
  async def main(self):
337
  async with async_playwright() as playwright:
338
  await self.upload(playwright)