Merge pull request #135 from leordsong/feature/douyin-productlink
Browse files- myUtils/postVideo.py +3 -2
- sau_backend.py +6 -2
- sau_frontend/src/views/PublishCenter.vue +54 -31
- uploader/douyin_uploader/main.py +94 -3
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 |
-
|
| 548 |
-
|
| 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 |
-
|
|
|
|
| 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)
|