From f5cb31079cd2f790996549c81fb726afe10471d9 Mon Sep 17 00:00:00 2001 From: changyandaoc Date: Sat, 13 Jun 2026 11:54:29 +0800 Subject: [PATCH 01/18] =?UTF-8?q?fix(uploader):=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=8A=96=E9=9F=B3=E4=B8=8E=E8=A7=86=E9=A2=91=E5=8F=B7=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E9=A1=B5=E6=9C=80=E6=96=B0=20DOM=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抖音 douyin_uploader: - 填标题/描述:改用 input[placeholder*=填写作品标题] 与 div.zone-container[contenteditable] 定位 - 话题输入后按 Escape 收起下拉,避免浮层拦截 - '选择封面'与'发布'改用 force 点击,并移除拦截点击的新手引导(shepherd)浮层 - 封面弹窗适配:div.dy-creator-content-modal + input.semi-upload-hidden-input;横竖版切换改用'设置竖封面'按钮;完成后等弹窗关闭 视频号 tencent_uploader: - upload_video_file 遍历所有 frame 查找文件框 + 重试等待 说明:两平台发布均需有头模式(--headed),无头下视频号上传框不渲染。 --- uploader/douyin_uploader/main.py | 34 +++++++++++++++---------------- uploader/tencent_uploader/main.py | 31 ++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 5b25597..b89c5e4 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -263,17 +263,12 @@ class DouYinBaseUploader(BaseVideoUploader): await asyncio.sleep(1) async def fill_title_and_description(self, page: Page, title: str, description: str, tags: list[str] | None = None): - description_section = ( - page.get_by_text("作品描述", exact=True) - .locator("xpath=ancestor::div[2]") - .locator("xpath=following-sibling::div[1]") - ) - - title_input = description_section.locator('input[type="text"]').first + # 2026-06 抖音发布页 DOM:标题=input[placeholder*=填写作品标题],描述=div.zone-container[contenteditable] + title_input = page.locator('input[placeholder*="填写作品标题"]').first await title_input.wait_for(state="visible", timeout=10000) await title_input.fill(title[:30]) - description_editor = description_section.locator('.zone-container[contenteditable="true"]').first + description_editor = page.locator('div.zone-container[contenteditable="true"]').first await description_editor.wait_for(state="visible", timeout=10000) await description_editor.click() await page.keyboard.press("Control+KeyA") @@ -283,6 +278,7 @@ class DouYinBaseUploader(BaseVideoUploader): for tag in tags or []: await page.keyboard.type(" #" + tag) await page.keyboard.press("Space") + await page.keyboard.press("Escape") # 收起话题下拉,避免浮层拦截后续点击 async def set_location(self, page: Page, location: str = ""): if not location: @@ -471,12 +467,12 @@ class DouYinVideo(DouYinBaseUploader): return douyin_logger.info(_msg("🏃", "小人正在设置视频封面")) - await page.click('text="选择封面"') - cover_locator_str = 'div[id*="creator-content-modal"]' - cover_locator = page.locator(cover_locator_str) - await page.wait_for_selector(cover_locator_str) + await page.click('text="选择封面"', force=True) + cover_locator_str = 'div.dy-creator-content-modal' + cover_locator = page.locator(cover_locator_str).first + await page.wait_for_selector(cover_locator_str, timeout=15000) - upload_input = cover_locator.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input") + upload_input = cover_locator.locator("input.semi-upload-hidden-input").first if self.thumbnail_landscape_path: await page.wait_for_timeout(1000) @@ -485,15 +481,15 @@ class DouYinVideo(DouYinBaseUploader): douyin_logger.info(_msg("🖼️", "横版封面上传完成")) if self.thumbnail_portrait_path: - await cover_locator.locator("div[class*='steps'] div").nth(1).click() + await cover_locator.locator('button:has-text("设置竖封面")').first.click() await page.wait_for_timeout(1000) await upload_input.set_input_files(self.thumbnail_portrait_path) await page.wait_for_timeout(2000) douyin_logger.info(_msg("🖼️", "竖版封面上传完成")) - await cover_locator.locator('button:visible:has-text("完成")').click() + await cover_locator.locator('button:has-text("完成")').first.click() douyin_logger.info(_msg("🥳", "视频封面设置完成")) - await page.wait_for_selector("div.extractFooter", state="detached") + await cover_locator.wait_for(state="detached", timeout=15000) async def upload(self, playwright: Playwright) -> None: douyin_logger.info(_msg("🧍", "小人先检查 cookie、视频文件、封面和发布时间")) @@ -573,9 +569,13 @@ class DouYinVideo(DouYinBaseUploader): while True: try: + # 移除会拦截发布按钮点击的新手引导/话题下拉浮层 + await page.evaluate( + "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" + ) publish_button = page.get_by_role("button", name="发布", exact=True) if await publish_button.count(): - await publish_button.click() + await publish_button.click(force=True) await page.wait_for_url( "https://creator.douyin.com/creator-micro/content/manage**", timeout=3000, diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index 94cded1..db887f7 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -495,12 +495,35 @@ class TencentBaseUploader(BaseVideoUploader): await page.locator("div.input-editor").click() async def open_upload_page(self, page: Page) -> None: - await page.goto(TENCENT_UPLOAD_URL) - await page.wait_for_url(TENCENT_UPLOAD_URL) + await page.goto(TENCENT_UPLOAD_URL, timeout=120000, wait_until="domcontentloaded") + await page.wait_for_url(TENCENT_UPLOAD_URL, timeout=120000) async def upload_video_file(self, page: Page, file_path: str) -> None: - file_input = page.locator('input[type="file"]') - await file_input.set_input_files(file_path) + async def find_file_input(): + for fr in page.frames: # 主 frame + 所有 iframe(视频号编辑器可能在 iframe 内) + try: + fi = fr.locator('input[type="file"]') + if await fi.count(): + return fi.first + except Exception: + continue + return None + + fi = await find_file_input() + if fi is None: + # 助手落在首页:先点「发表视频」唤出编辑器与上传控件 + publish_btn = page.get_by_text("发表视频").first + if await publish_btn.count(): + await publish_btn.click() + await asyncio.sleep(3) + for _ in range(20): + fi = await find_file_input() + if fi is not None: + break + await asyncio.sleep(1) + if fi is None: + raise RuntimeError("未找到视频号文件上传框") + await fi.set_input_files(file_path) async def set_short_title(self, page: Page, title: str, short_title: str | None = None) -> None: short_title_element = ( From 5e28092f201ef881a402078cf9eba16e46a6714a Mon Sep 17 00:00:00 2001 From: changyandao Date: Sat, 13 Jun 2026 15:02:29 +0800 Subject: [PATCH 02/18] =?UTF-8?q?fix(douyin):=20version=5F2=20=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E9=A1=B5=E9=80=82=E9=85=8D=E2=80=94=E2=80=94=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E8=B6=85=E6=97=B6/=E5=B0=81=E9=9D=A2=E8=BE=93?= =?UTF-8?q?=E5=85=A5/=E8=AF=9D=E9=A2=98=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 标题/描述等待 10s→120s:version_2(post/video) 要等视频上传完才渲染表单(实测约40s) - 封面上传修正:封面弹窗有4个隐藏input,前2个是左侧AI参考图,真封面是 nth(1);旧 .first 传错位置导致封面不生效 - set_thumbnail 先清 shepherd 引导浮层,避免拦截选择封面点击 - 完成按钮用 exact 文本,避免误中完成编辑 --- uploader/douyin_uploader/main.py | 56 ++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index b89c5e4..fea5b3f 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -264,12 +264,13 @@ class DouYinBaseUploader(BaseVideoUploader): async def fill_title_and_description(self, page: Page, title: str, description: str, tags: list[str] | None = None): # 2026-06 抖音发布页 DOM:标题=input[placeholder*=填写作品标题],描述=div.zone-container[contenteditable] + # version_2(post/video) 发布页要等视频上传完才渲染表单(实测约 40s),故等待超时给到 120s title_input = page.locator('input[placeholder*="填写作品标题"]').first - await title_input.wait_for(state="visible", timeout=10000) + await title_input.wait_for(state="visible", timeout=120000) await title_input.fill(title[:30]) description_editor = page.locator('div.zone-container[contenteditable="true"]').first - await description_editor.wait_for(state="visible", timeout=10000) + await description_editor.wait_for(state="visible", timeout=120000) await description_editor.click() await page.keyboard.press("Control+KeyA") await page.keyboard.press("Delete") @@ -467,29 +468,46 @@ class DouYinVideo(DouYinBaseUploader): return douyin_logger.info(_msg("🏃", "小人正在设置视频封面")) - await page.click('text="选择封面"', force=True) + # 先清掉 shepherd 新手引导浮层,否则它会拦截“选择封面”点击导致弹窗打不开 + await page.evaluate( + "() => document.querySelectorAll('.shepherd-element,.shepherd-modal-overlay-container').forEach(e=>e.remove())" + ) + await page.get_by_text("选择封面", exact=True).first.click(force=True) cover_locator_str = 'div.dy-creator-content-modal' cover_locator = page.locator(cover_locator_str).first - await page.wait_for_selector(cover_locator_str, timeout=15000) + await page.wait_for_selector(cover_locator_str, timeout=20000) - upload_input = cover_locator.locator("input.semi-upload-hidden-input").first - - if self.thumbnail_landscape_path: - await page.wait_for_timeout(1000) - await upload_input.set_input_files(self.thumbnail_landscape_path) - await page.wait_for_timeout(2000) - douyin_logger.info(_msg("🖼️", "横版封面上传完成")) + await page.wait_for_timeout(1500) + # version_2 封面弹窗有 4 个隐藏 file input: + # [0]/[1] 左侧“AI生成参考图”上传/替换,[2]/[3] 才是“上传封面”/替换。 + # 旧代码用 .first 传到了 AI 参考图(不会成为封面)→ 这就是“传了却没封面”的根因。 + # 取 input.semi-upload-hidden-input 的第 2 个(nth(1)),即真正的封面上传输入。 + cover_upload = cover_locator.locator("input.semi-upload-hidden-input").nth(1) if self.thumbnail_portrait_path: - await cover_locator.locator('button:has-text("设置竖封面")').first.click() - await page.wait_for_timeout(1000) - await upload_input.set_input_files(self.thumbnail_portrait_path) - await page.wait_for_timeout(2000) - douyin_logger.info(_msg("🖼️", "竖版封面上传完成")) + # 弹窗默认就在“设置竖封面”页;防御性点一下 tab(已激活则忽略) + try: + await cover_locator.get_by_text("设置竖封面", exact=True).first.click(timeout=3000) + await page.wait_for_timeout(800) + except Exception: + pass + await cover_upload.set_input_files(self.thumbnail_portrait_path) + await page.wait_for_timeout(3000) + douyin_logger.info(_msg("🖼️", "竖版封面已上传到预览")) + elif self.thumbnail_landscape_path: + try: + await cover_locator.get_by_text("设置横封面", exact=True).first.click(timeout=3000) + await page.wait_for_timeout(800) + except Exception: + pass + await cover_upload.set_input_files(self.thumbnail_landscape_path) + await page.wait_for_timeout(3000) + douyin_logger.info(_msg("🖼️", "横版封面已上传到预览")) - await cover_locator.locator('button:has-text("完成")').first.click() + # 点红色主按钮“完成”应用封面(exact 避免误中“完成编辑”) + await cover_locator.get_by_role("button", name="完成", exact=True).first.click() douyin_logger.info(_msg("🥳", "视频封面设置完成")) - await cover_locator.wait_for(state="detached", timeout=15000) + await cover_locator.wait_for(state="detached", timeout=20000) async def upload(self, playwright: Playwright) -> None: douyin_logger.info(_msg("🧍", "小人先检查 cookie、视频文件、封面和发布时间")) @@ -508,6 +526,8 @@ class DouYinVideo(DouYinBaseUploader): douyin_logger.info(_msg("🏃", f"小人开始搬运视频: {self.title}.mp4")) douyin_logger.info(_msg("🧭", "小人正在赶往上传主页")) await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload") + # wait_for_url 完成时上传页可能尚未渲染出文件 input(实测偶发),先等它挂载再 set_input_files + await page.wait_for_selector("div[class^='container'] input", state="attached", timeout=60000) await page.locator("div[class^='container'] input").set_input_files(self.file_path) while True: From e192fa0cb1a5d44c546930380b2e1092fcc7c158 Mon Sep 17 00:00:00 2001 From: changyandao Date: Sat, 13 Jun 2026 15:33:01 +0800 Subject: [PATCH 03/18] =?UTF-8?q?fix(tencent):=20=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=8F=B7=E5=AE=9A=E6=97=B6=E6=97=B6=E9=97=B4=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E6=94=B6=E5=B0=BE=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_schedule_time 键入小时后改用 Enter 确认 + 收起浮层做容错(直接点 input-editor 可能被 weui-desktop-dialog 时间选择浮层遮挡),避免定时步偶发卡死 --- uploader/tencent_uploader/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index db887f7..32feaa2 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -492,7 +492,13 @@ class TencentBaseUploader(BaseVideoUploader): await page.click('input[placeholder="请选择时间"]') await page.keyboard.press("Control+KeyA") await page.keyboard.type(publish_date.strftime("%H")) - await page.locator("div.input-editor").click() + await page.keyboard.press("Enter") # 确认小时并关闭时间下拉 + await page.wait_for_timeout(500) + # 收起时间选择浮层:直接点描述区可能被 weui-desktop-dialog 遮挡,做容错 + try: + await page.locator("div.input-editor").click(timeout=5000) + except Exception: + await page.keyboard.press("Escape") async def open_upload_page(self, page: Page) -> None: await page.goto(TENCENT_UPLOAD_URL, timeout=120000, wait_until="domcontentloaded") From b9ca5448929ee3fb288e3d2e5a18b2204204b5c4 Mon Sep 17 00:00:00 2001 From: changyandao Date: Sat, 13 Jun 2026 16:23:23 +0800 Subject: [PATCH 04/18] =?UTF-8?q?fix(douyin):=20goto=20=E6=94=B9=20domcont?= =?UTF-8?q?entloaded=20=E9=81=BF=E5=85=8D=E6=8A=96=E9=9F=B3=20SPA=20load?= =?UTF-8?q?=20=E4=BA=8B=E4=BB=B6=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创作者中心发布页 load 事件常迟迟不触发,默认 goto(wait_until=load) 偶发 30s 超时。 3 处 content/upload 的 goto 改 wait_until=domcontentloaded + timeout=90000; 对应 wait_for_url 保持仅 timeout(不接受 wait_until) --- uploader/douyin_uploader/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index fea5b3f..e8ad943 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -55,7 +55,7 @@ async def cookie_auth(account_file): context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) page = await context.new_page() - await page.goto("https://creator.douyin.com/creator-micro/content/upload") + await page.goto("https://creator.douyin.com/creator-micro/content/upload", wait_until="domcontentloaded", timeout=90000) try: await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) except Exception: @@ -522,10 +522,10 @@ class DouYinVideo(DouYinBaseUploader): context = await set_init_script(context) page = await context.new_page() - await page.goto("https://creator.douyin.com/creator-micro/content/upload") + await page.goto("https://creator.douyin.com/creator-micro/content/upload", wait_until="domcontentloaded", timeout=90000) douyin_logger.info(_msg("🏃", f"小人开始搬运视频: {self.title}.mp4")) douyin_logger.info(_msg("🧭", "小人正在赶往上传主页")) - await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload") + await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=90000) # wait_for_url 完成时上传页可能尚未渲染出文件 input(实测偶发),先等它挂载再 set_input_files await page.wait_for_selector("div[class^='container'] input", state="attached", timeout=60000) await page.locator("div[class^='container'] input").set_input_files(self.file_path) @@ -725,9 +725,9 @@ class DouYinNote(DouYinBaseUploader): upload_success = False try: page = await context.new_page() - await page.goto("https://creator.douyin.com/creator-micro/content/upload") + await page.goto("https://creator.douyin.com/creator-micro/content/upload", wait_until="domcontentloaded", timeout=90000) douyin_logger.info(_msg("🧭", "小人正在赶往图文发布页")) - await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload") + await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=90000) await self.upload_note_content(page) upload_success = True From a083ad911f81c464796ee52c944ffe48493dd777 Mon Sep 17 00:00:00 2001 From: changyandao Date: Sat, 13 Jun 2026 17:34:39 +0800 Subject: [PATCH 05/18] =?UTF-8?q?fix(douyin=20login):=20=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=8F=90=E5=8F=96=E5=A4=B1=E8=B4=A5=E4=B8=8D=E8=87=B4?= =?UTF-8?q?=E5=91=BD=20+=20=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E6=94=BE=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _save_douyin_qrcode 定位不到二维码元素时不再抛错中止(有头浏览器可直接扫码),仅警告并继续轮询等登录跳转 - _is_douyin_login_completed 放宽到 creator-micro 任意页(不止 /home) - Path(image_path) 对空路径做守卫 --- uploader/douyin_uploader/main.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index e8ad943..3927993 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -106,7 +106,12 @@ async def _extract_douyin_qrcode_src(page: Page) -> str: async def _save_douyin_qrcode(page: Page, account_file: str, previous_qrcode_path: Path | None = None, qrcode_callback=None) -> dict: - qrcode_src = await _extract_douyin_qrcode_src(page) + # 提取二维码 src 仅为了保存/终端显示;定位不到时不致命——有头浏览器里二维码可见,直接扫码即可 + try: + qrcode_src = await _extract_douyin_qrcode_src(page) + except Exception as exc: + douyin_logger.warning(_msg("😵", f"没定位到二维码元素({str(exc)[:50]})——请直接在弹出的浏览器里扫码,小人继续等登录跳转")) + return {"image_path": "", "image_data_url": ""} qrcode_path = save_data_url_image(qrcode_src, build_login_qrcode_path(account_file)) if previous_qrcode_path and previous_qrcode_path != qrcode_path: if remove_qrcode_file(previous_qrcode_path): @@ -126,7 +131,8 @@ async def _save_douyin_qrcode(page: Page, account_file: str, previous_qrcode_pat async def _is_douyin_login_completed(page: Page) -> bool: - if not page.url.startswith("https://creator.douyin.com/creator-micro/home"): + # 登录后会跳到 creator-micro 下任意页(home/content 等);登录页是 creator.douyin.com/ 根路径 + if "creator.douyin.com/creator-micro" not in page.url: return False login_markers = [ @@ -149,7 +155,7 @@ async def _is_douyin_login_completed(page: Page) -> bool: async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dict, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100) -> dict: - qrcode_path = Path(qrcode_info["image_path"]) + qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None for _ in range(max_checks): if await _is_douyin_login_completed(page): douyin_logger.info(_msg("🥳", f"扫码成功,已经跳转到登录后页面: {page.url}")) @@ -161,7 +167,7 @@ async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dic await expired_box.click() await asyncio.sleep(1) qrcode_info = await _save_douyin_qrcode(page, account_file, qrcode_path, qrcode_callback=qrcode_callback) - qrcode_path = Path(qrcode_info["image_path"]) + qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None await asyncio.sleep(poll_interval) @@ -185,7 +191,7 @@ async def douyin_cookie_gen( page = await context.new_page() await page.goto("https://creator.douyin.com/") qrcode_info = await _save_douyin_qrcode(page, account_file, qrcode_callback=qrcode_callback) - qrcode_path = Path(qrcode_info["image_path"]) + qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None douyin_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成")) result = await _wait_for_douyin_login( page, From 914a5f0910ca767f4c169aa888989a358831ed75 Mon Sep 17 00:00:00 2001 From: changyandao Date: Sat, 13 Jun 2026 18:23:35 +0800 Subject: [PATCH 06/18] =?UTF-8?q?fix(douyin):=20cookie=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=B9=E6=9C=89=E5=A4=B4=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=97=A0=E5=A4=B4=E8=A2=AB=E5=8F=8D=E7=88=AC=E5=A2=99=E8=AF=AF?= =?UTF-8?q?=E5=88=A4=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cookie_auth 原硬编码 headless=True,但抖音无头会撞反爬墙→content/upload 跳登录→ 间歇性误报 cookie 失效(实测登录刚成功 90s 后校验即失败)。改 headless=False + 反检测 args。 --- uploader/douyin_uploader/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 3927993..05b123e 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -50,7 +50,11 @@ def _build_login_result(success: bool, status: str, message: str, account_file: async def cookie_auth(account_file): async with async_playwright() as playwright: - browser = await playwright.chromium.launch(headless=True, channel="chrome") + # 抖音无头会撞反爬墙→content/upload 跳登录→误判 cookie 失效(间歇性)。校验必须有头。 + browser = await playwright.chromium.launch( + headless=False, channel="chrome", + args=["--no-sandbox", "--disable-blink-features=AutomationControlled"], + ) try: context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) From 991bcdfdf3a29a0141a97f1093c8b7f652543cad Mon Sep 17 00:00:00 2001 From: nirolee Date: Sun, 14 Jun 2026 01:05:35 +0900 Subject: [PATCH 07/18] feat(youtube): add YouTube uploader via browser automation Add a YouTube uploader that follows the existing cookie-based browser automation pattern (uploader/youtube_uploader + sau youtube CLI: login/check/upload-video). Supports title/description/tags, thumbnail, adding to a playlist (for series), and visibility (public/unlisted/private). Browser automation is used instead of the official Data API on purpose: videos uploaded by an unaudited API project are force-locked to private and cannot be made public without Google's compliance audit, which is impractical for personal/single-channel use. Browser automation has no such restriction and matches how every other platform here works. --- README.md | 12 +- sau_cli.py | 108 +++++++++++ uploader/youtube_uploader/__init__.py | 5 + uploader/youtube_uploader/main.py | 264 ++++++++++++++++++++++++++ utils/log.py | 1 + 5 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 uploader/youtube_uploader/__init__.py create mode 100644 uploader/youtube_uploader/main.py diff --git a/README.md b/README.md index cee4e55..e5188e1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # social-auto-upload `social-auto-upload` 是一个强大的自动化工具,旨在帮助内容创作者和运营者高效地将视频内容一键发布到多个国内外主流社交媒体平台。 -项目实现了对 `抖音`、`Bilibili`、`小红书`、`快手`、`视频号`、`百家号` 以及 `TikTok` 等平台的视频上传、定时发布等功能。 +项目实现了对 `抖音`、`Bilibili`、`小红书`、`快手`、`视频号`、`百家号`、`TikTok` 以及 `YouTube` 等平台的视频上传、定时发布等功能。 结合各平台 `uploader` 模块,您可以轻松配置和扩展支持的平台,并通过示例脚本快速上手。 tiktok show @@ -69,6 +69,7 @@ | 视频号 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 对应 `tencent_uploader` | | 百家号 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 浏览器自动化 | | TikTok | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | 当前示例走 Chrome 版实现 | +| YouTube | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | 浏览器自动化(Studio),支持加入播放列表/可见性 | ### AI这么强,为什么还需要这个项目 在你使用AI的能力,browser agent等等,每次都让 agent 重新解析网页、截图理解, 临场判断 @@ -176,8 +177,17 @@ sau xiaohongshu upload-note --account --images videos/1.png video sau bilibili login --account sau bilibili check --account sau bilibili upload-video --account --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tid 249 + +sau youtube login --account +sau youtube check --account +sau youtube upload-video --account --file videos/demo.mp4 --title "示例标题" --desc "示例简介" --tags tag1,tag2 --playlist "我的系列" --visibility public ``` +> YouTube 说明:登录是交互式的(Google 账号,浏览器里完成,无二维码)。这里走浏览器自动化而不是官方 API, +> 是因为**未通过 Google 合规审核的 API 项目上传的视频会被强制锁为私享、无法改公开**,对个人/单频道不实用; +> 浏览器自动化没有此限制,可直接发布公开视频,也与本项目其它平台的 cookie 方案一致。 +> `--playlist` 适合连载/系列追更;`--visibility` 可选 `public`/`unlisted`/`private`。 + 补充说明: - `creator` 之类的名字只是示例值,真正含义是 `account_name` diff --git a/sau_cli.py b/sau_cli.py index b4152aa..d0475e3 100644 --- a/sau_cli.py +++ b/sau_cli.py @@ -41,6 +41,11 @@ from uploader.xiaohongshu_uploader.main import ( cookie_auth as xiaohongshu_cookie_auth, xiaohongshu_setup, ) +from uploader.youtube_uploader.main import ( + YouTubeVideo, + cookie_auth as youtube_cookie_auth, + youtube_setup, +) SCHEDULE_FORMAT = "%Y-%m-%d %H:%M" @@ -160,6 +165,20 @@ class TencentVideoUploadRequest: headless: bool = True +@dataclass(slots=True) +class YouTubeVideoUploadRequest: + account_name: str + video_file: Path + title: str + description: str + tags: list[str] + thumbnail_file: Path | None = None + playlist: str | None = None + visibility: str = "public" + debug: bool = True + headless: bool = False + + def has_interactive_terminal() -> bool: return sys.stdin.isatty() and sys.stdout.isatty() @@ -274,6 +293,42 @@ async def check_tencent_account(account_name: str) -> bool: return await tencent_cookie_auth(str(account_file)) +async def login_youtube_account(account_name: str, headless: bool = False) -> dict: + account_file = resolve_account_file("youtube", account_name) + return await youtube_setup(str(account_file), handle=True, return_detail=True, headless=headless) + + +async def check_youtube_account(account_name: str) -> bool: + account_file = resolve_account_file("youtube", account_name) + if not account_file.exists(): + return False + return await youtube_cookie_auth(str(account_file)) + + +async def upload_youtube_video(request: YouTubeVideoUploadRequest) -> Path: + account_file = resolve_account_file("youtube", request.account_name) + is_ready = await youtube_setup(str(account_file), handle=False) + if not is_ready: + raise RuntimeError( + f"YouTube cookie is missing or expired: {account_file}. Run `sau youtube login --account {request.account_name}` first." + ) + + app = YouTubeVideo( + request.title, + str(request.video_file), + request.tags, + str(account_file), + description=request.description, + thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None, + playlist=request.playlist, + visibility=request.visibility, + debug=request.debug, + headless=request.headless, + ) + await app.main() + return account_file + + async def upload_video(request: DouyinVideoUploadRequest) -> Path: account_file = resolve_account_file("douyin", request.account_name) is_ready = await douyin_setup(str(account_file), handle=False) @@ -646,6 +701,27 @@ def build_parser() -> argparse.ArgumentParser: tencent_upload_video_parser.add_argument("--category", help="Optional original content category") tencent_upload_video_parser.add_argument("--draft", action="store_true", help="Save as draft instead of publishing") add_runtime_flags(tencent_upload_video_parser) + + youtube_parser = platform_parsers.add_parser("youtube", help="YouTube operations") + youtube_actions = youtube_parser.add_subparsers(dest="action", required=True) + + for action_name in ("login", "check"): + action_parser = youtube_actions.add_parser(action_name, help=f"YouTube {action_name}") + action_parser.add_argument("--account", required=True, help="YouTube user-defined account_name") + if action_name == "login": + add_runtime_flags(action_parser) + + youtube_upload_video_parser = youtube_actions.add_parser("upload-video", help="Upload one video to YouTube") + youtube_upload_video_parser.add_argument("--account", required=True, help="YouTube user-defined account_name") + youtube_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") + youtube_upload_video_parser.add_argument("--title", required=True, help="Video title (<=100 chars)") + youtube_upload_video_parser.add_argument("--desc", default="", help="Optional video description") + youtube_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") + youtube_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail image path") + youtube_upload_video_parser.add_argument("--playlist", help="Optional playlist name to add the video to (for series)") + youtube_upload_video_parser.add_argument( + "--visibility", default="public", choices=["public", "unlisted", "private"], help="Video visibility") + add_runtime_flags(youtube_upload_video_parser) return parser @@ -882,6 +958,38 @@ async def dispatch(args: argparse.Namespace) -> int: raise RuntimeError(f"Unsupported Tencent/WeChat Channels action: {args.action}") + if args.platform == "youtube": + if args.action == "login": + result = await login_youtube_account(args.account, headless=args.headless) + if not result["success"]: + raise RuntimeError(result["message"]) + print(f"YouTube login flow completed: {result['account_file']}") + return 0 + + if args.action == "check": + is_valid = await check_youtube_account(args.account) + print("valid" if is_valid else "invalid") + return 0 if is_valid else 1 + + if args.action == "upload-video": + request = YouTubeVideoUploadRequest( + account_name=args.account, + video_file=args.file, + title=args.title, + description=args.desc, + tags=parse_tags(args.tags), + thumbnail_file=args.thumbnail, + playlist=args.playlist, + visibility=args.visibility, + debug=args.debug, + headless=args.headless, + ) + await upload_youtube_video(request) + print(f"YouTube video upload submitted: {request.video_file}") + return 0 + + raise RuntimeError(f"Unsupported YouTube action: {args.action}") + raise RuntimeError(f"Unsupported platform: {args.platform}") diff --git a/uploader/youtube_uploader/__init__.py b/uploader/youtube_uploader/__init__.py new file mode 100644 index 0000000..69323f7 --- /dev/null +++ b/uploader/youtube_uploader/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +from conf import BASE_DIR + +Path(BASE_DIR / "cookies" / "youtube_uploader").mkdir(exist_ok=True) diff --git a/uploader/youtube_uploader/main.py b/uploader/youtube_uploader/main.py new file mode 100644 index 0000000..8fd4456 --- /dev/null +++ b/uploader/youtube_uploader/main.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +"""YouTube uploader (browser automation via YouTube Studio). + +Unlike the other platforms here, YouTube also offers an official Data API. We deliberately +use browser automation instead, because videos uploaded through an *unaudited* API project +are force-locked to private and cannot be made public without passing Google's compliance +audit (which is impractical for personal/single-channel use). Browser automation has no such +restriction and can publish public videos right away, and it matches the cookie-based pattern +used by every other uploader in this project. + +Login is interactive (Google account, no QR code): the browser opens, the user signs in, and +the storage_state is saved. Reuse it afterwards for fully unattended uploads. +""" +import asyncio +from pathlib import Path + +from patchright.async_api import Page, Playwright, async_playwright + +from conf import DEBUG_MODE +from uploader.base_video import BaseVideoUploader +from utils.base_social_media import set_init_script +from utils.log import youtube_logger + +STUDIO_URL = "https://studio.youtube.com" +UPLOAD_URL = "https://www.youtube.com/upload" +VISIBILITY = {"public": "PUBLIC", "unlisted": "UNLISTED", "private": "PRIVATE"} + + +def _msg(emoji: str, text: str) -> str: + return f"{emoji} {text}" + + +def _build_login_result(success, status, message, account_file, current_url=""): + return { + "success": success, + "status": status, + "message": message, + "account_file": str(account_file), + "current_url": current_url, + } + + +async def cookie_auth(account_file) -> bool: + """登录态是否仍有效:带 cookie 打开 Studio,没被踢到 Google 登录页且进入了频道页即有效。""" + async with async_playwright() as playwright: + browser = await playwright.chromium.launch(headless=True, channel="chrome") + try: + context = await browser.new_context(storage_state=account_file) + context = await set_init_script(context) + page = await context.new_page() + await page.goto(STUDIO_URL, wait_until="domcontentloaded") + await page.wait_for_timeout(3000) + url = page.url + if "accounts.google.com" in url or "/signin" in url.lower(): + return False + return "/channel/" in url + except Exception: + return False + finally: + await browser.close() + + +async def youtube_cookie_gen(account_file, headless: bool = False): + """交互式登录:开浏览器让用户登录 Google/YouTube,进入频道页后保存 storage_state。""" + async with async_playwright() as playwright: + # 登录必须显形,让用户输账号密码/二步验证 + browser = await playwright.chromium.launch(headless=False, channel="chrome") + context = await browser.new_context() + context = await set_init_script(context) + page = await context.new_page() + await page.goto(STUDIO_URL, wait_until="domcontentloaded") + youtube_logger.info(_msg("🔐", "请在弹出的浏览器里登录 Google / YouTube 账号,登录后会自动保存")) + ok = False + for _ in range(600): # 最多等 10 分钟 + if "/channel/" in page.url: + await page.wait_for_timeout(2000) # 让 cookie 落定 + ok = True + break + await asyncio.sleep(1) + if ok: + await context.storage_state(path=account_file) + youtube_logger.success(_msg("✅", f"YouTube 登录态已保存: {account_file}")) + else: + youtube_logger.error(_msg("😵", "等待登录超时,未保存登录态")) + await browser.close() + return _build_login_result(ok, "logged_in" if ok else "timeout", + "登录成功" if ok else "登录超时", account_file, page.url) + + +async def youtube_setup(account_file, handle: bool = False, return_detail: bool = False, headless: bool = False): + """校验登录态,失效且 handle=True 时拉起交互式登录。""" + if not Path(account_file).exists() or not await cookie_auth(account_file): + if not handle: + result = _build_login_result(False, "cookie_invalid", "登录态不存在或已失效", account_file) + return result if return_detail else False + youtube_logger.info(_msg("🥹", "YouTube 登录态不存在或失效,准备打开浏览器登录")) + result = await youtube_cookie_gen(account_file, headless=headless) + return result if return_detail else result["success"] + result = _build_login_result(True, "cookie_valid", "登录态有效", account_file) + return result if return_detail else True + + +async def _fill_editable(page: Page, selector: str, text: str): + """填 YouTube Studio 的 contenteditable 富文本框(标题/简介),先清空再输入。""" + box = page.locator(selector).first + await box.wait_for(state="visible", timeout=30000) + await box.click() + await page.keyboard.press("Control+A") + await page.keyboard.press("Delete") + await box.type(text, delay=6) + await page.wait_for_timeout(400) + + +async def _click_if_present(page: Page, selector: str, timeout: int = 4000) -> bool: + try: + el = page.locator(selector).first + await el.wait_for(state="visible", timeout=timeout) + await el.click() + return True + except Exception: + return False + + +class YouTubeVideo(BaseVideoUploader): + def __init__(self, title, file_path, tags, account_file, *, + description="", thumbnail_path=None, playlist=None, + visibility="public", debug=DEBUG_MODE, headless=False): + self.title = title + self.file_path = str(file_path) + self.tags = tags or [] + self.account_file = str(account_file) + self.description = description or "" + self.thumbnail_path = str(thumbnail_path) if thumbnail_path else None + self.playlist = playlist + self.visibility = visibility if visibility in VISIBILITY else "public" + self.debug = debug + self.headless = headless + + async def upload(self, playwright: Playwright) -> None: + browser = await playwright.chromium.launch(headless=self.headless, channel="chrome") + context = await browser.new_context(storage_state=self.account_file) + context = await set_init_script(context) + page = await context.new_page() + page.set_default_timeout(60000) + + youtube_logger.info(_msg("🎬", f"开始上传: {Path(self.file_path).name}")) + await page.goto(UPLOAD_URL, wait_until="domcontentloaded") + await page.wait_for_timeout(3000) + if "accounts.google.com" in page.url or "signin" in page.url.lower(): + await browser.close() + raise RuntimeError("YouTube 登录态失效,请重新执行 login") + + # 1) 选择视频文件 + file_input = page.locator('input[type="file"]').first + await file_input.wait_for(state="attached", timeout=60000) + await file_input.set_input_files(self.file_path) + + # 2) 等详情对话框 + await page.locator("#title-textarea").wait_for(state="visible", timeout=120000) + + # 3) 标题 + youtube_logger.info(_msg("✍️", "填写标题")) + await _fill_editable(page, "#title-textarea #textbox", self.title[:100]) + + # 4) 简介 + if self.description.strip(): + youtube_logger.info(_msg("✍️", "填写简介")) + await _fill_editable(page, "#description-textarea #textbox", self.description) + + # 5) 封面(处理到一定进度才允许传,失败不致命) + if self.thumbnail_path and Path(self.thumbnail_path).exists(): + try: + thumb_input = page.locator( + "#file-loader input[type='file'], ytcp-thumbnail-uploader input[type='file']" + ).first + await thumb_input.wait_for(state="attached", timeout=20000) + await thumb_input.set_input_files(self.thumbnail_path) + await page.wait_for_timeout(2000) + youtube_logger.info(_msg("🖼️", "封面已上传")) + except Exception as exc: + youtube_logger.warning(_msg("⚠️", f"封面上传跳过(不影响发布): {exc}")) + + # 6) 加入播放列表(连载/系列追更)。弹窗务必关闭,否则挡住后续步骤。 + if self.playlist: + try: + await _click_if_present( + page, "#basics ytcp-text-dropdown-trigger, ytcp-video-metadata-playlists ytcp-dropdown-trigger", 8000) + await page.wait_for_timeout(1200) + existing = page.locator( + f"tp-yt-paper-checkbox:has-text('{self.playlist}'), " + f"ytcp-checkbox-group:has-text('{self.playlist}')").first + if await existing.count(): + await existing.click() + else: + if await _click_if_present(page, "ytcp-button:has-text('New playlist'), ytcp-button:has-text('创建播放列表')", 4000): + await page.wait_for_timeout(800) + await _click_if_present(page, "tp-yt-paper-item:has-text('New playlist'), tp-yt-paper-item:has-text('新建播放列表')", 3000) + title_box = page.locator("ytcp-playlist-metadata-editor #textbox, #create-playlist-form #textbox").first + if await title_box.count(): + await title_box.click() + await title_box.type(self.playlist, delay=6) + await _click_if_present(page, "ytcp-button#create-button, tp-yt-paper-dialog ytcp-button:has-text('Create'), tp-yt-paper-dialog ytcp-button:has-text('创建')", 4000) + except Exception as exc: + youtube_logger.warning(_msg("⚠️", f"播放列表处理跳过(不影响发布): {exc}")) + finally: + await _click_if_present(page, "ytcp-playlist-dialog #save-button, ytcp-button:has-text('Done'), ytcp-button:has-text('完成')", 3000) + await page.keyboard.press("Escape") + await page.wait_for_timeout(600) + + # 7) 受众:非儿童向(必填) + if not await _click_if_present(page, "tp-yt-paper-radio-button[name='VIDEO_MADE_FOR_KIDS_NOT_MFK']", 10000): + await _click_if_present(page, "tp-yt-paper-radio-button:has-text('not made for kids'), tp-yt-paper-radio-button:has-text('不是面向儿童')", 6000) + + # 8) 标签(“显示更多”里) + if self.tags: + try: + await _click_if_present(page, "#toggle-button", 6000) + await page.wait_for_timeout(800) + tag_input = page.locator("#tags-container #text-input, ytcp-form-input-container#tags-container input").first + await tag_input.click() + await tag_input.type(",".join(self.tags)[:500] + ",", delay=4) + except Exception as exc: + youtube_logger.warning(_msg("⚠️", f"标签填写跳过(不影响发布): {exc}")) + + # 9) 连点 Next 到“可见性”步骤 + for _ in range(5): + vis = page.locator("tp-yt-paper-radio-button[name='PUBLIC']") + if await vis.count() and await vis.first.is_visible(): + break + if not await _click_if_present(page, "#next-button", 6000): + await page.wait_for_timeout(1200) + await page.wait_for_timeout(1000) + + # 10) 可见性 + youtube_logger.info(_msg("🌐", f"设置可见性 = {self.visibility}")) + await _click_if_present(page, f"tp-yt-paper-radio-button[name='{VISIBILITY[self.visibility]}']", 10000) + + # 11) 发布 + await page.wait_for_timeout(1200) + if not await _click_if_present(page, "#done-button", 15000): + youtube_logger.warning(_msg("🤔", "未找到发布按钮,可能上传未到可发布进度;请在窗口里手动发布")) + else: + await page.wait_for_timeout(4000) + video_url = "" + try: + link = page.locator("a[href*='youtu.be'], a[href*='watch?v=']").first + if await link.count(): + video_url = await link.get_attribute("href") or "" + except Exception: + pass + await _click_if_present(page, "ytcp-button:has-text('Close'), ytcp-button:has-text('关闭'), #close-button", 8000) + youtube_logger.success(_msg("🥳", f"发布完成({self.visibility}){(' ' + video_url) if video_url else ''}")) + + # 刷新 cookie + try: + await context.storage_state(path=self.account_file) + except Exception: + pass + await page.wait_for_timeout(2000) + await browser.close() + + async def main(self): + async with async_playwright() as playwright: + await self.upload(playwright) diff --git a/utils/log.py b/utils/log.py index 3271da9..5a2de54 100644 --- a/utils/log.py +++ b/utils/log.py @@ -57,3 +57,4 @@ bilibili_logger = create_logger('bilibili', 'logs/bilibili.log') kuaishou_logger = create_logger('kuaishou', 'logs/kuaishou.log') baijiahao_logger = create_logger('baijiahao', 'logs/baijiahao.log') xiaohongshu_logger = create_logger('xiaohongshu', 'logs/xiaohongshu.log') +youtube_logger = create_logger('youtube', 'logs/youtube.log') From aa5112e985bc788e698c4826661f1e50e7ea7d72 Mon Sep 17 00:00:00 2001 From: nirolee Date: Sun, 14 Jun 2026 15:18:05 +0900 Subject: [PATCH 08/18] fix(youtube): wait for upload to finish before publishing; add optional proxy - Wait until the upload progress reaches 100% before clicking publish. The browser-based upload only progresses while the page is open, so publishing (and closing the browser) mid-upload cut it off and left the video stuck partway (e.g. 76%). - Add optional YT_PROXY (conf.py): where youtube.com is blocked, direct connections time out and the chromium does not use the system proxy, so allow pointing it at a local proxy explicitly. --- README.md | 2 ++ conf.example.py | 4 +++ uploader/youtube_uploader/main.py | 45 ++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5188e1..44ebe4f 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,8 @@ sau youtube upload-video --account --file videos/demo.mp4 --title > 是因为**未通过 Google 合规审核的 API 项目上传的视频会被强制锁为私享、无法改公开**,对个人/单频道不实用; > 浏览器自动化没有此限制,可直接发布公开视频,也与本项目其它平台的 cookie 方案一致。 > `--playlist` 适合连载/系列追更;`--visibility` 可选 `public`/`unlisted`/`private`。 +> 上传会**等进度到 100% 再点发布**(浏览器上传靠窗口开着传,传一半就发布会被掐断卡在中途)。 +> youtube.com 被墙的地区:在 `conf.py` 设 `YT_PROXY = "http://127.0.0.1:7890"`(chromium 不吃系统代理,需显式指定)。 补充说明: diff --git a/conf.example.py b/conf.example.py index 11e33c6..cf2e942 100644 --- a/conf.example.py +++ b/conf.example.py @@ -5,3 +5,7 @@ XHS_SERVER = "http://127.0.0.1:11901" # only used by xhs-related flows LOCAL_CHROME_PATH = "" # optional, e.g. C:/Program Files/Google/Chrome/Application/chrome.exe LOCAL_CHROME_HEADLESS = True # default headless behavior for uploader/examples DEBUG_MODE = True # default debug behavior +# Optional proxy for the YouTube uploader. Where youtube.com is blocked, direct +# connections time out and the (patchright) chromium does NOT use the system proxy. +# Point this at your local proxy port, e.g. "http://127.0.0.1:7890". None = no proxy. +YT_PROXY = None diff --git a/uploader/youtube_uploader/main.py b/uploader/youtube_uploader/main.py index 8fd4456..a036e24 100644 --- a/uploader/youtube_uploader/main.py +++ b/uploader/youtube_uploader/main.py @@ -21,6 +21,13 @@ from uploader.base_video import BaseVideoUploader from utils.base_social_media import set_init_script from utils.log import youtube_logger +try: + # 国内直连 youtube.com 会超时,且 patchright 启的 chromium 不吃系统代理。 + # 在 conf.py 设 YT_PROXY = "http://127.0.0.1:7890"(本地代理端口)即可;不设则不走代理。 + from conf import YT_PROXY +except Exception: + YT_PROXY = None + STUDIO_URL = "https://studio.youtube.com" UPLOAD_URL = "https://www.youtube.com/upload" VISIBILITY = {"public": "PUBLIC", "unlisted": "UNLISTED", "private": "PRIVATE"} @@ -121,6 +128,34 @@ async def _click_if_present(page: Page, selector: str, timeout: int = 4000) -> b return False +async def _wait_upload_complete(page: Page, max_polls: int = 360) -> bool: + """等网页上传从 X% 跑到 100% 再发布。浏览器上传靠窗口开着才传得完, + 若上传到一半就点发布并关闭浏览器,上传会被掐断卡在中途(如 76%)。 + 出现“处理/检查/上传完成”或不再“正在上传”即视为传完。max_polls*5s=30min 上限。""" + last = "" + for _ in range(max_polls): + txt = "" + for sel in (".progress-label", "span.progress-label", "ytcp-video-upload-progress"): + loc = page.locator(sel).first + try: + if await loc.count(): + txt = (await loc.inner_text()).strip() + if txt: + break + except Exception: + pass + if txt: + if any(k in txt for k in ("处理", "检查", "上传完成", "已上传", "Processing", "complete", "Checks", "Finished")): + youtube_logger.info(_msg("✅", f"上传完成: {txt[:40]}")) + return True + if txt != last: + youtube_logger.info(_msg("⏳", f"上传中: {txt[:40]}")) + last = txt + await page.wait_for_timeout(5000) + youtube_logger.warning(_msg("⚠️", "等上传超时(30min),仍尝试发布")) + return False + + class YouTubeVideo(BaseVideoUploader): def __init__(self, title, file_path, tags, account_file, *, description="", thumbnail_path=None, playlist=None, @@ -137,7 +172,10 @@ class YouTubeVideo(BaseVideoUploader): self.headless = headless async def upload(self, playwright: Playwright) -> None: - browser = await playwright.chromium.launch(headless=self.headless, channel="chrome") + browser = await playwright.chromium.launch( + headless=self.headless, channel="chrome", + proxy={"server": YT_PROXY} if YT_PROXY else None, + ) context = await browser.new_context(storage_state=self.account_file) context = await set_init_script(context) page = await context.new_page() @@ -235,6 +273,11 @@ class YouTubeVideo(BaseVideoUploader): youtube_logger.info(_msg("🌐", f"设置可见性 = {self.visibility}")) await _click_if_present(page, f"tp-yt-paper-radio-button[name='{VISIBILITY[self.visibility]}']", 10000) + # 10.5) 关键:等上传真正传完再发布。浏览器上传靠窗口开着传, + # 传到一半就点发布+关浏览器 = 上传被掐断卡在中途(如 76%)。 + youtube_logger.info(_msg("📤", "等待上传完成(传完才发布)…")) + await _wait_upload_complete(page) + # 11) 发布 await page.wait_for_timeout(1200) if not await _click_if_present(page, "#done-button", 15000): From 9677fa3d70a06b6d14326d32796f3fc7fa3e0bad Mon Sep 17 00:00:00 2001 From: billy Date: Mon, 15 Jun 2026 15:01:36 +0800 Subject: [PATCH 09/18] =?UTF-8?q?fix(douyin)=E4=BC=98=E5=8C=96=E5=9B=BE?= =?UTF-8?q?=E6=96=87=E5=8F=91=E5=B8=83=E6=AD=A3=E6=96=87=E5=92=8C=E8=AF=9D?= =?UTF-8?q?=E9=A2=98=E8=BE=93=E5=85=A5=E6=96=B9=E5=BC=8F=20,tags=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E6=8B=BC=E6=8E=A5=E5=88=B0description=E6=9C=AB?= =?UTF-8?q?=E5=B0=BE=E4=B8=80=E8=B5=B7=E8=B5=8B=E5=80=BC=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8Dhashtag=E8=A7=A3=E6=9E=90=E6=B7=B7=E4=B9=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uploader/douyin_uploader/main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 5b25597..61f555c 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -278,11 +278,15 @@ class DouYinBaseUploader(BaseVideoUploader): await description_editor.click() await page.keyboard.press("Control+KeyA") await page.keyboard.press("Delete") - await page.keyboard.type(description) - for tag in tags or []: - await page.keyboard.type(" #" + tag) - await page.keyboard.press("Space") + # tags 合并到 description 结尾,一起用 textContent 赋值 + if tags: + tags_text = " ".join(f"#{t}" for t in tags) + full_text = description + "\n\n" + tags_text + else: + full_text = description + + await description_editor.evaluate('(el, text) => el.textContent = text', full_text) async def set_location(self, page: Page, location: str = ""): if not location: From 73d3b11ca8b8a2ab01a2a074d365643b479c207f Mon Sep 17 00:00:00 2001 From: billy Date: Mon, 15 Jun 2026 15:04:03 +0800 Subject: [PATCH 10/18] =?UTF-8?q?docs(douyin)=EF=BC=9A=E6=B7=BB=E5=8A=A0ta?= =?UTF-8?q?gs=E5=8F=82=E6=95=B0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 175b23c..c75c900 100644 --- a/docs/install.md +++ b/docs/install.md @@ -103,7 +103,7 @@ sau bilibili --help sau douyin login --account sau douyin check --account sau douyin upload-video --account --file videos/demo.mp4 --title "示例标题" --desc "示例简介" -sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note "图文正文" +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note "图文正文" --tags 'tag1,tag2' ``` 抖音卡login手动获取cookie: From 621b94fc050f20802f2935c57d168d8c4af25e3b Mon Sep 17 00:00:00 2001 From: billy Date: Mon, 15 Jun 2026 15:10:24 +0800 Subject: [PATCH 11/18] =?UTF-8?q?docs(douyin)=EF=BC=9A=E5=9B=BE=E6=96=87?= =?UTF-8?q?=E6=AD=A3=E6=96=87=E7=BB=88=E7=AB=AF=E4=BF=9D=E7=95=99=E9=A2=84?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=A0=BC=E5=BC=8F=E6=8F=90=E4=BA=A4=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/install.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index c75c900..6ad97a0 100644 --- a/docs/install.md +++ b/docs/install.md @@ -103,7 +103,8 @@ sau bilibili --help sau douyin login --account sau douyin check --account sau douyin upload-video --account --file videos/demo.mp4 --title "示例标题" --desc "示例简介" -sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note "图文正文" --tags 'tag1,tag2' +$noteText = @"图文正文"@ +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' ``` 抖音卡login手动获取cookie: From 2c2aea32fe829d83cf882b82eda9c40840efc35b Mon Sep 17 00:00:00 2001 From: billy Date: Mon, 15 Jun 2026 15:31:03 +0800 Subject: [PATCH 12/18] =?UTF-8?q?feat(douyin):=E6=96=B0=E5=A2=9E--notef?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BB=8Etxt/md?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=AF=BB=E5=8F=96=E6=AD=A3=E6=96=87=E9=A2=84?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/install.md | 3 +++ sau_cli.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 6ad97a0..f028478 100644 --- a/docs/install.md +++ b/docs/install.md @@ -103,8 +103,11 @@ sau bilibili --help sau douyin login --account 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' +图文正文2: +sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --notef '图文文件路径' --tags 'tag1,tag2' ``` 抖音卡login手动获取cookie: diff --git a/sau_cli.py b/sau_cli.py index b4152aa..73cb298 100644 --- a/sau_cli.py +++ b/sau_cli.py @@ -547,6 +547,7 @@ def build_parser() -> argparse.ArgumentParser: upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths") upload_note_parser.add_argument("--title", required=True, help="Note title") 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("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") add_runtime_flags(upload_note_parser) @@ -687,11 +688,20 @@ async def dispatch(args: argparse.Namespace) -> int: return 0 if args.action == "upload-note": + # 如果指定了 --notef,读取文件内容作为 note + note_content = args.note + if args.notef: + note_file = Path(args.notef) + if not note_file.exists(): + print(f"错误:文件不存在: {note_file}", file=sys.stderr) + return 1 + note_content = note_file.read_text(encoding="utf-8") + request = DouyinNoteUploadRequest( account_name=args.account, image_files=parse_image_files(args.images), title=args.title, - note=args.note, + note=note_content, tags=parse_tags(args.tags), publish_date=args.schedule or 0, publish_strategy=publish_strategy, From 9c3d8de82d49ea5292a59d8b9c5740fb7eb7e75a Mon Sep 17 00:00:00 2001 From: billy Date: Mon, 15 Jun 2026 15:32:54 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat(douyin):=E6=96=B0=E5=A2=9E=E5=9B=BE?= =?UTF-8?q?=E6=96=87=E6=A0=87=E9=A2=98=E5=92=8C=E6=AD=A3=E6=96=87=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E6=95=B0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uploader/douyin_uploader/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 61f555c..b9b71a6 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -636,6 +636,10 @@ class DouYinNote(DouYinBaseUploader): await self.validate_base_args() if not self.title or not str(self.title).strip(): raise ValueError("图文模式下,title 是必须的") + + if len(self.title) > 20: + raise ValueError(f"标题不能超过20字符,当前: {len(self.title)}字符") + if not self.image_paths: raise ValueError("图文模式下,图片是必须的") @@ -645,6 +649,10 @@ class DouYinNote(DouYinBaseUploader): if len(self.image_paths) > 35: raise ValueError("图文模式下最多只支持上传 35 张图片") + note_len = len(self.note) if self.note else 0 + if note_len > 1000: + raise ValueError(f"正文不能超过1000字符,当前: {note_len}字符") + normalized_image_paths = [] for image_path in self.image_paths: normalized_image_paths.append(str(self.validate_image_file(image_path))) From d826a19bb2f6e56159c07839c937af0fc3eea76a Mon Sep 17 00:00:00 2001 From: billy Date: Tue, 16 Jun 2026 15:50:41 +0800 Subject: [PATCH 14/18] =?UTF-8?q?feat(douyin):=20=E6=B7=BB=E5=8A=A0=20--bg?= =?UTF-8?q?m=20=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E9=9F=B3=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) From 1cb7d8a7979295421d35319831cd269eae039765 Mon Sep 17 00:00:00 2001 From: home-dev-pookz Date: Sat, 20 Jun 2026 00:54:00 +0800 Subject: [PATCH 15/18] add new sponsor --- README.md | 10 ++++++++++ static/DolOffer.png | Bin 0 -> 104979 bytes 2 files changed, 10 insertions(+) create mode 100644 static/DolOffer.png diff --git a/README.md b/README.md index 44ebe4f..f0a6bc1 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ ## 💎 赞助商 + + + +
+ + DolOffer Sponsor + + + 感谢 DolOffer 对本项目的支持!对于做内容矩阵、多平台分发和 AI 自动化运营的创作者来说,ChatGPT、Claude、YouTube Premium、Spotify、Apple Music、Notion、Office 等数字工具往往是长期成本。DolOffer 提供 AI、视频、音乐和效率工具相关的订阅与充值服务,帮助用户更低成本地配置常用数字产品。更多说明可查看 DolOffer Guide。使用优惠码 AI8888 可额外享受 9 折优惠,具体价格和服务规则以官网为准。 +
diff --git a/static/DolOffer.png b/static/DolOffer.png new file mode 100644 index 0000000000000000000000000000000000000000..55e19114def346101927d70616533aea935036bf GIT binary patch literal 104979 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

-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

f%H~8b^y054+VE9U?QpSgvE-doo!R8T_fE3L~ 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 KgUYlW-uxdQ3!m=* literal 0 HcmV?d00001 From 5b26106fb13d1389dbf25d1c217a145fcfaae9c3 Mon Sep 17 00:00:00 2001 From: nirolee Date: Sat, 20 Jun 2026 03:34:55 +0900 Subject: [PATCH 16/18] fix(youtube): prevent #Shorts hashtag autocomplete from blocking upload Title and description were typed character-by-character, so a '#' (e.g. '#Shorts') triggered YouTube's topic-autocomplete dropdown that follows the caret and covers the Next/Publish buttons, stalling the upload. Switch to fill() (one-shot insert, no per-keystroke autocomplete) with a type() fallback, and add _dismiss_autocomplete (blur, then Escape only when a dropdown is actually visible so the upload dialog is never closed by mistake). --- uploader/youtube_uploader/main.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/uploader/youtube_uploader/main.py b/uploader/youtube_uploader/main.py index a036e24..4a1b0e1 100644 --- a/uploader/youtube_uploader/main.py +++ b/uploader/youtube_uploader/main.py @@ -107,15 +107,41 @@ async def youtube_setup(account_file, handle: bool = False, return_detail: bool return result if return_detail else True +async def _dismiss_autocomplete(page: Page): + """关掉 # 话题 / @ 提及 自动补全下拉浮层(会挡住后续“继续/发布”按钮)。 + + 先 blur 失焦;若浮层仍可见再补一次 Escape——仅在检测到浮层时才按, + 避免在没有浮层时误关掉整个上传对话框。""" + try: + await page.evaluate("() => { const a = document.activeElement; if (a && a.blur) a.blur(); }") + except Exception: + pass + try: + dropdown = page.locator("tp-yt-iron-dropdown:visible") + if await dropdown.count() > 0: + await page.keyboard.press("Escape") + await page.wait_for_timeout(200) + except Exception: + pass + + async def _fill_editable(page: Page, selector: str, text: str): - """填 YouTube Studio 的 contenteditable 富文本框(标题/简介),先清空再输入。""" + """填 YouTube Studio 的 contenteditable 富文本框(标题/简介),先清空再输入。 + + 用 fill() 一次性灌入而非逐字 type():标题/简介里的 # 字符(如 #Shorts)会触发 + YouTube 的话题自动补全下拉浮层;逐字输入会让浮层持续跟随光标弹出、盖住输入框与 + 后续“继续/发布”按钮,导致上传流程卡死。fill() 一次性写入不会逐字触发补全。""" box = page.locator(selector).first await box.wait_for(state="visible", timeout=30000) await box.click() await page.keyboard.press("Control+A") await page.keyboard.press("Delete") - await box.type(text, delay=6) + try: + await box.fill(text) # 一次性灌入,不逐字触发 # 话题自动补全 + except Exception: + await box.type(text, delay=6) # 个别 contenteditable 不支持 fill 时退回逐字输入 await page.wait_for_timeout(400) + await _dismiss_autocomplete(page) # 收尾关掉可能弹出的补全浮层 async def _click_if_present(page: Page, selector: str, timeout: int = 4000) -> bool: From ea2bcb0d8a9ccc33b02db7b77ff641d0d7f8b910 Mon Sep 17 00:00:00 2001 From: tankztz Date: Sat, 20 Jun 2026 09:45:31 +0800 Subject: [PATCH 17/18] fix: allow configuring Xiaohongshu creator domain --- docs/CLI.md | 6 ++++ tests/test_xiaohongshu_uploader.py | 24 +++++++++++++++ uploader/xiaohongshu_uploader/main.py | 43 +++++++++++++++++++-------- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index bff5e2e..9e65c89 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -70,6 +70,12 @@ sau xiaohongshu upload-video --account --file videos/demo.mp4 --t sau xiaohongshu upload-note --account --images videos/1.png videos/2.png videos/3.png --title "图文标题" --note "图文示例" --tags 图文,测试 ``` +海外环境如果无法登录默认创作者后台,可以通过环境变量切换到 RedNote 域名。该设置同时作用于登录、cookie 校验、视频发布和图文发布: + +```bash +SAU_XHS_CREATOR_BASE_URL=https://creator.rednote.com sau xiaohongshu login --account +``` + ## Bilibili CLI 子命令 ```bash diff --git a/tests/test_xiaohongshu_uploader.py b/tests/test_xiaohongshu_uploader.py index d075cc9..d2b2fcd 100644 --- a/tests/test_xiaohongshu_uploader.py +++ b/tests/test_xiaohongshu_uploader.py @@ -1,4 +1,5 @@ import asyncio +import os import tempfile import unittest from pathlib import Path @@ -89,6 +90,29 @@ class RecordingPage: class XiaohongshuUploaderTests(unittest.TestCase): + def test_creator_urls_keep_xiaohongshu_domain_by_default(self): + with patch.dict(os.environ, {"SAU_XHS_CREATOR_BASE_URL": ""}): + self.assertEqual( + xhs_main._build_xhs_creator_url("/login"), + "https://creator.xiaohongshu.com/login", + ) + + def test_creator_urls_use_configured_rednote_domain(self): + with patch.dict( + os.environ, + {"SAU_XHS_CREATOR_BASE_URL": "https://creator.rednote.com/"}, + ): + self.assertEqual( + xhs_main._build_xhs_creator_url("/login"), + "https://creator.rednote.com/login", + ) + self.assertEqual( + xhs_main._build_xhs_creator_url( + "/publish/publish?from=homepage&target=video" + ), + "https://creator.rednote.com/publish/publish?from=homepage&target=video", + ) + def test_find_xhs_qrcode_locator_prefers_scan_sibling_inside_login_box(self): qrcode_locator = FakeLocator("qrcode", count=1, src="data:image/png;base64,abc") scan_text_locator = FakeLocator( diff --git a/uploader/xiaohongshu_uploader/main.py b/uploader/xiaohongshu_uploader/main.py index 0a51076..b709bf4 100644 --- a/uploader/xiaohongshu_uploader/main.py +++ b/uploader/xiaohongshu_uploader/main.py @@ -21,9 +21,8 @@ from utils.login_qrcode import remove_qrcode_file from utils.login_qrcode import save_data_url_image from utils.log import xiaohongshu_logger -XHS_LOGIN_URL = "https://creator.xiaohongshu.com/login" -XHS_PUBLISH_VIDEO_URL = "https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video" -XHS_PUBLISH_NOTE_URL = "https://creator.xiaohongshu.com/publish/publish?from=homepage&target=image" +XHS_DEFAULT_CREATOR_BASE_URL = "https://creator.xiaohongshu.com" +XHS_CREATOR_BASE_URL_ENV = "SAU_XHS_CREATOR_BASE_URL" XHS_PUBLISH_SUCCESS_URL_PATTERN = "**/publish/success?**" XHS_LOGIN_BOX_SELECTOR = "div[class*='login-box']" XHS_LOGIN_SWITCH_SELECTOR = "img.css-wemwzq" @@ -31,6 +30,16 @@ XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE = "immediate" XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED = "scheduled" +def _build_xhs_creator_url(path: str) -> str: + base_url = os.getenv( + XHS_CREATOR_BASE_URL_ENV, + XHS_DEFAULT_CREATOR_BASE_URL, + ).strip().rstrip("/") + if not base_url: + base_url = XHS_DEFAULT_CREATOR_BASE_URL + return f"{base_url}/{path.lstrip('/')}" + + def _msg(emoji: str, text: str) -> str: return f"{emoji} {text}" @@ -132,7 +141,7 @@ async def _save_xhs_qrcode( async def _is_xhs_login_completed(page: Page) -> bool: - if page.url.startswith(XHS_LOGIN_URL): + if page.url.startswith(_build_xhs_creator_url("/login")): return False login_box = page.locator(XHS_LOGIN_BOX_SELECTOR).first @@ -158,10 +167,14 @@ async def cookie_auth(account_file): context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) page = await context.new_page() - await page.goto(XHS_PUBLISH_VIDEO_URL) + await page.goto( + _build_xhs_creator_url( + "/publish/publish?from=homepage&target=video" + ) + ) await page.wait_for_timeout(3000) - if page.url.startswith(XHS_LOGIN_URL): + if page.url.startswith(_build_xhs_creator_url("/login")): xiaohongshu_logger.info(_msg("🥹", "cookie 已失效,得重新登录一下")) return False @@ -228,7 +241,7 @@ async def xiaohongshu_cookie_gen( result = _build_login_result(False, "failed", "小红书登录失败", account_file) try: page = await context.new_page() - await page.goto(XHS_LOGIN_URL) + await page.goto(_build_xhs_creator_url("/login")) qrcode_info = await _save_xhs_qrcode(page, account_file, qrcode_callback=qrcode_callback) qrcode_path = Path(qrcode_info["image_path"]) xiaohongshu_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成")) @@ -527,8 +540,11 @@ class XiaoHongShuVideo(XiaoHongShuBaseUploader): async def upload_video_content(self, page: Page) -> None: xiaohongshu_logger.info(_msg("🏃", f"小人开始搬运视频: {self.title}.mp4")) xiaohongshu_logger.info(_msg("🧭", "小人正在赶往视频发布页")) - await page.goto(XHS_PUBLISH_VIDEO_URL) - await page.wait_for_url(XHS_PUBLISH_VIDEO_URL) + publish_url = _build_xhs_creator_url( + "/publish/publish?from=homepage&target=video" + ) + await page.goto(publish_url) + await page.wait_for_url(publish_url) await page.locator("div[class^='upload-content'] input[class='upload-input']").set_input_files(self.file_path) while True: @@ -588,7 +604,7 @@ class XiaoHongShuVideo(XiaoHongShuBaseUploader): else: await page.locator('button:has-text("发布")').click() await page.wait_for_url( - "https://creator.xiaohongshu.com/publish/success?**", + XHS_PUBLISH_SUCCESS_URL_PATTERN, timeout=3000 ) xiaohongshu_logger.success(_msg("🥳", "视频发布成功,小人开心收工")) @@ -672,8 +688,11 @@ class XiaoHongShuNote(XiaoHongShuBaseUploader): async def upload_note_content(self, page: Page) -> None: xiaohongshu_logger.info(_msg("🏃", f"小人开始搬运图文,共 {len(self.image_paths)} 张图片")) xiaohongshu_logger.info(_msg("🧭", "小人正在赶往图文发布页")) - await page.goto(XHS_PUBLISH_NOTE_URL) - await page.wait_for_url(XHS_PUBLISH_NOTE_URL) + publish_url = _build_xhs_creator_url( + "/publish/publish?from=homepage&target=image" + ) + await page.goto(publish_url) + await page.wait_for_url(publish_url) upload_input = page.locator('input[type="file"][accept*="image"]').first if not await upload_input.count(): From 51209a133183616f31e4bc86424934b7d30e812f Mon Sep 17 00:00:00 2001 From: changyandaoc Date: Sun, 14 Jun 2026 07:15:21 +0800 Subject: [PATCH 18/18] =?UTF-8?q?fix(douyin):=20cookie=5Fauth=20=E9=97=B4?= =?UTF-8?q?=E6=AD=87=E6=80=A7=E8=AF=AF=E5=88=A4=E5=A4=B1=E6=95=88=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=87=8D=E8=AF=953=E6=AC=A1+=E5=AE=BD=E6=9D=BE?= =?UTF-8?q?=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 有头校验仍会因页面慢/瞬时跳转让 wait_for_url(精确URL,5s) 超时误判, 导致 check 通过但 upload 前置校验失败。改为重试3次 + 等2.5s稳定 + 宽松判定(URL含 content/upload 且无登录文案)。 --- uploader/douyin_uploader/main.py | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 5a76a3c..c82adbf 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -49,28 +49,28 @@ def _build_login_result(success: bool, status: str, message: str, account_file: async def cookie_auth(account_file): - async with async_playwright() as playwright: - # 抖音无头会撞反爬墙→content/upload 跳登录→误判 cookie 失效(间歇性)。校验必须有头。 - browser = await playwright.chromium.launch( - headless=False, channel="chrome", - args=["--no-sandbox", "--disable-blink-features=AutomationControlled"], - ) - try: - context = await browser.new_context(storage_state=account_file) - context = await set_init_script(context) - page = await context.new_page() - await page.goto("https://creator.douyin.com/creator-micro/content/upload", wait_until="domcontentloaded", timeout=90000) + # 抖音无头会撞反爬墙→content/upload 跳登录→误判 cookie 失效(间歇性)。校验必须有头。 + # 即便有头,页面慢/瞬时跳转仍会让 wait_for_url(精确URL,5s) 误判→重试3次+宽松判定(URL含 content/upload 且无登录文案)。 + for _attempt in range(3): + async with async_playwright() as playwright: + browser = await playwright.chromium.launch( + headless=False, channel="chrome", + args=["--no-sandbox", "--disable-blink-features=AutomationControlled"], + ) try: - await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) + context = await browser.new_context(storage_state=account_file) + context = await set_init_script(context) + page = await context.new_page() + await page.goto("https://creator.douyin.com/creator-micro/content/upload", wait_until="domcontentloaded", timeout=90000) + await page.wait_for_timeout(2500) # 等页面稳定,避免瞬时跳转误判 + has_login = await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count() + if "content/upload" in page.url and not has_login: + return True except Exception: - return False - - if await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count(): - return False - - return True - finally: - await browser.close() + pass + finally: + await browser.close() + return False async def douyin_setup(account_file, handle=False, return_detail=False, qrcode_callback=None, headless: bool = LOCAL_CHROME_HEADLESS):