""" Xiaohongshu (小红书) uploader using Playwright Based on social-auto-upload implementation """ from datetime import datetime from pathlib import Path from typing import Optional, List, Dict, Any import asyncio from playwright.async_api import Playwright, async_playwright from loguru import logger from .base_uploader import BaseUploader from .cookie_utils import set_init_script class XiaohongshuUploader(BaseUploader): """Xiaohongshu video uploader using Playwright""" # 超时配置 (秒) UPLOAD_TIMEOUT = 300 # 视频上传超时 PUBLISH_TIMEOUT = 120 # 发布检测超时 POLL_INTERVAL = 1 # 轮询间隔 def __init__( self, title: str, file_path: str, tags: List[str], publish_date: Optional[datetime] = None, account_file: Optional[str] = None, description: str = "" ): super().__init__(title, file_path, tags, publish_date, account_file, description) self.upload_url = "https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video" async def set_schedule_time(self, page, publish_date): """Set scheduled publish time""" try: logger.info("[小红书] 正在设置定时发布时间...") # Click "定时发布" label label_element = page.locator("label:has-text('定时发布')") await label_element.click() await asyncio.sleep(1) # Format time publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M") # Fill datetime input await page.locator('.el-input__inner[placeholder="选择日期和时间"]').click() await page.keyboard.press("Control+KeyA") await page.keyboard.type(str(publish_date_hour)) await page.keyboard.press("Enter") await asyncio.sleep(1) logger.info(f"[小红书] 已设置定时发布: {publish_date_hour}") except Exception as e: logger.error(f"[小红书] 设置定时发布失败: {e}") async def upload(self, playwright: Playwright) -> dict: """Main upload logic with guaranteed resource cleanup""" browser = None context = None try: # Launch browser (headless for server deployment) browser = await playwright.chromium.launch(headless=True) context = await browser.new_context( viewport={"width": 1600, "height": 900}, storage_state=self.account_file ) context = await set_init_script(context) page = await context.new_page() # Go to upload page await page.goto(self.upload_url) logger.info(f"[小红书] 正在上传: {self.file_path.name}") # Upload video file await page.locator("div[class^='upload-content'] input[class='upload-input']").set_input_files(str(self.file_path)) # Wait for upload to complete (with timeout) import time upload_start = time.time() while time.time() - upload_start < self.UPLOAD_TIMEOUT: try: upload_input = await page.wait_for_selector('input.upload-input', timeout=3000) preview_new = await upload_input.query_selector( 'xpath=following-sibling::div[contains(@class, "preview-new")]' ) if preview_new: stage_elements = await preview_new.query_selector_all('div.stage') upload_success = False for stage in stage_elements: text_content = await page.evaluate('(element) => element.textContent', stage) if '上传成功' in text_content: upload_success = True break if upload_success: logger.info("[小红书] 检测到上传成功标识") break else: logger.info("[小红书] 未找到上传成功标识,继续等待...") else: logger.info("[小红书] 未找到预览元素,继续等待...") await asyncio.sleep(self.POLL_INTERVAL) except Exception as e: logger.info(f"[小红书] 检测过程: {str(e)},重新尝试...") await asyncio.sleep(0.5) else: logger.error("[小红书] 视频上传超时") return { "success": False, "message": "视频上传超时", "url": None } # Fill title and tags await asyncio.sleep(1) logger.info("[小红书] 正在填充标题和话题...") title_container = page.locator('div.plugin.title-container').locator('input.d-text') if await title_container.count(): await title_container.fill(self.title[:30]) # Add tags css_selector = ".tiptap" for tag in self.tags: await page.type(css_selector, "#" + tag) await page.press(css_selector, "Space") logger.info(f"[小红书] 总共添加 {len(self.tags)} 个话题") # Set scheduled publish time if needed if self.publish_date != 0: await self.set_schedule_time(page, self.publish_date) # Click publish button (with timeout) publish_start = time.time() while time.time() - publish_start < self.PUBLISH_TIMEOUT: try: if self.publish_date != 0: await page.locator('button:has-text("定时发布")').click() else: await page.locator('button:has-text("发布")').click() await page.wait_for_url( "https://creator.xiaohongshu.com/publish/success?**", timeout=3000 ) logger.success("[小红书] 视频发布成功") break except Exception: logger.info("[小红书] 视频正在发布中...") await asyncio.sleep(0.5) else: logger.warning("[小红书] 发布检测超时,请手动确认") # Save updated cookies await context.storage_state(path=self.account_file) logger.success("[小红书] Cookie 更新完毕") await asyncio.sleep(2) return { "success": True, "message": "发布成功,待审核" if self.publish_date == 0 else "已设置定时发布", "url": None } except Exception as e: logger.exception(f"[小红书] 上传失败: {e}") return { "success": False, "message": f"上传失败: {str(e)}", "url": None } finally: # 确保资源释放 if context: try: await context.close() except Exception: pass if browser: try: await browser.close() except Exception: pass async def main(self) -> Dict[str, Any]: """Execute upload""" async with async_playwright() as playwright: return await self.upload(playwright)