From d826a19bb2f6e56159c07839c937af0fc3eea76a Mon Sep 17 00:00:00 2001 From: billy Date: Tue, 16 Jun 2026 15:50:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyin):=20=E6=B7=BB=E5=8A=A0=20--bgm=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=E9=80=89=E6=8B=A9=E9=9F=B3?= =?UTF-8?q?=E4=B9=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/install.md | 6 ++- sau_cli.py | 4 ++ uploader/douyin_uploader/main.py | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index f028478..5530ee2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -105,9 +105,11 @@ sau douyin check --account sau douyin upload-video --account --file videos/demo.mp4 --title "示例标题" --desc "示例简介" 图文正文1: $noteText = @"图文正文"@ -sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' 图文正文2: -sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --notef '图文文件路径' --tags 'tag1,tag2' +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --notef '图文文件路径' --tags 'tag1,tag2' +添加 BGM(可选): +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' --bgm '音乐名称' ``` 抖音卡login手动获取cookie: diff --git a/sau_cli.py b/sau_cli.py index 73cb298..18c40f0 100644 --- a/sau_cli.py +++ b/sau_cli.py @@ -74,6 +74,7 @@ class DouyinNoteUploadRequest: publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE debug: bool = True headless: bool = True + bgm: str = "" @dataclass(slots=True) @@ -323,6 +324,7 @@ async def upload_note(request: DouyinNoteUploadRequest) -> Path: publish_strategy=request.publish_strategy, debug=request.debug, headless=request.headless, + bgm=request.bgm, ) await app.douyin_upload_note() return account_file @@ -549,6 +551,7 @@ def build_parser() -> argparse.ArgumentParser: upload_note_parser.add_argument("--note", default="", help="Optional note content") upload_note_parser.add_argument("--notef", default="", help="Read note content from file (txt/md)") upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") + upload_note_parser.add_argument("--bgm", default="", help="BGM music name to search and select") upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") add_runtime_flags(upload_note_parser) @@ -707,6 +710,7 @@ async def dispatch(args: argparse.Namespace) -> int: publish_strategy=publish_strategy, debug=args.debug, headless=args.headless, + bgm=args.bgm or "", ) await upload_note(request) print(f"Douyin note upload submitted: {len(request.image_files)} images") diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index b9b71a6..38bd9d0 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -400,6 +400,72 @@ class DouYinBaseUploader(BaseVideoUploader): except Exception as exc: douyin_logger.warning(_msg("🧾", f"自主声明设置失败,跳过该步骤继续发布:{exc}")) + async def select_bgm(self, page: Page, bgm_name: str) -> bool: + """为图文发布选择 BGM:可选增强功能,搜索无结果或异常均跳过不中断发布。""" + try: + # 点击「选择音乐」按钮 + music_entry = page.locator('text="选择音乐"').nth(1) + if not await music_entry.count(): + music_entry = page.locator('text="选择音乐"').first + await music_entry.wait_for(state="visible", timeout=10000) + await music_entry.click() + + # 等待侧边栏出现并搜索 + sidesheet = page.locator(".semi-sidesheet-content").first + await sidesheet.wait_for(state="visible", timeout=8000) + search_input = sidesheet.locator('input.semi-input[placeholder="搜索音乐"]').first + await search_input.wait_for(state="visible", timeout=5000) + await search_input.fill(bgm_name) + await search_input.press("Enter") + + # 等待搜索结果 + await asyncio.sleep(2) + first_card = sidesheet.locator(".card-container-tmocjc").first + try: + await first_card.wait_for(state="visible", timeout=8000) + except Exception: + douyin_logger.warning(_msg("🎵", f"音乐「{bgm_name}」搜索结果为空,小人跳过")) + await self._close_music_sidesheet(page) + return False + + # 打印找到的音乐名称 + try: + song_name_el = first_card.locator(".song-name-oRge4d").first + if await song_name_el.count(): + song_name = await song_name_el.inner_text() + douyin_logger.info(_msg("🎵", f"小人找到了: {song_name}")) + except Exception: + pass + + # JS 点击「使用」(按钮 visibility:hidden,普通 click 无效) + apply_btn = first_card.locator(".apply-btn-LUPP0D").first + await apply_btn.evaluate("el => el.click()") + douyin_logger.info(_msg("🥳", f"BGM「{bgm_name}」已应用")) + + # 等待侧边栏关闭,超时则手动关闭 + try: + await sidesheet.wait_for(state="hidden", timeout=5000) + except Exception: + await self._close_music_sidesheet(page) + + return True + except Exception as exc: + douyin_logger.warning(_msg("🎵", f"添加 BGM 时出错,跳过该步骤继续发布:{exc}")) + try: + await self._close_music_sidesheet(page) + except Exception: + pass + return False + + async def _close_music_sidesheet(self, page: Page) -> None: + try: + close_btn = page.locator(".semi-sidesheet-close").first + if await close_btn.count() and await close_btn.is_visible(): + await close_btn.click() + await asyncio.sleep(1) + except Exception: + pass + class DouYinVideo(DouYinBaseUploader): def __init__( @@ -619,6 +685,7 @@ class DouYinNote(DouYinBaseUploader): publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE, debug: bool = DEBUG_MODE, headless: bool = LOCAL_CHROME_HEADLESS, + bgm: str = "", ): super().__init__( publish_date=publish_date, @@ -631,6 +698,7 @@ class DouYinNote(DouYinBaseUploader): self.note = note or "" self.title = title or (self.note[:30] if self.note else "") self.tags = tags or [] + self.bgm = bgm or "" async def validate_upload_args(self): await self.validate_base_args() @@ -682,8 +750,15 @@ class DouYinNote(DouYinBaseUploader): await asyncio.sleep(1) douyin_logger.info(_msg("✍️", "小人开始填标题、描述和话题")) await self.fill_title_and_description(page, self.title, self.note, self.tags) + title_len = len(self.title) if self.title else 0 + tags_text = " ".join(f"#{t}" for t in self.tags) if self.tags else "" + desc_and_tags_len = len(self.note or "") + (len(tags_text) + 2 if self.tags else 0) + douyin_logger.info(_msg("📝", f"标题总字数: {title_len},描述+话题总字数: {desc_and_tags_len}")) douyin_logger.info(_msg("🏷️", f"小人一共贴了 {len(self.tags)} 个话题")) + if self.bgm: + await self.select_bgm(page, self.bgm) + if self.publish_strategy == DOUYIN_PUBLISH_STRATEGY_SCHEDULED and self.publish_date != 0: await self.set_schedule_time_douyin(page, self.publish_date)