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

feature/支持抖音的购物车功能
This commit is contained in:
Alleria
2025-10-17 11:01:40 +08:00
committed by GitHub
4 changed files with 157 additions and 38 deletions
+3 -2
View File
@@ -29,7 +29,8 @@ def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.L
asyncio.run(app.main(), debug=False) 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] account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file]
files = [Path(BASE_DIR / "videoFile" / file) for file in files] 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"视频文件名:{file}")
print(f"标题:{title}") print(f"标题:{title}")
print(f"Hashtag:{tags}") 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) asyncio.run(app.main(), debug=False)
+6 -2
View File
@@ -335,6 +335,8 @@ def postVideo():
enableTimer = data.get('enableTimer') enableTimer = data.get('enableTimer')
if category == 0: if category == 0:
category = None category = None
productLink = data.get('productLink', '')
productTitle = data.get('productTitle', '')
videos_per_day = data.get('videosPerDay') videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes') daily_times = data.get('dailyTimes')
@@ -351,7 +353,7 @@ def postVideo():
start_days) start_days)
case 3: case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days) start_days, productLink, productTitle)
case 4: case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days) start_days)
@@ -418,6 +420,8 @@ def postVideoBatch():
enableTimer = data.get('enableTimer') enableTimer = data.get('enableTimer')
if category == 0: if category == 0:
category = None category = None
productLink = data.get('productLink', '')
productTitle = data.get('productTitle', '')
videos_per_day = data.get('videosPerDay') videos_per_day = data.get('videosPerDay')
daily_times = data.get('dailyTimes') daily_times = data.get('dailyTimes')
@@ -433,7 +437,7 @@ def postVideoBatch():
start_days) start_days)
case 3: case 3:
post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days) start_days, productLink, productTitle)
case 4: case 4:
post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times,
start_days) start_days)
+54 -31
View File
@@ -381,6 +381,27 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 标签 (仅在抖音可见) -->
<div v-if="tab.selectedPlatform === 3" class="product-section">
<h3>商品链接</h3>
<el-input
v-model="tab.productTitle"
type="text"
:rows="1"
placeholder="请输入商品名称"
maxlength="200"
class="product-name-input"
/>
<el-input
v-model="tab.productLink"
type="text"
:rows="1"
placeholder="请输入商品链接"
maxlength="200"
class="product-link-input"
/>
</div>
<!-- 定时发布 --> <!-- 定时发布 -->
<div class="schedule-section"> <div class="schedule-section">
<h3>定时发布</h3> <h3>定时发布</h3>
@@ -490,23 +511,27 @@ const platforms = [
{ key: 1, name: '小红书' } { 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 // tab页数据 - 默认只有一个tab
const tabs = reactive([ const tabs = reactive([
{ defaultTabInit
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
}
]) ])
// 账号相关状态 // 账号相关状态
@@ -543,21 +568,9 @@ const recommendedTopics = [
// 添加新tab // 添加新tab
const addTab = () => { const addTab = () => {
tabCounter++ tabCounter++
const newTab = { const newTab = defaultTabInit
name: `tab${tabCounter}`, newTab['name'] = `tab${tabCounter}`
label: `发布${tabCounter}`, newTab['label'] = `发布${tabCounter}`
fileList: [],
displayFileList: [],
selectedAccounts: [],
selectedPlatform: 1,
title: '',
selectedTopics: [],
scheduleEnabled: false,
videosPerDay: 1,
dailyTimes: ['10:00'],
startDays: 0,
publishStatus: null
}
tabs.push(newTab) tabs.push(newTab)
activeTab.value = newTab.name activeTab.value = newTab.name
} }
@@ -747,7 +760,9 @@ const confirmPublish = async (tab) => {
videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55 videosPerDay: tab.scheduleEnabled ? tab.videosPerDay || 1 : 1, // 每天发布视频数量,1-55
dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点 dailyTimes: tab.scheduleEnabled ? tab.dailyTimes || ['10:00'] : ['10:00'], // 每天发布时间点
startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天 startDays: tab.scheduleEnabled ? tab.startDays || 0 : 0, // 从今天开始计算的发布天数,0表示明天,1表示后天
category: 0 //表示非原创 category: 0, //表示非原创
productLink: tab.productLink.trim() || '', // 商品链接
productTitle: tab.productTitle.trim() || '' // 商品名称
} }
// 调用后端发布API // 调用后端发布API
@@ -1131,10 +1146,18 @@ const batchPublish = async () => {
.account-section, .account-section,
.platform-section, .platform-section,
.title-section, .title-section,
.product-section,
.topic-section, .topic-section,
.schedule-section { .schedule-section {
margin-bottom: 30px; margin-bottom: 30px;
} }
.product-section {
.product-name-input,
.product-link-input {
margin-bottom: 5px;
}
}
.video-upload { .video-upload {
width: 100%; width: 100%;
+94 -3
View File
@@ -64,7 +64,7 @@ async def douyin_cookie_gen(account_file):
class DouYinVideo(object): 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.title = title # 视频标题
self.file_path = file_path self.file_path = file_path
self.tags = tags self.tags = tags
@@ -73,6 +73,8 @@ class DouYinVideo(object):
self.date_format = '%Y年%m月%d日 %H:%M' self.date_format = '%Y年%m月%d日 %H:%M'
self.local_executable_path = LOCAL_CHROME_PATH self.local_executable_path = LOCAL_CHROME_PATH
self.thumbnail_path = thumbnail_path self.thumbnail_path = thumbnail_path
self.productLink = productLink
self.productTitle = productTitle
async def set_schedule_time_douyin(self, page, publish_date): async def set_schedule_time_douyin(self, page, publish_date):
# 选择包含特定文本内容的 label 元素 # 选择包含特定文本内容的 label 元素
@@ -157,6 +159,12 @@ class DouYinVideo(object):
await page.press(css_selector, "Space") await page.press(css_selector, "Space")
douyin_logger.info(f'总共添加{len(self.tags)}个话题') douyin_logger.info(f'总共添加{len(self.tags)}个话题')
if self.productLink and self.productTitle:
douyin_logger.info(f' [-] 正在设置商品链接...')
await asyncio.sleep(1)
await self.set_product_link(page, self.productLink, self.productTitle)
douyin_logger.info(f' [+] 完成设置商品链接...')
while True: while True:
# 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待
try: try:
@@ -180,7 +188,8 @@ class DouYinVideo(object):
await self.set_thumbnail(page, self.thumbnail_path) await self.set_thumbnail(page, self.thumbnail_path)
# 更换可见元素 # 更换可见元素
# await self.set_location(page, "杭州市") await self.set_location(page, "")
# 頭條/西瓜 # 頭條/西瓜
third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
@@ -231,7 +240,9 @@ class DouYinVideo(object):
# await finish_confirm_element.click() # await finish_confirm_element.click()
# await page.locator("div[class^='footer'] button:has-text('完成')").click() # await page.locator("div[class^='footer'] button:has-text('完成')").click()
async def set_location(self, page: Page, location: str = "杭州市"): async def set_location(self, page: Page, location: str = ""):
if not location:
return
# todo supoort location later # todo supoort location later
# await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator( # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator(
# "div.semi-select-single").nth(0).click() # "div.semi-select-single").nth(0).click()
@@ -242,6 +253,86 @@ class DouYinVideo(object):
await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000) await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000)
await page.locator('div[role="listbox"] [role="option"]').first.click() 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 def main(self):
async with async_playwright() as playwright: async with async_playwright() as playwright:
await self.upload(playwright) await self.upload(playwright)