From 250fff19a957a3a708267444b9f5a9ae56a39db5 Mon Sep 17 00:00:00 2001 From: Leo Song Date: Wed, 15 Oct 2025 16:25:12 +0800 Subject: [PATCH] [feature] support product link in douyin --- myUtils/postVideo.py | 5 +- sau_backend.py | 5 +- sau_frontend/src/views/PublishCenter.vue | 85 ++++++++++++++--------- uploader/douyin_uploader/main.py | 88 +++++++++++++++++++++++- 4 files changed, 148 insertions(+), 35 deletions(-) diff --git a/myUtils/postVideo.py b/myUtils/postVideo.py index a4a152e..8d41dce 100644 --- a/myUtils/postVideo.py +++ b/myUtils/postVideo.py @@ -29,7 +29,8 @@ def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.L asyncio.run(app.main(), debug=False) -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): +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, + productLink = '', productTitle = ''): # 生成文件的完整路径 account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] 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 print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") - app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, category) + app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, category, productLink, productTitle) asyncio.run(app.main(), debug=False) diff --git a/sau_backend.py b/sau_backend.py index cced7d9..397f343 100644 --- a/sau_backend.py +++ b/sau_backend.py @@ -335,6 +335,7 @@ def postVideo(): enableTimer = data.get('enableTimer') if category == 0: category = None + productLink = data.get('productLink', '') videos_per_day = data.get('videosPerDay') daily_times = data.get('dailyTimes') @@ -418,6 +419,8 @@ def postVideoBatch(): enableTimer = data.get('enableTimer') if category == 0: category = None + productLink = data.get('productLink', '') + productTitle = data.get('productTitle', '') videos_per_day = data.get('videosPerDay') daily_times = data.get('dailyTimes') @@ -433,7 +436,7 @@ def postVideoBatch(): start_days) case 3: post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, - start_days) + start_days, productLink, productTitle) case 4: post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days) diff --git a/sau_frontend/src/views/PublishCenter.vue b/sau_frontend/src/views/PublishCenter.vue index 731b5f7..94faca7 100644 --- a/sau_frontend/src/views/PublishCenter.vue +++ b/sau_frontend/src/views/PublishCenter.vue @@ -381,6 +381,27 @@ + +
+

商品链接

+ + +
+

定时发布

@@ -490,23 +511,27 @@ const platforms = [ { key: 1, name: '小红书' } ] +const defaultTabInit = { + name: 'tab1', + label: '发布1', + fileList: [], // 后端返回的文件名列表 + displayFileList: [], // 用于显示的文件列表 + selectedAccounts: [], // 选中的账号ID列表 + selectedPlatform: 1, // 选中的平台(单选) + title: '', + productLink: '', // 商品链接 + productTitle: '', // 商品名称 + selectedTopics: [], // 话题列表(不带#号) + scheduleEnabled: false, // 定时发布开关 + videosPerDay: 1, // 每天发布视频数量 + dailyTimes: ['10:00'], // 每天发布时间点列表 + startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天 + publishStatus: null // 发布状态,包含message和type +} + // tab页数据 - 默认只有一个tab const tabs = reactive([ - { - name: 'tab1', - label: '发布1', - fileList: [], // 后端返回的文件名列表 - displayFileList: [], // 用于显示的文件列表 - selectedAccounts: [], // 选中的账号ID列表 - selectedPlatform: 1, // 选中的平台(单选) - title: '', - selectedTopics: [], // 话题列表(不带#号) - scheduleEnabled: false, // 定时发布开关 - videosPerDay: 1, // 每天发布视频数量 - dailyTimes: ['10:00'], // 每天发布时间点列表 - startDays: 0, // 从今天开始计算的发布天数,0表示明天,1表示后天 - publishStatus: null // 发布状态,包含message和type - } + defaultTabInit ]) // 账号相关状态 @@ -543,21 +568,9 @@ const recommendedTopics = [ // 添加新tab const addTab = () => { tabCounter++ - const newTab = { - name: `tab${tabCounter}`, - label: `发布${tabCounter}`, - fileList: [], - displayFileList: [], - selectedAccounts: [], - selectedPlatform: 1, - title: '', - selectedTopics: [], - scheduleEnabled: false, - videosPerDay: 1, - dailyTimes: ['10:00'], - startDays: 0, - publishStatus: null - } + const newTab = defaultTabInit + newTab['name'] = `tab${tabCounter}` + newTab['label'] = `发布${tabCounter}` tabs.push(newTab) activeTab.value = newTab.name } @@ -747,7 +760,9 @@ const confirmPublish = async (tab) => { videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55 dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点 startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天 - category: 0 //表示非原创 + category: 0, //表示非原创 + productLink: tab.productLink.trim() || '', // 商品链接 + productTitle: tab.productTitle.trim() || '' // 商品名称 } // 调用后端发布API @@ -1131,10 +1146,18 @@ const batchPublish = async () => { .account-section, .platform-section, .title-section, + .product-section, .topic-section, .schedule-section { margin-bottom: 30px; } + + .product-section { + .product-name-input, + .product-link-input { + margin-bottom: 5px; + } + } .video-upload { width: 100%; diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 0ddee00..32fb1b7 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -64,7 +64,7 @@ async def douyin_cookie_gen(account_file): class DouYinVideo(object): - def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None): + def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None, productLink='', productTitle=''): self.title = title # 视频标题 self.file_path = file_path self.tags = tags @@ -73,6 +73,8 @@ class DouYinVideo(object): self.date_format = '%Y年%m月%d日 %H:%M' self.local_executable_path = LOCAL_CHROME_PATH self.thumbnail_path = thumbnail_path + self.productLink = productLink + self.productTitle = productTitle async def set_schedule_time_douyin(self, page, publish_date): # 选择包含特定文本内容的 label 元素 @@ -157,6 +159,10 @@ class DouYinVideo(object): await page.press(css_selector, "Space") douyin_logger.info(f'总共添加{len(self.tags)}个话题') + if self.productLink and self.productTitle: + await asyncio.sleep(1) + await self.set_product_link(page, self.productLink, self.productTitle) + while True: # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 try: @@ -242,6 +248,86 @@ class DouYinVideo(object): await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000) await page.locator('div[role="listbox"] [role="option"]').first.click() + async def handle_product_dialog(self, page: Page, product_title: str): + """处理商品编辑弹窗""" + + await page.wait_for_timeout(2000) + short_title_input = page.locator('input[placeholder="请输入商品短标题"]') + if not await short_title_input.count(): + douyin_logger.error("[-] 未找到商品短标题输入框") + return False + product_title = product_title[:10] + await short_title_input.fill(product_title) + # 等待一下让界面响应 + await page.wait_for_timeout(1000) + + finish_button = page.locator('button:has-text("完成编辑")') + if 'disabled' not in await finish_button.get_attribute('class'): + await finish_button.click() + douyin_logger.debug("[+] 成功点击'完成编辑'按钮") + + # 等待对话框关闭 + await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000) + return True + else: + douyin_logger.error("[-] '完成编辑'按钮处于禁用状态,尝试直接关闭对话框") + # 如果按钮禁用,尝试点击取消或关闭按钮 + cancel_button = page.locator('button:has-text("取消")') + if await cancel_button.count(): + await cancel_button.click() + else: + # 点击右上角的关闭按钮 + close_button = page.locator('.semi-modal-close') + await close_button.click() + + await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000) + return False + + async def set_product_link(self, page: Page, product_link: str, product_title: str): + """设置商品链接功能""" + try: + # 定位"添加标签"文本,然后向上导航到容器,再找到下拉框 + dropdown = page.get_by_text('添加标签').locator("..").locator("..").locator("..").locator(".semi-select").first + if not await dropdown.count(): + douyin_logger.error("[-] 未找到标签下拉框") + return False + douyin_logger.debug("[-] 找到标签下拉框,准备选择'购物车'") + await dropdown.click() + ## 等待下拉选项出现 + await page.wait_for_selector('[role="listbox"]', timeout=5000) + ## 选择"购物车"选项 + await page.locator('[role="option"]:has-text("购物车")').click() + douyin_logger.debug("[+] 成功选择'购物车'") + + # 输入商品链接 + ## 等待商品链接输入框出现 + await page.wait_for_selector('input[placeholder="粘贴商品链接"]', timeout=5000) + # 输入 + input_field = page.locator('input[placeholder="粘贴商品链接"]') + await input_field.fill(product_link) + douyin_logger.debug(f"[+] 已输入商品链接: {product_link}") + + # 点击"添加链接"按钮 + add_button = page.locator('span:has-text("添加链接")') + ## 检查按钮是否可用(没有disable类) + button_class = await add_button.get_attribute('class') + if 'disable' in button_class: + douyin_logger.error("[-] '添加链接'按钮不可用") + return False + await add_button.click() + douyin_logger.debug("[+] 成功点击'添加链接'按钮") + + # 填写商品短标题 + if not await self.handle_product_dialog(page, product_title): + return False + + # 等待链接添加完成 + douyin_logger.debug("[+] 成功设置商品链接") + return True + except Exception as e: + douyin_logger.error(f"[-] 设置商品链接时出错: {str(e)}") + return False + async def main(self): async with async_playwright() as playwright: await self.upload(playwright)