Repository: dreammis/social-auto-upload Branch: main Commit: b8b3107881f2 Files: 86 Total size: 313.5 KB Directory structure: gitextract_x6olz_e2/ ├── .dockerignore ├── .gitignore ├── CLAUDE.md ├── Dockerfile ├── README.md ├── __init__.py ├── cli_main.py ├── conf.example.py ├── db/ │ └── createTable.py ├── examples/ │ ├── __init__.py │ ├── get_baijiahao_cookie.py │ ├── get_bilibili_cookie.py │ ├── get_douyin_cookie.py │ ├── get_kuaishou_cookie.py │ ├── get_tencent_cookie.py │ ├── get_tk_cookie.py │ ├── get_xiaohongshu_cookie.py │ ├── upload_video_to_baijiahao.py │ ├── upload_video_to_bilibili.py │ ├── upload_video_to_douyin.py │ ├── upload_video_to_kuaishou.py │ ├── upload_video_to_tencent.py │ ├── upload_video_to_tiktok.py │ ├── upload_video_to_xhs.py │ └── upload_video_to_xiaohongshu.py ├── myUtils/ │ ├── __init__.py │ ├── auth.py │ ├── login.py │ └── postVideo.py ├── requirements.txt ├── sau_backend/ │ └── README.md ├── sau_backend.py ├── sau_frontend/ │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── api/ │ │ │ ├── account.js │ │ │ ├── index.js │ │ │ ├── material.js │ │ │ └── user.js │ │ ├── main.js │ │ ├── router/ │ │ │ └── index.js │ │ ├── stores/ │ │ │ ├── account.js │ │ │ ├── app.js │ │ │ ├── index.js │ │ │ └── user.js │ │ ├── styles/ │ │ │ ├── index.scss │ │ │ ├── reset.scss │ │ │ └── variables.scss │ │ ├── utils/ │ │ │ └── request.js │ │ └── views/ │ │ ├── About.vue │ │ ├── AccountManagement.vue │ │ ├── Dashboard.vue │ │ ├── MaterialManagement.vue │ │ └── PublishCenter.vue │ └── vite.config.js ├── start-win.bat ├── uploader/ │ ├── __init__.py │ ├── baijiahao_uploader/ │ │ ├── __init__.py │ │ └── main.py │ ├── bilibili_uploader/ │ │ ├── __init__.py │ │ └── main.py │ ├── douyin_uploader/ │ │ ├── __init__.py │ │ └── main.py │ ├── ks_uploader/ │ │ ├── __init__.py │ │ └── main.py │ ├── tencent_uploader/ │ │ ├── __init__.py │ │ └── main.py │ ├── tk_uploader/ │ │ ├── __init__.py │ │ ├── main.py │ │ ├── main_chrome.py │ │ └── tk_config.py │ ├── xhs_uploader/ │ │ ├── __init__.py │ │ ├── accounts.ini │ │ ├── main.py │ │ └── xhs_login_qrcode.py │ └── xiaohongshu_uploader/ │ ├── __init__.py │ └── main.py ├── utils/ │ ├── __init__.py │ ├── base_social_media.py │ ├── browser_hook.py │ ├── constant.py │ ├── files_times.py │ ├── log.py │ └── network.py └── videos/ └── demo.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .venv .git .idea cookies conf.py sau_frontend/dist sau_frontend/node_modules sau_frontend/package-lock.json ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ .DS_Store # ignore cookie file tencent_uploader/*.json youtube_uploader/*.json douyin_uploader/*.json bilibili_uploader/*.json tk_uploader/*.json # 配置文件 conf.py # 账号文件 cookies # Frontend .vite/ dist/ node_modules/ package-lock.json # database db/database.db # 临时文件夹 cookiesFile uploadFile videoFile ================================================ FILE: CLAUDE.md ================================================ ## Project Overview This project, `social-auto-upload`, is a powerful automation tool designed to help content creators and operators efficiently publish video content to multiple domestic and international mainstream social media platforms in one click. The project implements video upload, scheduled release and other functions for platforms such as `Douyin`, `Bilibili`, `Xiaohongshu`, `Kuaishou`, `WeChat Channel`, `Baijiahao` and `TikTok`. The project consists of a Python backend and a Vue.js frontend. **Backend:** * Framework: Flask * Core Functionality: * Handles file uploads and management. * Interacts with a SQLite database to store information about files and user accounts. * Uses `playwright` for browser automation to interact with social media platforms. * Provides a RESTful API for the frontend to consume. * Uses Server-Sent Events (SSE) for real-time communication with the frontend during the login process. **Frontend:** * Framework: Vue.js * Build Tool: Vite * UI Library: Element Plus * State Management: Pinia * Routing: Vue Router * Core Functionality: * Provides a web interface for managing social media accounts, video files, and publishing videos. * Communicates with the backend via a RESTful API. **Command-line Interface:** The project also provides a command-line interface (CLI) for users who prefer to work from the terminal. The CLI supports two main actions: * `login`: To log in to a social media platform. * `upload`: To upload a video to a social media platform, with an option to schedule the upload. ## Building and Running ### Backend 1. **Install dependencies:** ```bash pip install -r requirements.txt ``` 2. **Install Playwright browser drivers:** ```bash playwright install chromium ``` 3. **Initialize the database:** ```bash python db/createTable.py ``` 4. **Run the backend server:** ```bash python sau_backend.py ``` The backend server will start on `http://localhost:5409`. ### Frontend 1. **Navigate to the frontend directory:** ```bash cd sau_frontend ``` 2. **Install dependencies:** ```bash npm install ``` 3. **Run the development server:** ```bash npm run dev ``` The frontend development server will start on `http://localhost:5173`. ### Command-line Interface To use the CLI, you can run the `cli_main.py` script with the appropriate arguments. **Login:** ```bash python cli_main.py login ``` **Upload:** ```bash python cli_main.py upload [-pt {0,1}] [-t YYYY-MM-DD HH:MM] ``` ## Development Conventions * The backend code is located in the root directory and the `myUtils` and `uploader` directories. * The frontend code is located in the `sau_frontend` directory. * The project uses a SQLite database for data storage. The database file is located at `db/database.db`. * The `conf.example.py` file should be copied to `conf.py` and configured with the appropriate settings. * The `requirements.txt` file lists the Python dependencies. * The `package.json` file in the `sau_frontend` directory lists the frontend dependencies. ================================================ FILE: Dockerfile ================================================ FROM node:22.21.1 AS builder WORKDIR /app RUN npm config set registry https://registry.npmmirror.com COPY sau_frontend . RUN npm install ENV NODE_ENV=production ENV PATH=/app/node_modules/.bin:$PATH # 替换前端中的地址 RUN sed -i 's#\${baseUrl}##g' /app/src/views/AccountManagement.vue RUN sed -i "s#\${import\.meta\.env\.VITE_API_BASE_URL || 'http:\/\/localhost:5409'}##g" /app/src/api/material.js RUN sed -i 's#localhost:5409##g' /app/.env.production RUN npm run build FROM python:3.10.19 WORKDIR /app ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright RUN apt-get update && apt-get install -y --no-install-recommends libnss3 \ libnspr4 \ libdbus-1-3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libatspi2.0-0 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libxkbcommon0 \ libasound2 && rm -rf /var/lib/apt/lists/* RUN pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple COPY requirements.txt requirements.txt RUN pip install -r requirements.txt RUN playwright install chromium-headless-shell COPY . . COPY --from=builder /app/dist/index.html /app COPY --from=builder /app/dist/assets /app/assets COPY --from=builder /app/dist/vite.svg /app/assets RUN cp conf.example.py conf.py RUN mkdir -p /app/videoFile RUN mkdir -p /app/cookiesFile EXPOSE 5409 CMD ["python", "sau_backend.py"] ================================================ FILE: README.md ================================================ # social-auto-upload `social-auto-upload` 是一个强大的自动化工具,旨在帮助内容创作者和运营者高效地将视频内容一键发布到多个国内外主流社交媒体平台。 项目实现了对 `抖音`、`Bilibili`、`小红书`、`快手`、`视频号`、`百家号` 以及 `TikTok` 等平台的视频上传、定时发布等功能。 结合各平台 `uploader` 模块,您可以轻松配置和扩展支持的平台,并通过示例脚本快速上手。 tiktok show ## 目录 - [💡 功能特性](#💡功能特性) - [🚀 支持的平台](#🚀支持的平台) - [💾 安装指南](#💾安装指南) - [🏁 快速开始](#🏁快速开始) - [🐇 项目背景](#🐇项目背景) - [📃 详细文档](#📃详细文档) - [🐾 交流与支持](#🐾交流与支持) - [🤝 贡献指南](#🤝贡献指南) - [📜 许可证](#📜许可证) - [⭐ Star History](#⭐Star-History) ## 💡功能特性 ### 已支持平台 - **国内平台**: - [x] 抖音 - [x] 视频号 - [x] Bilibili - [x] 小红书 - [x] 快手 - [x] 百家号 - **国外平台**: - [x] TikTok ### 核心功能 - [x] 定时上传 (Cron Job / Scheduled Upload) - [ ] Cookie 管理 (部分实现,持续优化中) - [ ] 国外平台 Proxy 设置 (部分实现) ### 计划支持与开发中 - **平台扩展**: - [ ] YouTube - **功能增强**: - [x] 更易用的版本 (GUI / CLI 交互优化) - [x] API 封装 - [x] Docker 部署 - [ ] 自动化上传 (更智能的调度策略) - [ ] 多线程/异步上传优化 - [ ] Slack/消息推送通知 ### 2025.10.30目前现状 该项目本人很长一段时间没维护了,有比较大的问题也是能简单快速修复就修复掉 因为我自己也在创业,每天时间都用不完 目前问题主要集中在 1. 小红书部分,这部分是直接适用xhs这个库来实现的 2. web 端(vue版本),这个版本是群友LeeDebug他帮忙做的(再次感谢他) 因为我日常也在用,我用的不是web端,而是最初`uploader`文件夹里的版本,也就是文档里提到的部分https://sap-doc.nasdaddy.com/ 所以这里一般遇到的问题,我都会尝试去解决,一并推送到该仓库 目前能遇到的问题,基本上都比较小,可能是元素变化导致的 在初期设计的时候,其实我已经参考了某些不可变元素去选择,极大的避免了后期因为平台页面修改导致的元素变化 该项目不仅仅是技术人员,有不少是非技术的从业人员,他们是没能力修复一个简单弱小的bug 为了能帮助更多的人,所以呼吁**技术小伙伴** 如果大家 - 修复了一些bug - 增加一些对大家有帮助的功能 请积极的提出pr,我会想尽可能的确认后合并的,在此感谢大家对于开源项目的支持,帮助更多的人 我自己也会尽100%的力量,在自己项目稳定后,修bug,加更多的平台,开发出gradio版本(更易部署),大家谅解 --- ## 🚀支持的平台 本项目通过各平台对应的 `uploader` 模块实现视频上传功能。您可以在 `examples` 目录下找到各个平台的使用示例脚本。 每个示例脚本展示了如何配置和调用相应的 uploader。 ## 💾安装指南 1. **克隆项目**: ```bash git clone https://github.com/dreammis/social-auto-upload.git cd social-auto-upload ``` 2. **安装依赖**: 建议在虚拟环境中安装依赖。 ```bash conda create -n social-auto-upload python=3.10 conda activate social-auto-upload # 挂载清华镜像 or 命令行代理 pip install -r requirements.txt ``` 3. **安装 Playwright 浏览器驱动**: ```bash playwright install chromium firefox ``` 根据您的需求,至少需要安装 `chromium`。`firefox` 主要用于 TikTok 上传(旧版)。 4. **修改配置文件**: 复制 `conf.example.py` 并重命名为 `conf.py`。 在 `conf.py` 中,您需要配置以下内容: - `LOCAL_CHROME_PATH`: 本地 Chrome 浏览器的路径,比如 `C:\Program Files\Google\Chrome\Application\chrome.exe` 保存。 **临时解决方案** 需要在根目录创建 `cookiesFile` 和 `videoFile` 两个文件夹,分别是 存储cookie文件 和 存储上传文件 的文件夹 5. **配置数据库**: 如果 db/database.db 文件不存在,您可以运行以下命令来初始化数据库: ```bash cd db python createTable.py ``` 此命令将初始化 SQLite 数据库。 6. **启动后端项目**: ```bash python sau_backend.py ``` 后端项目将在 `http://localhost:5409` 启动。 7. **启动前端项目**: ```bash cd sau_frontend npm install npm run dev ``` 前端项目将在 `http://localhost:5173` 启动,在浏览器中打开此链接即可访问。 > 非程序员用户可以参考:[新手级教程](https://juejin.cn/post/7372114027840208911) ## 🏁快速开始 1. **准备 Cookie**: 大多数平台需要登录后的 Cookie 信息才能进行操作。请参照 examples 目录下各 `get_xxx_cookie.py` 脚本(例如 get_douyin_cookie.py, get_ks_cookie.py)的说明,运行脚本以生成并保存 Cookie 文件(通常在 `cookies/[PLATFORM]_uploader/account.json`)。 2. **准备视频文件**: 将需要上传的视频文件(通常为 `.mp4` 格式)放置在 videos 目录下。 部分平台支持视频封面,可以将封面图片(例如 `.png` 格式,与视频同名)也放在此目录。 如果需要上传标题及标签,请在视频文件旁边创建一个同名的 `.txt` 文件,内容为标题和标签,以换行分隔。 3. **修改并运行示例脚本**: 打开 examples 目录中您想使用的平台的上传脚本(例如 upload_video_to_douyin.py)。 - 根据脚本内的注释和说明,确认 Cookie 文件路径、视频文件路径等配置是否正确。 - 您可以修改脚本以适应您的具体需求,例如批量上传、自定义标题、标签等。 4. **执行上传**: 运行修改后的示例脚本,例如: ```bash python examples/upload_video_to_douyin.py ``` ## Docker 环境 ### 自己构建镜像 1. **构建Docker镜像**: ``` docker build -t social-auto-upload:latest . ``` 2. **运行Docker容器**: ``` docker run -d -it -p 5409:5409 social-auto-upload:latest ``` ### 使用预构建镜像 1. **拉取镜像**: ``` docker pull gzxy/social-auto-upload:latest ``` 2. **运行Docker容器**: ``` docker run -d -it -p 5409:5409 gzxy/social-auto-upload:latest ``` 启动容器后访问:[http://localhost:5409](http://localhost:5409) ## 🐇项目背景 该项目最初是我个人用于自动化管理社交媒体视频发布的工具。我的主要发布策略是提前一天设置定时发布,因此项目中很多定时发布相关的逻辑是基于“第二天”的时间进行计算的。 如果您需要立即发布或其他定制化的发布策略,欢迎研究源码或在社区提问。 ## 📃详细文档 更详细的文档和说明,请查看:[social-auto-upload 官方文档](https://sap-doc.nasdaddy.com/) ## 🐾交流与支持 [☕ Donate as u like](https://www.buymeacoffee.com/hysn2001m) - 如果您觉得这个项目对您有帮助,可以考虑赞助。 如果您也是独立开发者、技术爱好者,对 #技术变现 #AI创业 #跨境电商 #自动化工具 #视频创作 等话题感兴趣,欢迎加入社群交流。 ### Creator
NasDaddy公众号
微信公众号

💻
关注公众号,后台回复 `上传` 获取加群方式
赞赏码/入群引导
交流群 (通过公众号获取)

📖
如果您觉得项目有用,可以考虑打赏支持一下
### Active Core Team
Edan Lee
Edan Lee

💻 📖
封装了 api 接口和 web 前端管理界面
(请注明来意:进群、学习、企业咨询等)
## 🤝贡献指南 欢迎各种形式的贡献,包括但不限于: - 提交 Bug报告 和 Feature请求。 - 改进代码、文档。 - 分享使用经验和教程。 如果您希望贡献代码,请遵循以下步骤: 1. Fork 本仓库。 2. 创建一个新的分支 (`git checkout -b feature/YourFeature` 或 `bugfix/YourBugfix`)。 3. 提交您的更改 (`git commit -m 'Add some feature'`)。 4. Push到您的分支 (`git push origin feature/YourFeature`)。 5. 创建一个 Pull Request。 ## 📜许可证 本项目暂时采用 [MIT License](LICENSE) 开源许可证。 ## ⭐Star-History > 如果这个项目对您有帮助,请给一个 ⭐ Star 以表示支持! [![Star History Chart](https://api.star-history.com/svg?repos=dreammis/social-auto-upload&type=Date)](https://star-history.com/#dreammis/social-auto-upload&Date) ================================================ FILE: __init__.py ================================================ ================================================ FILE: cli_main.py ================================================ import argparse import asyncio from datetime import datetime from os.path import exists from pathlib import Path from conf import BASE_DIR from uploader.douyin_uploader.main import douyin_setup, DouYinVideo from uploader.ks_uploader.main import ks_setup, KSVideo from uploader.tencent_uploader.main import weixin_setup, TencentVideo from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \ SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU from utils.constant import TencentZoneTypes from utils.files_times import get_title_and_hashtags def parse_schedule(schedule_raw): if schedule_raw: schedule = datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M') else: schedule = None return schedule async def main(): # 主解析器 parser = argparse.ArgumentParser(description="Upload video to multiple social-media.") parser.add_argument("platform", metavar='platform', choices=get_supported_social_media(), help="Choose social-media platform: douyin tencent tiktok kuaishou") parser.add_argument("account_name", type=str, help="Account name for the platform: xiaoA") subparsers = parser.add_subparsers(dest="action", metavar='action', help="Choose action", required=True) actions = get_cli_action() for action in actions: action_parser = subparsers.add_parser(action, help=f'{action} operation') if action == 'login': # Login 不需要额外参数 continue elif action == 'upload': action_parser.add_argument("video_file", help="Path to the Video file") action_parser.add_argument("-pt", "--publish_type", type=int, choices=[0, 1], help="0 for immediate, 1 for scheduled", default=0) action_parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format') # 解析命令行参数 args = parser.parse_args() # 参数校验 if args.action == 'upload': if not exists(args.video_file): raise FileNotFoundError(f'Could not find the video file at {args["video_file"]}') if args.publish_type == 1 and not args.schedule: parser.error("The schedule must must be specified for scheduled publishing.") account_file = Path(BASE_DIR / "cookies" / f"{args.platform}_{args.account_name}.json") account_file.parent.mkdir(exist_ok=True) # 根据 action 处理不同的逻辑 if args.action == 'login': print(f"Logging in with account {args.account_name} on platform {args.platform}") if args.platform == SOCIAL_MEDIA_DOUYIN: await douyin_setup(str(account_file), handle=True) elif args.platform == SOCIAL_MEDIA_TIKTOK: await tiktok_setup(str(account_file), handle=True) elif args.platform == SOCIAL_MEDIA_TENCENT: await weixin_setup(str(account_file), handle=True) elif args.platform == SOCIAL_MEDIA_KUAISHOU: await ks_setup(str(account_file), handle=True) elif args.action == 'upload': title, tags = get_title_and_hashtags(args.video_file) video_file = args.video_file if args.publish_type == 0: print("Uploading immediately...") publish_date = 0 else: print("Scheduling videos...") publish_date = parse_schedule(args.schedule) if args.platform == SOCIAL_MEDIA_DOUYIN: await douyin_setup(account_file, handle=False) app = DouYinVideo(title, video_file, tags, publish_date, account_file) elif args.platform == SOCIAL_MEDIA_TIKTOK: await tiktok_setup(account_file, handle=True) app = TiktokVideo(title, video_file, tags, publish_date, account_file) elif args.platform == SOCIAL_MEDIA_TENCENT: await weixin_setup(account_file, handle=True) category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传 app = TencentVideo(title, video_file, tags, publish_date, account_file, category) elif args.platform == SOCIAL_MEDIA_KUAISHOU: await ks_setup(account_file, handle=True) app = KSVideo(title, video_file, tags, publish_date, account_file) else: print("Wrong platform, please check your input") exit() await app.main() if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: conf.example.py ================================================ from pathlib import Path BASE_DIR = Path(__file__).parent.resolve() XHS_SERVER = "http://127.0.0.1:11901" LOCAL_CHROME_PATH = "" # change me necessary! for example C:/Program Files/Google/Chrome/Application/chrome.exe LOCAL_CHROME_HEADLESS = False ================================================ FILE: db/createTable.py ================================================ import sqlite3 import json import os # 数据库文件路径(如果不存在会自动创建) db_file = './database.db' # 如果数据库已存在,则删除旧的表(可选) # if os.path.exists(db_file): # os.remove(db_file) # 连接到SQLite数据库(如果文件不存在则会自动创建) conn = sqlite3.connect(db_file) cursor = conn.cursor() # 创建账号记录表 cursor.execute(''' CREATE TABLE IF NOT EXISTS user_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, filePath TEXT NOT NULL, -- 存储文件路径 userName TEXT NOT NULL, status INTEGER DEFAULT 0 ) ''') # 创建文件记录表 cursor.execute('''CREATE TABLE IF NOT EXISTS file_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 唯一标识每条记录 filename TEXT NOT NULL, -- 文件名 filesize REAL, -- 文件大小(单位:MB) upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 上传时间,默认当前时间 file_path TEXT -- 文件路径 ) ''') # 提交更改 conn.commit() print("✅ 表创建成功") # 关闭连接 conn.close() ================================================ FILE: examples/__init__.py ================================================ ================================================ FILE: examples/get_baijiahao_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.baijiahao_uploader.main import baijiahao_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(baijiahao_setup(str(account_file), handle=True)) ================================================ FILE: examples/get_bilibili_cookie.py ================================================ # cd uploader/bilibili_uploader # biliup.exe -u account.json login ================================================ FILE: examples/get_douyin_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.douyin_uploader.main import douyin_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(douyin_setup(str(account_file), handle=True)) ================================================ FILE: examples/get_kuaishou_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.ks_uploader.main import ks_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(ks_setup(str(account_file), handle=True)) ================================================ FILE: examples/get_tencent_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.tencent_uploader.main import weixin_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(weixin_setup(str(account_file), handle=True)) ================================================ FILE: examples/get_tk_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.tk_uploader.main_chrome import tiktok_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(tiktok_setup(str(account_file), handle=True)) ================================================ FILE: examples/get_xiaohongshu_cookie.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.xiaohongshu_uploader.main import xiaohongshu_setup if __name__ == '__main__': account_file = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "account.json") account_file.parent.mkdir(exist_ok=True) cookie_setup = asyncio.run(xiaohongshu_setup(str(account_file), handle=True)) ================================================ FILE: examples/upload_video_to_baijiahao.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.baijiahao_uploader.main import baijiahao_setup, BaiJiaHaoVideo from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json") # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(baijiahao_setup(account_file, handle=False)) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) thumbnail_path = file.with_suffix('.png') # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = BaiJiaHaoVideo(title, file, tags, publish_datetimes[index], account_file) asyncio.run(app.main(), debug=False) ================================================ FILE: examples/upload_video_to_bilibili.py ================================================ import time from pathlib import Path from uploader.bilibili_uploader.main import read_cookie_json_file, extract_keys_from_json, random_emoji, BilibiliUploader from conf import BASE_DIR from utils.constant import VideoZoneTypes from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" # how to get cookie, see the file of get_bilibili_cookie.py. account_file = Path(BASE_DIR / "cookies" / "bilibili_uploader" / "account.json") if not account_file.exists(): print(f"{account_file.name} 配置文件不存在") exit() cookie_data = read_cookie_json_file(account_file) cookie_data = extract_keys_from_json(cookie_data) tid = VideoZoneTypes.SPORTS_FOOTBALL.value # 设置分区id # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) timestamps = generate_schedule_time_next_day(file_num, 1, daily_times=[16], timestamps=True) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) # just avoid error, bilibili don't allow same title of video. title += random_emoji() tags_str = ','.join([tag for tag in tags]) # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") # I set desc same as title, do what u like. desc = title bili_uploader = BilibiliUploader(cookie_data, file, title, desc, tid, tags, timestamps[index]) bili_uploader.upload() # life is beautiful don't so rush. be kind be patience time.sleep(30) ================================================ FILE: examples/upload_video_to_douyin.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.douyin_uploader.main import douyin_setup, DouYinVideo from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json") # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(douyin_setup(account_file, handle=False)) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) thumbnail_path = file.with_suffix('.png') # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") # 暂时没有时间修复封面上传,故先隐藏掉该功能 # if thumbnail_path.exists(): # app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path) # else: app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file) asyncio.run(app.main(), debug=False) ================================================ FILE: examples/upload_video_to_kuaishou.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.ks_uploader.main import ks_setup, KSVideo from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json") # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(ks_setup(account_file, handle=False)) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = KSVideo(title, file, tags, publish_datetimes[index], account_file) asyncio.run(app.main(), debug=False) ================================================ FILE: examples/upload_video_to_tencent.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.tencent_uploader.main import weixin_setup, TencentVideo from utils.constant import TencentZoneTypes from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json") # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(weixin_setup(account_file, handle=True)) category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传 for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = TencentVideo(title, file, tags, publish_datetimes[index], account_file, category) asyncio.run(app.main(), debug=False) ================================================ FILE: examples/upload_video_to_tiktok.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR # from tk_uploader.main import tiktok_setup, TiktokVideo from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json") folder_path = Path(filepath) # get video files from folder files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(tiktok_setup(account_file, handle=True)) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) thumbnail_path = file.with_suffix('.png') print(f"video_file_name:{file}") print(f"video_title:{title}") print(f"video_hashtag:{tags}") if thumbnail_path.exists(): print(f"thumbnail_file_name:{thumbnail_path}") app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path) else: app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file) asyncio.run(app.main(), debug=False) ================================================ FILE: examples/upload_video_to_xhs.py ================================================ import configparser from pathlib import Path from time import sleep from xhs import XhsClient from conf import BASE_DIR from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags from uploader.xhs_uploader.main import sign_local, beauty_print config = configparser.RawConfigParser() config.read(Path(BASE_DIR / "uploader" / "xhs_uploader" / "accounts.ini")) if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) cookies = config['account1']['cookies'] xhs_client = XhsClient(cookies, sign=sign_local, timeout=60) # auth cookie # 注意:该校验cookie方式可能并没那么准确 try: xhs_client.get_video_first_frame_image_id("3214") except: print("cookie 失效") exit() publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) # 加入到标题 补充标题(xhs 可以填1000字不写白不写) tags_str = ' '.join(['#' + tag for tag in tags]) hash_tags_str = '' hash_tags = [] # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") topics = [] # 获取hashtag for i in tags[:3]: topic_official = xhs_client.get_suggest_topic(i) if topic_official: topic_official[0]['type'] = 'topic' topic_one = topic_official[0] hash_tag_name = topic_one['name'] hash_tags.append(hash_tag_name) topics.append(topic_one) hash_tags_str = ' ' + ' '.join(['#' + tag + '[话题]#' for tag in hash_tags]) note = xhs_client.create_video_note(title=title[:20], video_path=str(file), desc=title + tags_str + hash_tags_str, topics=topics, is_private=False, post_time=publish_datetimes[index].strftime("%Y-%m-%d %H:%M:%S")) beauty_print(note) # 强制休眠30s,避免风控(必要) sleep(30) ================================================ FILE: examples/upload_video_to_xiaohongshu.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.xiaohongshu_uploader.main import xiaohongshu_setup, XiaoHongShuVideo from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags if __name__ == '__main__': filepath = Path(BASE_DIR) / "videos" account_file = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "58a391ba-4082-11f0-a321-44e51723d63c.json") # 获取视频目录 folder_path = Path(filepath) # 获取文件夹中的所有文件 files = list(folder_path.glob("*.mp4")) file_num = len(files) publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) cookie_setup = asyncio.run(xiaohongshu_setup(account_file, handle=False)) for index, file in enumerate(files): title, tags = get_title_and_hashtags(str(file)) thumbnail_path = file.with_suffix('.png') # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") # 暂时没有时间修复封面上传,故先隐藏掉该功能 # if thumbnail_path.exists(): # app = XiaoHongShuVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path) # else: app = XiaoHongShuVideo(title, file, tags, 0, account_file) asyncio.run(app.main(), debug=False) ================================================ FILE: myUtils/__init__.py ================================================ ================================================ FILE: myUtils/auth.py ================================================ import asyncio import configparser import os from playwright.async_api import async_playwright from xhs import XhsClient from conf import BASE_DIR, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.log import tencent_logger, kuaishou_logger, douyin_logger from pathlib import Path from uploader.xhs_uploader.main import sign_local async def cookie_auth_douyin(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.douyin.com/creator-micro/content/upload") try: await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) # 2024.06.17 抖音创作者中心改版 # 判断 # 等待“扫码登录”元素出现,超时 5 秒(如果 5 秒没出现,说明 cookie 有效) try: await page.get_by_text("扫码登录").wait_for(timeout=5000) douyin_logger.error("[+] cookie 失效,需要扫码登录") return False except: douyin_logger.success("[+] cookie 有效") return True except: douyin_logger.error("[+] 等待5秒 cookie 失效") await context.close() await browser.close() return False async def cookie_auth_tencent(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://channels.weixin.qq.com/platform/post/create") try: await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒 tencent_logger.error("[+] 等待5秒 cookie 失效") return False except: tencent_logger.success("[+] cookie 有效") return True async def cookie_auth_ks(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://cp.kuaishou.com/article/publish/video") try: await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒 kuaishou_logger.info("[+] 等待5秒 cookie 失效") return False except: kuaishou_logger.success("[+] cookie 有效") return True async def cookie_auth_xhs(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.xiaohongshu.com/creator-micro/content/upload") try: await page.wait_for_url("https://creator.xiaohongshu.com/creator-micro/content/upload", timeout=5000) except: print("[+] 等待5秒 cookie 失效") await context.close() await browser.close() return False # 2024.06.17 抖音创作者中心改版 if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): print("[+] 等待5秒 cookie 失效") return False else: print("[+] cookie 有效") return True async def check_cookie(type, file_path): match type: # 小红书 case 1: return await cookie_auth_xhs(Path(BASE_DIR / "cookiesFile" / file_path)) # 视频号 case 2: return await cookie_auth_tencent(Path(BASE_DIR / "cookiesFile" / file_path)) # 抖音 case 3: return await cookie_auth_douyin(Path(BASE_DIR / "cookiesFile" / file_path)) # 快手 case 4: return await cookie_auth_ks(Path(BASE_DIR / "cookiesFile" / file_path)) case _: return False # a = asyncio.run(check_cookie(1,"3a6cfdc0-3d51-11f0-8507-44e51723d63c.json")) # print(a) ================================================ FILE: myUtils/login.py ================================================ import asyncio import sqlite3 from playwright.async_api import async_playwright from myUtils.auth import check_cookie from utils.base_social_media import set_init_script import uuid from pathlib import Path from conf import BASE_DIR, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH # 统一获取浏览器启动配置(防风控+引入本地浏览器) def get_browser_options(): options = { 'headless': LOCAL_CHROME_HEADLESS, 'args': [ '--disable-blink-features=AutomationControlled', # 核心防爬屏蔽:去掉 window.navigator.webdriver 标签 '--lang=zh-CN', '--disable-infobars', '--start-maximized' ] } # 如果用户在 conf.py 里配置了本地 Chrome,就用本地的,这样成功率极高 if LOCAL_CHROME_PATH: options['executable_path'] = LOCAL_CHROME_PATH return options # 抖音登录 async def douyin_cookie_gen(id,status_queue): url_changed_event = asyncio.Event() async def on_url_change(): # 检查是否是主框架的变化 if page.url != original_url: url_changed_event.set() async with async_playwright() as playwright: options = get_browser_options() # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://creator.douyin.com/") original_url = page.url img_locator = page.get_by_role("img", name="二维码") # 获取 src 属性值 src = await img_locator.get_attribute("src") print("✅ 图片地址:", src) status_queue.put(src) # 监听页面的 'framenavigated' 事件,只关注主框架的变化 page.on('framenavigated', lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) try: # 等待 URL 变化或超时 await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 print("监听页面跳转成功") except asyncio.TimeoutError: print("监听页面跳转超时") await page.close() await context.close() await browser.close() status_queue.put("500") return None uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") # 确保cookiesFile目录存在 cookies_dir = Path(BASE_DIR / "cookiesFile") cookies_dir.mkdir(exist_ok=True) await context.storage_state(path=cookies_dir / f"{uuid_v1}.json") result = await check_cookie(3, f"{uuid_v1}.json") if not result: status_queue.put("500") await page.close() await context.close() await browser.close() return None await page.close() await context.close() await browser.close() with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO user_info (type, filePath, userName, status) VALUES (?, ?, ?, ?) ''', (3, f"{uuid_v1}.json", id, 1)) conn.commit() print("✅ 用户状态已记录") status_queue.put("200") # 视频号登录 async def get_tencent_cookie(id,status_queue): url_changed_event = asyncio.Event() async def on_url_change(): # 检查是否是主框架的变化 if page.url != original_url: url_changed_event.set() async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options # Pause the page, and start recording manually. context = await set_init_script(context) page = await context.new_page() await page.goto("https://channels.weixin.qq.com") original_url = page.url # 监听页面的 'framenavigated' 事件,只关注主框架的变化 page.on('framenavigated', lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) # 等待 iframe 出现(最多等 60 秒) iframe_locator = page.frame_locator("iframe").first # 获取 iframe 中的第一个 img 元素 img_locator = iframe_locator.get_by_role("img").first # 获取 src 属性值 src = await img_locator.get_attribute("src") print("✅ 图片地址:", src) status_queue.put(src) try: # 等待 URL 变化或超时 await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 print("监听页面跳转成功") except asyncio.TimeoutError: status_queue.put("500") print("监听页面跳转超时") await page.close() await context.close() await browser.close() return None uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") # 确保cookiesFile目录存在 cookies_dir = Path(BASE_DIR / "cookiesFile") cookies_dir.mkdir(exist_ok=True) await context.storage_state(path=cookies_dir / f"{uuid_v1}.json") result = await check_cookie(2,f"{uuid_v1}.json") if not result: status_queue.put("500") await page.close() await context.close() await browser.close() return None await page.close() await context.close() await browser.close() with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO user_info (type, filePath, userName, status) VALUES (?, ?, ?, ?) ''', (2, f"{uuid_v1}.json", id, 1)) conn.commit() print("✅ 用户状态已记录") status_queue.put("200") # 快手登录 async def get_ks_cookie(id,status_queue): url_changed_event = asyncio.Event() async def on_url_change(): # 检查是否是主框架的变化 if page.url != original_url: url_changed_event.set() async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://cp.kuaishou.com") # 定位并点击“立即登录”按钮(类型为 link) await page.get_by_role("link", name="立即登录").click() await page.get_by_text("扫码登录").click() img_locator = page.get_by_role("img", name="qrcode") # 获取 src 属性值 src = await img_locator.get_attribute("src") original_url = page.url print("✅ 图片地址:", src) status_queue.put(src) # 监听页面的 'framenavigated' 事件,只关注主框架的变化 page.on('framenavigated', lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) try: # 等待 URL 变化或超时 await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 print("监听页面跳转成功") except asyncio.TimeoutError: status_queue.put("500") print("监听页面跳转超时") await page.close() await context.close() await browser.close() return None uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") # 确保cookiesFile目录存在 cookies_dir = Path(BASE_DIR / "cookiesFile") cookies_dir.mkdir(exist_ok=True) await context.storage_state(path=cookies_dir / f"{uuid_v1}.json") result = await check_cookie(4, f"{uuid_v1}.json") if not result: status_queue.put("500") await page.close() await context.close() await browser.close() return None await page.close() await context.close() await browser.close() with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO user_info (type, filePath, userName, status) VALUES (?, ?, ?, ?) ''', (4, f"{uuid_v1}.json", id, 1)) conn.commit() print("✅ 用户状态已记录") status_queue.put("200") # 小红书登录 async def xiaohongshu_cookie_gen(id,status_queue): url_changed_event = asyncio.Event() async def on_url_change(): # 检查是否是主框架的变化 if page.url != original_url: url_changed_event.set() async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://creator.xiaohongshu.com/") await page.locator('img.css-wemwzq').click() img_locator = page.get_by_role("img").nth(2) # 获取 src 属性值 src = await img_locator.get_attribute("src") original_url = page.url print("✅ 图片地址:", src) status_queue.put(src) # 监听页面的 'framenavigated' 事件,只关注主框架的变化 page.on('framenavigated', lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) try: # 等待 URL 变化或超时 await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 print("监听页面跳转成功") except asyncio.TimeoutError: status_queue.put("500") print("监听页面跳转超时") await page.close() await context.close() await browser.close() return None uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") # 确保cookiesFile目录存在 cookies_dir = Path(BASE_DIR / "cookiesFile") cookies_dir.mkdir(exist_ok=True) await context.storage_state(path=cookies_dir / f"{uuid_v1}.json") result = await check_cookie(1, f"{uuid_v1}.json") if not result: status_queue.put("500") await page.close() await context.close() await browser.close() return None await page.close() await context.close() await browser.close() with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO user_info (type, filePath, userName, status) VALUES (?, ?, ?, ?) ''', (1, f"{uuid_v1}.json", id, 1)) conn.commit() print("✅ 用户状态已记录") status_queue.put("200") # a = asyncio.run(xiaohongshu_cookie_gen(4,None)) # print(a) ================================================ FILE: myUtils/postVideo.py ================================================ import asyncio from pathlib import Path from conf import BASE_DIR from uploader.douyin_uploader.main import DouYinVideo from uploader.ks_uploader.main import KSVideo from uploader.tencent_uploader.main import TencentVideo from uploader.xiaohongshu_uploader.main import XiaoHongShuVideo from utils.constant import TencentZoneTypes from utils.files_times import generate_schedule_time_next_day def post_video_tencent(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0, is_draft=False): # 生成文件的完整路径 account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] files = [Path(BASE_DIR / "videoFile" / file) for file in files] if enableTimer: publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) else: publish_datetimes = [0 for i in range(len(files))] for index, file in enumerate(files): for cookie in account_file: print(f"文件路径{str(file)}") # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = TencentVideo(title, str(file), tags, publish_datetimes[index], cookie, category, is_draft) asyncio.run(app.main(), debug=False) def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0, thumbnail_path = '', productLink = '', productTitle = ''): # 生成文件的完整路径 account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] files = [Path(BASE_DIR / "videoFile" / file) for file in files] if enableTimer: publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) else: publish_datetimes = [0 for i in range(len(files))] for index, file in enumerate(files): for cookie in account_file: print(f"文件路径{str(file)}") # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, thumbnail_path, productLink, productTitle) asyncio.run(app.main(), debug=False) def post_video_ks(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0): # 生成文件的完整路径 account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] files = [Path(BASE_DIR / "videoFile" / file) for file in files] if enableTimer: publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) else: publish_datetimes = [0 for i in range(len(files))] for index, file in enumerate(files): for cookie in account_file: print(f"文件路径{str(file)}") # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = KSVideo(title, str(file), tags, publish_datetimes[index], cookie) asyncio.run(app.main(), debug=False) def post_video_xhs(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0): # 生成文件的完整路径 account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] files = [Path(BASE_DIR / "videoFile" / file) for file in files] file_num = len(files) if enableTimer: publish_datetimes = generate_schedule_time_next_day(file_num, videos_per_day, daily_times,start_days) else: publish_datetimes = 0 for index, file in enumerate(files): for cookie in account_file: # 打印视频文件名、标题和 hashtag print(f"视频文件名:{file}") print(f"标题:{title}") print(f"Hashtag:{tags}") app = XiaoHongShuVideo(title, file, tags, publish_datetimes, cookie) asyncio.run(app.main(), debug=False) # post_video("333",["demo.mp4"],"d","d") # post_video_DouYin("333",["demo.mp4"],"d","d") ================================================ FILE: sau_backend/README.md ================================================ ## 启动项目: python 版本:3.10 1. 安装依赖 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 2. 删除 db 目录下 database.db(如果没有直接运行createTable.py即可),运行 createTable.py 重新建库,避免出现脏数据 3. 修改 conf.py最下方 LOCAL_CHROME_PATH 为本地 chrome 浏览器地址 4. 运行根目录的 sau_backend.py 5. type字段(平台标识) 1 小红书 2 视频号 3 抖音 4 快手 ## 接口说明 1. /upload post 上传接口,上传成功会返回文件的唯一id,后期靠这个发布视频 2. /login id参数 用户名 type参数 平台标识:登录流程,前端和后端建立sse连接,后端获取到图片base64编码后返回给前端,前端接受扫码后后端存库后返回200,前端主动断开连接,然后调取/getValidAccounts获取当前所有可用账号 3. /getValidAccounts 会获取当前所有可用cookie,时间较慢,会逐个校验cookie,status 1 有效 0 无效cookie 4. /postVideo 发布视频接口 post json传参 file_list /upload获取的文件唯一标识 account_list /getValidAccounts获取的filePath字段 type 类型字段(平台标识) title 视频标题 tags 视频tag 列表,不带# category 原作者说是原创表示,0表示不是原创其他表示为原创,但测试该字段没有效果 enableTimer 是否开启定时发布,默认关闭,开启传True,如果开启,下面三个必传,否则不传 videos_per_day 每天发布几个视频 daily_times 每天发布视频的时间,整形列表,与上面列表长度保持一致 start_days 开始天数,0 代表明天开始定时发布 1 代表明天的明天 以上三个字段是我的理解,不知道对不对,也不知道原作者为什么要这么设置 ## 数据库说明 见当前目录下 db目录,py文件是创建脚本,db文件是sqlite数据库 ## 文件说明 cookiesFile文件夹 存储cookie文件 myUtils文件夹 存储自己封装的python模块 videoFile文件夹 文件上传存放位置 web 文件夹 web路由目录 conf.py 全局配置,记得修改配置中 LOCAL_CHROME_PATH 为本机浏览器地址 ================================================ FILE: sau_backend.py ================================================ import asyncio import os import sqlite3 import threading import time import uuid from pathlib import Path from queue import Queue from flask_cors import CORS from myUtils.auth import check_cookie from flask import Flask, request, jsonify, Response, render_template, send_from_directory from conf import BASE_DIR from myUtils.login import get_tencent_cookie, douyin_cookie_gen, get_ks_cookie, xiaohongshu_cookie_gen from myUtils.postVideo import post_video_tencent, post_video_DouYin, post_video_ks, post_video_xhs active_queues = {} app = Flask(__name__) #允许所有来源跨域访问 CORS(app) # 限制上传文件大小为160MB app.config['MAX_CONTENT_LENGTH'] = 160 * 1024 * 1024 # 获取当前目录(假设 index.html 和 assets 在这里) current_dir = os.path.dirname(os.path.abspath(__file__)) # 处理所有静态资源请求(未来打包用) @app.route('/assets/') def custom_static(filename): return send_from_directory(os.path.join(current_dir, 'assets'), filename) # 处理 favicon.ico 静态资源(未来打包用) @app.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg') @app.route('/vite.svg') def vite_svg(): return send_from_directory(os.path.join(current_dir, 'assets'), 'vite.svg') # (未来打包用) @app.route('/') def index(): # put application's code here return send_from_directory(current_dir, 'index.html') @app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return jsonify({ "code": 400, "data": None, "msg": "No file part in the request" }), 400 file = request.files['file'] if file.filename == '': return jsonify({ "code": 400, "data": None, "msg": "No selected file" }), 400 try: # 保存文件到指定位置 uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{file.filename}") file.save(filepath) return jsonify({"code":200,"msg": "File uploaded successfully", "data": f"{uuid_v1}_{file.filename}"}), 200 except Exception as e: return jsonify({"code":500,"msg": str(e),"data":None}), 500 @app.route('/getFile', methods=['GET']) def get_file(): # 获取 filename 参数 filename = request.args.get('filename') if not filename: return jsonify({"code": 400, "msg": "filename is required", "data": None}), 400 # 防止路径穿越攻击 if '..' in filename or filename.startswith('/'): return jsonify({"code": 400, "msg": "Invalid filename", "data": None}), 400 # 拼接完整路径 file_path = str(Path(BASE_DIR / "videoFile")) # 返回文件 return send_from_directory(file_path,filename) @app.route('/uploadSave', methods=['POST']) def upload_save(): if 'file' not in request.files: return jsonify({ "code": 400, "data": None, "msg": "No file part in the request" }), 400 file = request.files['file'] if file.filename == '': return jsonify({ "code": 400, "data": None, "msg": "No selected file" }), 400 # 获取表单中的自定义文件名(可选) custom_filename = request.form.get('filename', None) if custom_filename: filename = custom_filename + "." + file.filename.split('.')[-1] else: filename = file.filename try: # 生成 UUID v1 uuid_v1 = uuid.uuid1() print(f"UUID v1: {uuid_v1}") # 构造文件名和路径 final_filename = f"{uuid_v1}_{filename}" filepath = Path(BASE_DIR / "videoFile" / f"{uuid_v1}_{filename}") # 保存文件 file.save(filepath) with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO file_records (filename, filesize, file_path) VALUES (?, ?, ?) ''', (filename, round(float(os.path.getsize(filepath)) / (1024 * 1024),2), final_filename)) conn.commit() print("✅ 上传文件已记录") return jsonify({ "code": 200, "msg": "File uploaded and saved successfully", "data": { "filename": filename, "filepath": final_filename } }), 200 except Exception as e: print(f"Upload failed: {e}") return jsonify({ "code": 500, "msg": f"upload failed: {e}", "data": None }), 500 @app.route('/getFiles', methods=['GET']) def get_all_files(): try: # 使用 with 自动管理数据库连接 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row # 允许通过列名访问结果 cursor = conn.cursor() # 查询所有记录 cursor.execute("SELECT * FROM file_records") rows = cursor.fetchall() # 将结果转为字典列表,并提取UUID data = [] for row in rows: row_dict = dict(row) # 从 file_path 中提取 UUID (文件名的第一部分,下划线前) if row_dict.get('file_path'): file_path_parts = row_dict['file_path'].split('_', 1) # 只分割第一个下划线 if len(file_path_parts) > 0: row_dict['uuid'] = file_path_parts[0] # UUID 部分 else: row_dict['uuid'] = '' else: row_dict['uuid'] = '' data.append(row_dict) return jsonify({ "code": 200, "msg": "success", "data": data }), 200 except Exception as e: return jsonify({ "code": 500, "msg": str("get file failed!"), "data": None }), 500 @app.route("/getAccounts", methods=['GET']) def getAccounts(): """快速获取所有账号信息,不进行cookie验证""" try: with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT * FROM user_info''') rows = cursor.fetchall() rows_list = [list(row) for row in rows] print("\n📋 当前数据表内容(快速获取):") for row in rows: print(row) return jsonify( { "code": 200, "msg": None, "data": rows_list }), 200 except Exception as e: print(f"获取账号列表时出错: {str(e)}") return jsonify({ "code": 500, "msg": f"获取账号列表失败: {str(e)}", "data": None }), 500 @app.route("/getValidAccounts",methods=['GET']) async def getValidAccounts(): with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: cursor = conn.cursor() cursor.execute(''' SELECT * FROM user_info''') rows = cursor.fetchall() rows_list = [list(row) for row in rows] print("\n📋 当前数据表内容:") for row in rows: print(row) for row in rows_list: flag = await check_cookie(row[1],row[2]) if not flag: row[4] = 0 cursor.execute(''' UPDATE user_info SET status = ? WHERE id = ? ''', (0,row[0])) conn.commit() print("✅ 用户状态已更新") for row in rows: print(row) return jsonify( { "code": 200, "msg": None, "data": rows_list }),200 @app.route('/deleteFile', methods=['GET']) def delete_file(): file_id = request.args.get('id') if not file_id or not file_id.isdigit(): return jsonify({ "code": 400, "msg": "Invalid or missing file ID", "data": None }), 400 try: # 获取数据库连接 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() # 查询要删除的记录 cursor.execute("SELECT * FROM file_records WHERE id = ?", (file_id,)) record = cursor.fetchone() if not record: return jsonify({ "code": 404, "msg": "File not found", "data": None }), 404 record = dict(record) # 获取文件路径并删除实际文件 file_path = Path(BASE_DIR / "videoFile" / record['file_path']) if file_path.exists(): try: file_path.unlink() # 删除文件 print(f"✅ 实际文件已删除: {file_path}") except Exception as e: print(f"⚠️ 删除实际文件失败: {e}") # 即使删除文件失败,也要继续删除数据库记录,避免数据不一致 else: print(f"⚠️ 实际文件不存在: {file_path}") # 删除数据库记录 cursor.execute("DELETE FROM file_records WHERE id = ?", (file_id,)) conn.commit() return jsonify({ "code": 200, "msg": "File deleted successfully", "data": { "id": record['id'], "filename": record['filename'] } }), 200 except Exception as e: return jsonify({ "code": 500, "msg": str("delete failed!"), "data": None }), 500 @app.route('/deleteAccount', methods=['GET']) def delete_account(): account_id = request.args.get('id') if not account_id or not account_id.isdigit(): return jsonify({ "code": 400, "msg": "Invalid or missing account ID", "data": None }), 400 account_id = int(account_id) try: # 获取数据库连接 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() # 查询要删除的记录 cursor.execute("SELECT * FROM user_info WHERE id = ?", (account_id,)) record = cursor.fetchone() if not record: return jsonify({ "code": 404, "msg": "account not found", "data": None }), 404 record = dict(record) # 删除关联的cookie文件 if record.get('filePath'): cookie_file_path = Path(BASE_DIR / "cookiesFile" / record['filePath']) if cookie_file_path.exists(): try: cookie_file_path.unlink() print(f"✅ Cookie文件已删除: {cookie_file_path}") except Exception as e: print(f"⚠️ 删除Cookie文件失败: {e}") # 删除数据库记录 cursor.execute("DELETE FROM user_info WHERE id = ?", (account_id,)) conn.commit() return jsonify({ "code": 200, "msg": "account deleted successfully", "data": None }), 200 except Exception as e: return jsonify({ "code": 500, "msg": f"delete failed: {str(e)}", "data": None }), 500 # SSE 登录接口 @app.route('/login') def login(): # 1 小红书 2 视频号 3 抖音 4 快手 type = request.args.get('type') # 账号名 id = request.args.get('id') # 模拟一个用于异步通信的队列 status_queue = Queue() active_queues[id] = status_queue def on_close(): print(f"清理队列: {id}") del active_queues[id] # 启动异步任务线程 thread = threading.Thread(target=run_async_function, args=(type,id,status_queue), daemon=True) thread.start() response = Response(sse_stream(status_queue,), mimetype='text/event-stream') response.headers['Cache-Control'] = 'no-cache' response.headers['X-Accel-Buffering'] = 'no' # 关键:禁用 Nginx 缓冲 response.headers['Content-Type'] = 'text/event-stream' response.headers['Connection'] = 'keep-alive' return response @app.route('/postVideo', methods=['POST']) def postVideo(): # 获取JSON数据 data = request.get_json() if not data: return jsonify({"code": 400, "msg": "请求数据不能为空", "data": None}), 400 # 从JSON数据中提取fileList和accountList file_list = data.get('fileList', []) account_list = data.get('accountList', []) type = data.get('type') title = data.get('title') tags = data.get('tags') category = data.get('category') enableTimer = data.get('enableTimer') if category == 0: category = None productLink = data.get('productLink', '') productTitle = data.get('productTitle', '') thumbnail_path = data.get('thumbnail', '') is_draft = data.get('isDraft', False) # 新增参数:是否保存为草稿 videos_per_day = data.get('videosPerDay') daily_times = data.get('dailyTimes') start_days = data.get('startDays') # 参数校验 if not file_list: return jsonify({"code": 400, "msg": "文件列表不能为空", "data": None}), 400 if not account_list: return jsonify({"code": 400, "msg": "账号列表不能为空", "data": None}), 400 if not type: return jsonify({"code": 400, "msg": "平台类型不能为空", "data": None}), 400 if not title: return jsonify({"code": 400, "msg": "标题不能为空", "data": None}), 400 # 打印获取到的数据(仅作为示例) print("File List:", file_list) print("Account List:", account_list) try: match type: case 1: post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days) case 2: post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days, is_draft) case 3: post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days, thumbnail_path, productLink, productTitle) case 4: post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days) case _: return jsonify({"code": 400, "msg": f"不支持的平台类型: {type}", "data": None}), 400 # 返回响应给客户端 return jsonify( { "code": 200, "msg": "发布任务已提交", "data": None }), 200 except Exception as e: print(f"发布视频时出错: {str(e)}") return jsonify({ "code": 500, "msg": f"发布失败: {str(e)}", "data": None }), 500 @app.route('/updateUserinfo', methods=['POST']) def updateUserinfo(): # 获取JSON数据 data = request.get_json() # 从JSON数据中提取 type 和 userName user_id = data.get('id') type = data.get('type') userName = data.get('userName') try: # 获取数据库连接 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() # 更新数据库记录 cursor.execute(''' UPDATE user_info SET type = ?, userName = ? WHERE id = ?; ''', (type, userName, user_id)) conn.commit() return jsonify({ "code": 200, "msg": "account update successfully", "data": None }), 200 except Exception as e: return jsonify({ "code": 500, "msg": str("update failed!"), "data": None }), 500 @app.route('/postVideoBatch', methods=['POST']) def postVideoBatch(): data_list = request.get_json() if not isinstance(data_list, list): return jsonify({"code": 400, "msg": "Expected a JSON array", "data": None}), 400 for data in data_list: # 从JSON数据中提取fileList和accountList file_list = data.get('fileList', []) account_list = data.get('accountList', []) type = data.get('type') title = data.get('title') tags = data.get('tags') category = data.get('category') enableTimer = data.get('enableTimer') if category == 0: category = None productLink = data.get('productLink', '') productTitle = data.get('productTitle', '') is_draft = data.get('isDraft', False) videos_per_day = data.get('videosPerDay') daily_times = data.get('dailyTimes') start_days = data.get('startDays') # 打印获取到的数据(仅作为示例) print("File List:", file_list) print("Account List:", account_list) match type: case 1: post_video_xhs(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days) case 2: post_video_tencent(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days, is_draft) case 3: post_video_DouYin(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days, productLink, productTitle) case 4: post_video_ks(title, file_list, tags, account_list, category, enableTimer, videos_per_day, daily_times, start_days) # 返回响应给客户端 return jsonify( { "code": 200, "msg": None, "data": None }), 200 # Cookie文件上传API @app.route('/uploadCookie', methods=['POST']) def upload_cookie(): try: if 'file' not in request.files: return jsonify({ "code": 400, "msg": "没有找到Cookie文件", "data": None }), 400 file = request.files['file'] if file.filename == '': return jsonify({ "code": 400, "msg": "Cookie文件名不能为空", "data": None }), 400 if not file.filename.endswith('.json'): return jsonify({ "code": 400, "msg": "Cookie文件必须是JSON格式", "data": None }), 400 # 获取账号信息 account_id = request.form.get('id') platform = request.form.get('platform') if not account_id or not platform: return jsonify({ "code": 400, "msg": "缺少账号ID或平台信息", "data": None }), 400 # 从数据库获取账号的文件路径 with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute('SELECT filePath FROM user_info WHERE id = ?', (account_id,)) result = cursor.fetchone() if not result: return jsonify({ "code": 500, "msg": "账号不存在", "data": None }), 404 # 保存上传的Cookie文件到对应路径 cookie_file_path = Path(BASE_DIR / "cookiesFile" / result['filePath']) cookie_file_path.parent.mkdir(parents=True, exist_ok=True) file.save(str(cookie_file_path)) # 更新数据库中的账号信息(可选,比如更新更新时间) # 这里可以根据需要添加额外的处理逻辑 return jsonify({ "code": 200, "msg": "Cookie文件上传成功", "data": None }), 200 except Exception as e: print(f"上传Cookie文件时出错: {str(e)}") return jsonify({ "code": 500, "msg": f"上传Cookie文件失败: {str(e)}", "data": None }), 500 # Cookie文件下载API @app.route('/downloadCookie', methods=['GET']) def download_cookie(): try: file_path = request.args.get('filePath') if not file_path: return jsonify({ "code": 500, "msg": "缺少文件路径参数", "data": None }), 400 # 验证文件路径的安全性,防止路径遍历攻击 cookie_file_path = Path(BASE_DIR / "cookiesFile" / file_path).resolve() base_path = Path(BASE_DIR / "cookiesFile").resolve() if not cookie_file_path.is_relative_to(base_path): return jsonify({ "code": 500, "msg": "非法文件路径", "data": None }), 400 if not cookie_file_path.exists(): return jsonify({ "code": 500, "msg": "Cookie文件不存在", "data": None }), 404 # 返回文件 return send_from_directory( directory=str(cookie_file_path.parent), path=cookie_file_path.name, as_attachment=True ) except Exception as e: print(f"下载Cookie文件时出错: {str(e)}") return jsonify({ "code": 500, "msg": f"下载Cookie文件失败: {str(e)}", "data": None }), 500 # 包装函数:在线程中运行异步函数 def run_async_function(type,id,status_queue): match type: case '1': loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(xiaohongshu_cookie_gen(id, status_queue)) loop.close() case '2': loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(get_tencent_cookie(id,status_queue)) loop.close() case '3': loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(douyin_cookie_gen(id,status_queue)) loop.close() case '4': loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(get_ks_cookie(id,status_queue)) loop.close() # SSE 流生成器函数 def sse_stream(status_queue): while True: if not status_queue.empty(): msg = status_queue.get() yield f"data: {msg}\n\n" else: # 避免 CPU 占满 time.sleep(0.1) if __name__ == '__main__': app.run(host='0.0.0.0' ,port=5409) ================================================ FILE: sau_frontend/README.md ================================================ # Vue3 + Vite 项目 一个基于 Vue3、Vite、Element Plus、Pinia、Vue Router 和 Axios 的现代化前端项目模板。 ## 🚀 特性 - ⚡️ **Vite** - 极速的构建工具 - 🖖 **Vue 3** - 渐进式 JavaScript 框架 - 🎨 **Element Plus** - 基于 Vue 3 的组件库 - 🗂 **Vue Router** - 官方路由管理器(WebHash 模式) - 📦 **Pinia** - 新一代状态管理 - 🔗 **Axios** - HTTP 请求库(已封装) - 🎯 **Sass** - CSS 预处理器 - 📁 **规范化目录结构** - views 存放页面,components 存放组件 - 🔧 **完整配置** - 包含开发和生产环境配置 ## 📦 安装 ```bash # 安装依赖 npm install # 启动开发服务器 npm run dev # 构建生产版本 npm run build # 预览生产构建 npm run preview ``` ## 📁 项目结构 ``` src/ ├── api/ # API 接口 │ ├── index.js # API 统一导出 │ └── user.js # 用户相关 API ├── components/ # 公共组件 │ └── HelloWorld.vue # 示例组件 ├── router/ # 路由配置 │ └── index.js # 路由主文件 ├── stores/ # 状态管理 │ ├── index.js # Pinia 配置 │ └── user.js # 用户状态 ├── styles/ # 样式文件 │ ├── index.scss # 主样式文件 │ ├── reset.scss # 重置样式 │ └── variables.scss # 样式变量 ├── utils/ # 工具函数 │ └── request.js # HTTP 请求封装 ├── views/ # 页面组件 │ ├── Home.vue # 首页 │ └── About.vue # 关于页面 ├── App.vue # 根组件 └── main.js # 入口文件 ``` ## 🔧 配置说明 ### 环境变量 - `.env` - 通用环境变量 - `.env.development` - 开发环境变量 - `.env.production` - 生产环境变量 ### 路由配置 项目使用 Vue Router 4,配置为 WebHash 模式,路由文件位于 `src/router/index.js`。 ### 状态管理 使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。 ### HTTP 请求 Axios 已经过封装,包含: - 请求/响应拦截器 - 错误处理 - Token 自动添加 - 统一的响应格式处理 使用方式: ```javascript import { http } from '@/utils/request' // GET 请求 const data = await http.get('/api/users') // POST 请求 const result = await http.post('/api/users', { name: 'John' }) ``` ### 样式系统 - 使用 Sass 作为 CSS 预处理器 - 已删除所有浏览器默认样式 - 提供了完整的样式变量和工具类 - 支持 Element Plus 主题定制 ## 🎨 组件库 项目集成了 Element Plus,所有组件都可以直接使用: ```vue ``` ## 📝 开发规范 1. **页面组件** 放在 `src/views/` 目录 2. **公共组件** 放在 `src/components/` 目录 3. **使用 setup 语法糖** 编写组件 4. **样式使用 Sass** 并遵循 BEM 命名规范 5. **API 请求** 统一放在 `src/api/` 目录 6. **状态管理** 按模块划分,放在 `src/stores/` 目录 ## 🚀 部署 ```bash # 构建生产版本 npm run build # 构建完成后,dist 目录包含所有静态文件 # 可以部署到任何静态文件服务器 ``` ## 📄 许可证 MIT License ================================================ FILE: sau_frontend/index.html ================================================ SAU自媒体自动化运营系统
================================================ FILE: sau_frontend/package.json ================================================ { "name": "sau-admin", "private": true, "version": "0.0.0", "author": "Edan.Lee", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", "axios": "^1.9.0", "element-plus": "^2.9.11", "pinia": "^3.0.2", "sass": "^1.89.1", "vue": "^3.5.13", "vue-router": "^4.5.1" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", "vite": "^6.3.5" } } ================================================ FILE: sau_frontend/src/App.vue ================================================ ================================================ FILE: sau_frontend/src/api/account.js ================================================ import { http } from '@/utils/request' // 账号管理相关API export const accountApi = { // 获取有效账号列表(带验证) getValidAccounts() { return http.get('/getValidAccounts') }, // 获取账号列表(不带验证,快速加载) getAccounts() { return http.get('/getAccounts') }, // 添加账号 addAccount(data) { return http.post('/account', data) }, // 更新账号 updateAccount(data) { return http.post('/updateUserinfo', data) }, // 删除账号 deleteAccount(id) { return http.get(`/deleteAccount?id=${id}`) } } ================================================ FILE: sau_frontend/src/api/index.js ================================================ // API 统一导出 export * from './user' export * from './account' export * from './material' // 可以在这里添加其他API模块的导出 // export * from './product' // export * from './order' // export * from './common' ================================================ FILE: sau_frontend/src/api/material.js ================================================ import { http } from '@/utils/request' // 素材管理API export const materialApi = { // 获取所有素材 getAllMaterials: () => { return http.get('/getFiles') }, // 上传素材 uploadMaterial: (formData, onUploadProgress) => { // 使用http.upload方法,它已经配置了正确的Content-Type return http.upload('/uploadSave', formData, onUploadProgress) }, // 删除素材 deleteMaterial: (id) => { return http.get(`/deleteFile?id=${id}`) }, // 下载素材 downloadMaterial: (filePath) => { return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/download/${filePath}` }, // 获取素材预览URL getMaterialPreviewUrl: (filename) => { return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/getFile?filename=${filename}` } } ================================================ FILE: sau_frontend/src/api/user.js ================================================ import { http } from '@/utils/request' // 用户相关API(预留) // 注意:当前后端暂无用户认证接口,以下为预留定义 export const userApi = {} ================================================ FILE: sau_frontend/src/main.js ================================================ import { createApp } from 'vue' import App from './App.vue' import router from './router' import pinia from './stores' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import './styles/index.scss' const app = createApp(App) // 注册 Element Plus 图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(router) app.use(pinia) app.use(ElementPlus) app.mount('#app') ================================================ FILE: sau_frontend/src/router/index.js ================================================ import { createRouter, createWebHashHistory } from 'vue-router' import Dashboard from '../views/Dashboard.vue' import AccountManagement from '../views/AccountManagement.vue' import MaterialManagement from '../views/MaterialManagement.vue' import PublishCenter from '../views/PublishCenter.vue' import About from '../views/About.vue' const routes = [ { path: '/', name: 'Dashboard', component: Dashboard }, { path: '/account-management', name: 'AccountManagement', component: AccountManagement }, { path: '/material-management', name: 'MaterialManagement', component: MaterialManagement }, { path: '/publish-center', name: 'PublishCenter', component: PublishCenter }, { path: '/about', name: 'About', component: About } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router ================================================ FILE: sau_frontend/src/stores/account.js ================================================ import { defineStore } from 'pinia' import { ref } from 'vue' export const useAccountStore = defineStore('account', () => { // 存储所有账号信息 const accounts = ref([]) // 平台类型映射 const platformTypes = { 1: '小红书', 2: '视频号', 3: '抖音', 4: '快手' } // 设置账号列表 const setAccounts = (accountsData) => { // 转换后端返回的数据格式为前端使用的格式 accounts.value = accountsData.map(item => { return { id: item[0], type: item[1], filePath: item[2], name: item[3], status: item[4] === -1 ? '验证中' : (item[4] === 1 ? '正常' : '异常'), platform: platformTypes[item[1]] || '未知' } }) } // 添加账号 const addAccount = (account) => { accounts.value.push(account) } // 更新账号 const updateAccount = (id, updatedAccount) => { const index = accounts.value.findIndex(acc => acc.id === id) if (index !== -1) { accounts.value[index] = { ...accounts.value[index], ...updatedAccount } } } // 删除账号 const deleteAccount = (id) => { accounts.value = accounts.value.filter(acc => acc.id !== id) } // 根据平台获取账号 const getAccountsByPlatform = (platform) => { return accounts.value.filter(acc => acc.platform === platform) } return { accounts, setAccounts, addAccount, updateAccount, deleteAccount, getAccountsByPlatform } }) ================================================ FILE: sau_frontend/src/stores/app.js ================================================ import { defineStore } from 'pinia' import { ref } from 'vue' export const useAppStore = defineStore('app', () => { // 是否是第一次进入账号管理页面 const isFirstTimeAccountManagement = ref(true) // 是否是第一次进入素材管理页面 const isFirstTimeMaterialManagement = ref(true) // 账号管理页面刷新状态 const isAccountRefreshing = ref(false) // 素材列表数据 const materials = ref([]) // 设置账号管理页面已访问 const setAccountManagementVisited = () => { isFirstTimeAccountManagement.value = false } // 设置素材管理页面已访问 const setMaterialManagementVisited = () => { isFirstTimeMaterialManagement.value = false } // 重置所有访问状态(用于重新登录或刷新应用时) const resetVisitStatus = () => { isFirstTimeAccountManagement.value = true isFirstTimeMaterialManagement.value = true } // 更新素材列表 const setMaterials = (materialList) => { materials.value = materialList } // 添加新素材 const addMaterial = (material) => { materials.value.push(material) } // 删除素材 const removeMaterial = (materialId) => { const index = materials.value.findIndex(m => m.id === materialId) if (index > -1) { materials.value.splice(index, 1) } } // 设置账号管理页面刷新状态 const setAccountRefreshing = (status) => { isAccountRefreshing.value = status } return { isFirstTimeAccountManagement, isFirstTimeMaterialManagement, isAccountRefreshing, materials, setAccountManagementVisited, setMaterialManagementVisited, resetVisitStatus, setMaterials, addMaterial, removeMaterial, setAccountRefreshing } }) ================================================ FILE: sau_frontend/src/stores/index.js ================================================ import { createPinia } from 'pinia' import { useUserStore } from './user' import { useAccountStore } from './account' import { useAppStore } from './app' const pinia = createPinia() export default pinia export { useUserStore, useAccountStore, useAppStore } ================================================ FILE: sau_frontend/src/stores/user.js ================================================ import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = defineStore('user', () => { const userInfo = ref({ name: '', email: '' }) const isLoggedIn = ref(false) const setUserInfo = (info) => { userInfo.value = info isLoggedIn.value = true } const logout = () => { userInfo.value = { name: '', email: '' } isLoggedIn.value = false } return { userInfo, isLoggedIn, setUserInfo, logout } }) ================================================ FILE: sau_frontend/src/styles/index.scss ================================================ // 导入重置样式 @use './reset.scss'; // 导入变量 @use './variables.scss' as *; // 全局样式 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: $text-primary; background-color: $bg-color-page; } #app { min-height: 100vh; } // 通用工具类 .text-center { text-align: center; } .text-left { text-align: left; } .text-right { text-align: right; } .flex { display: flex; } .flex-center { display: flex; justify-content: center; align-items: center; } .flex-between { display: flex; justify-content: space-between; align-items: center; } .flex-column { display: flex; flex-direction: column; } .w-full { width: 100%; } .h-full { height: 100%; } // 间距工具类 .m-0 { margin: 0; } .mt-0 { margin-top: 0; } .mr-0 { margin-right: 0; } .mb-0 { margin-bottom: 0; } .ml-0 { margin-left: 0; } .m-1 { margin: $spacing-xs; } .mt-1 { margin-top: $spacing-xs; } .mr-1 { margin-right: $spacing-xs; } .mb-1 { margin-bottom: $spacing-xs; } .ml-1 { margin-left: $spacing-xs; } .m-2 { margin: $spacing-sm; } .mt-2 { margin-top: $spacing-sm; } .mr-2 { margin-right: $spacing-sm; } .mb-2 { margin-bottom: $spacing-sm; } .ml-2 { margin-left: $spacing-sm; } .m-3 { margin: $spacing-md; } .mt-3 { margin-top: $spacing-md; } .mr-3 { margin-right: $spacing-md; } .mb-3 { margin-bottom: $spacing-md; } .ml-3 { margin-left: $spacing-md; } .m-4 { margin: $spacing-lg; } .mt-4 { margin-top: $spacing-lg; } .mr-4 { margin-right: $spacing-lg; } .mb-4 { margin-bottom: $spacing-lg; } .ml-4 { margin-left: $spacing-lg; } .p-0 { padding: 0; } .pt-0 { padding-top: 0; } .pr-0 { padding-right: 0; } .pb-0 { padding-bottom: 0; } .pl-0 { padding-left: 0; } .p-1 { padding: $spacing-xs; } .pt-1 { padding-top: $spacing-xs; } .pr-1 { padding-right: $spacing-xs; } .pb-1 { padding-bottom: $spacing-xs; } .pl-1 { padding-left: $spacing-xs; } .p-2 { padding: $spacing-sm; } .pt-2 { padding-top: $spacing-sm; } .pr-2 { padding-right: $spacing-sm; } .pb-2 { padding-bottom: $spacing-sm; } .pl-2 { padding-left: $spacing-sm; } .p-3 { padding: $spacing-md; } .pt-3 { padding-top: $spacing-md; } .pr-3 { padding-right: $spacing-md; } .pb-3 { padding-bottom: $spacing-md; } .pl-3 { padding-left: $spacing-md; } .p-4 { padding: $spacing-lg; } .pt-4 { padding-top: $spacing-lg; } .pr-4 { padding-right: $spacing-lg; } .pb-4 { padding-bottom: $spacing-lg; } .pl-4 { padding-left: $spacing-lg; } ================================================ FILE: sau_frontend/src/styles/reset.scss ================================================ /* CSS Reset - 删除浏览器默认样式 */ /* 1. Use a more-intuitive box-sizing model */ *, *::before, *::after { box-sizing: border-box; } /* 2. Remove default margin and padding */ * { margin: 0; padding: 0; } /* 3. Allow percentage-based heights in the application */ html, body { height: 100%; } /* 4. Add accessible line-height and improve text rendering */ body { line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; } /* 5. Improve media defaults */ img, picture, video, canvas, svg { display: block; max-width: 100%; } /* 6. Remove built-in form typography styles */ input, button, textarea, select { font: inherit; } /* 7. Avoid text overflows */ p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; } /* 8. Create a root stacking context */ #root, #__next, #app { isolation: isolate; } /* 9. Remove list styles */ ul, ol { list-style: none; } /* 10. Remove default button styles */ button { background: none; border: none; cursor: pointer; } /* 11. Remove default link styles */ a { text-decoration: none; color: inherit; } /* 12. Remove default table styles */ table { border-collapse: collapse; border-spacing: 0; } /* 13. Remove default fieldset styles */ fieldset { border: none; } /* 14. Remove default legend styles */ legend { display: table; } /* 15. Remove default details/summary styles */ details { display: block; } summary { display: list-item; } /* 16. Remove default hr styles */ hr { border: none; height: 1px; background: #ccc; } /* 17. Remove default blockquote styles */ blockquote { quotes: none; } blockquote:before, blockquote:after { content: ''; content: none; } /* 18. Remove default cite styles */ cite { font-style: normal; } /* 19. Remove default address styles */ address { font-style: normal; } ================================================ FILE: sau_frontend/src/styles/variables.scss ================================================ // 颜色变量 $primary-color: #409eff; $success-color: #67c23a; $warning-color: #e6a23c; $danger-color: #f56c6c; $info-color: #909399; // 文字颜色 $text-primary: #303133; $text-regular: #606266; $text-secondary: #909399; $text-placeholder: #c0c4cc; // 边框颜色 $border-base: #dcdfe6; $border-light: #e4e7ed; $border-lighter: #ebeef5; $border-extra-light: #f2f6fc; // 背景颜色 $bg-color: #ffffff; $bg-color-page: #f2f3f5; $bg-color-overlay: #ffffff; // 字体大小 $font-size-extra-large: 20px; $font-size-large: 18px; $font-size-medium: 16px; $font-size-base: 14px; $font-size-small: 13px; $font-size-extra-small: 12px; // 间距 $spacing-xs: 4px; $spacing-sm: 8px; $spacing-md: 16px; $spacing-lg: 24px; $spacing-xl: 32px; // 圆角 $border-radius-base: 4px; $border-radius-small: 2px; $border-radius-round: 20px; $border-radius-circle: 50%; // 阴影 $box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04); $box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12); $box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1); // 层级 $z-index-normal: 1; $z-index-top: 1000; $z-index-popper: 2000; ================================================ FILE: sau_frontend/src/utils/request.js ================================================ import axios from 'axios' import { ElMessage } from 'element-plus' // 创建axios实例 const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409', headers: { 'Content-Type': 'application/json' } }) // 请求拦截器 request.interceptors.request.use( (config) => { // 可以在这里添加token等认证信息 const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => { console.error('请求错误:', error) return Promise.reject(error) } ) // 响应拦截器 request.interceptors.response.use( (response) => { const { data } = response // 根据后端接口规范处理响应 if (data.code === 200 || data.success) { return data } else { ElMessage.error(data.msg || data.message || '请求失败') return Promise.reject(new Error(data.msg || data.message || '请求失败')) } }, (error) => { console.error('响应错误:', error) // 处理HTTP错误状态码 if (error.response) { const { status } = error.response switch (status) { case 401: ElMessage.error('未授权,请重新登录') // 可以在这里处理登录跳转 break case 403: ElMessage.error('拒绝访问') break case 404: ElMessage.error('请求地址不存在') break case 500: ElMessage.error('服务器内部错误') break default: ElMessage.error('网络错误') } } else { ElMessage.error('网络连接失败') } return Promise.reject(error) } ) // 封装常用的请求方法 export const http = { get(url, params) { return request.get(url, { params }) }, post(url, data, config = {}) { return request.post(url, data, config) }, put(url, data, config = {}) { return request.put(url, data, config) }, delete(url, params) { return request.delete(url, { params }) }, upload(url, formData, onUploadProgress) { return request.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress }) } } export default request ================================================ FILE: sau_frontend/src/views/About.vue ================================================ ================================================ FILE: sau_frontend/src/views/AccountManagement.vue ================================================ ================================================ FILE: sau_frontend/src/views/Dashboard.vue ================================================ ================================================ FILE: sau_frontend/src/views/MaterialManagement.vue ================================================ ================================================ FILE: sau_frontend/src/views/PublishCenter.vue ================================================ ================================================ FILE: sau_frontend/vite.config.js ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' // https://vite.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src'), }, }, css: { preprocessorOptions: { scss: { // 移除自动导入,改用@use语法 } } }, server: { port: 5173, open: true, proxy: { '/api': { target: 'http://localhost:5409', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, build: { outDir: 'dist', sourcemap: false, chunkSizeWarningLimit: 1600, rollupOptions: { output: { manualChunks: { vue: ['vue', 'vue-router', 'pinia'], elementPlus: ['element-plus'], utils: ['axios'] } } } } }) ================================================ FILE: start-win.bat ================================================ @echo off TITLE One-Click Starter for social-auto-upload ECHO ================================================== ECHO Starting social-auto-upload Servers... ECHO ================================================== ECHO. ECHO [1/2] Starting Python Backend Server in a new window... REM The START command launches a new process. REM The first quoted string "SAU Backend" is the title of the new window. REM cmd /k runs the command and keeps the window open to show logs. START "SAU Backend" cmd /k "python sau_backend.py" ECHO [2/2] Starting Vue.js Frontend Server in another new window... START "SAU Frontend" cmd /k "cd sau_frontend && npm run dev -- --host 0.0.0.0" ECHO. ECHO ================================================== ECHO Done. ECHO Two new windows have been opened for the backend ECHO and frontend servers. You can monitor logs there. ECHO ================================================== ECHO. ECHO This window will close in 10 seconds... timeout /t 10 /nobreak > nul ================================================ FILE: uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies").mkdir(exist_ok=True) ================================================ FILE: uploader/baijiahao_uploader/__init__.py ================================================ ================================================ FILE: uploader/baijiahao_uploader/main.py ================================================ # -*- coding: utf-8 -*- import random from datetime import datetime from playwright.async_api import Playwright, async_playwright, Page import os import time import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.log import baijiahao_logger from utils.network import async_retry async def baijiahao_cookie_gen(account_file): async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://baijiahao.baidu.com/builder/theme/bjh/login") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) baijiahao_logger.success("cookie saved") async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://baijiahao.baidu.com/builder/rc/home") await page.wait_for_timeout(timeout=5000) if await page.get_by_text('注册/登录百家号').count(): baijiahao_logger.error("等待5秒 cookie 失效") return False else: baijiahao_logger.success("[+] cookie 有效") return True async def baijiahao_setup(account_file, handle=False): if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: return False baijiahao_logger.error("cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件") await baijiahao_cookie_gen(account_file) return True class BaiJiaHaoVideo(object): def __init__(self, title, file_path, tags, publish_date: datetime, account_file, proxy_setting=None): self.title = title # 视频标题 self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.date_format = '%Y年%m月%d日 %H:%M' self.local_executable_path = LOCAL_CHROME_PATH self.headless = LOCAL_CHROME_HEADLESS self.proxy_setting = proxy_setting async def set_schedule_time(self, page, publish_date): """ todo 时间选择,日后在处理 百家号的时间选择不准确,目前是随机 """ publish_date_day = f"{publish_date.month}月{publish_date.day}日" if publish_date.day >9 else f"{publish_date.month}月0{publish_date.day}日" publish_date_hour = f"{publish_date.hour}点" publish_date_min = f"{publish_date.minute}分" await page.wait_for_selector('div.select-wrap', timeout=5000) for _ in range(3): try: await page.locator('div.select-wrap').nth(0).click() await page.wait_for_selector('div.rc-virtual-list div.cheetah-select-item', timeout=5000) break except: await page.locator('div.select-wrap').nth(0).click() # page.locator(f'div.rc-virtual-list-holder-inner >> text={publish_date_day}').click() await page.wait_for_timeout(2000) await page.locator(f'div.rc-virtual-list div.cheetah-select-item >> text={publish_date_day}').click() await page.wait_for_timeout(2000) # 改为随机点击一个 hour for _ in range(3): try: await page.locator('div.select-wrap').nth(1).click() await page.wait_for_selector('div.rc-virtual-list div.rc-virtual-list-holder-inner:visible', timeout=5000) break except: await page.locator('div.select-wrap').nth(1).click() await page.wait_for_timeout(2000) current_choice_hour = await page.locator('div.rc-virtual-list:visible div.cheetah-select-item-option').count() await page.wait_for_timeout(2000) await page.locator('div.rc-virtual-list:visible div.cheetah-select-item-option').nth( random.randint(1, current_choice_hour-3)).click() # 2024.08.05 current_choice_hour的获取可能有问题,页面有7,这里获取了10,暂时硬编码至6 await page.wait_for_timeout(2000) await page.locator("button >> text=定时发布").click() async def handle_upload_error(self, page): # 日后实现,目前没遇到 return print("视频出错了,重新上传中") async def upload(self, playwright: Playwright) -> None: # 使用 Chromium 浏览器启动一个浏览器实例 browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path, proxy=self.proxy_setting) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context(storage_state=f"{self.account_file}", user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.4324.150 Safari/537.36') # context = await set_init_script(context) await context.grant_permissions(['geolocation']) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://baijiahao.baidu.com/builder/rc/edit?type=videoV2", timeout=60000) baijiahao_logger.info(f"正在上传-------{self.title}.mp4") # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 baijiahao_logger.info('正在打开主页...') await page.wait_for_url("https://baijiahao.baidu.com/builder/rc/edit?type=videoV2", timeout=60000) # 点击 "上传视频" 按钮 await page.locator("div[class^='video-main-container'] input").set_input_files(self.file_path) # 等待页面跳转到指定的 URL while True: # 判断是是否进入视频发布页面,没进入,则自动等待到超时 try: await page.wait_for_selector("div#formMain:visible") break except: baijiahao_logger.info("正在等待进入视频发布页面...") await asyncio.sleep(0.1) # 填充标题和话题 # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素 await asyncio.sleep(1) baijiahao_logger.info("正在填充标题和话题...") await self.add_title_tags(page) upload_status = await self.uploading_video(page) if not upload_status: baijiahao_logger.error(f"发现上传出错了... 文件:{self.file_path}") raise # 判断视频封面图是否生成成功 while True: baijiahao_logger.info("正在确认封面完成, 准备去点击定时/发布...") if await page.locator("div.cheetah-spin-container img").count(): baijiahao_logger.info("封面已完成,点击定时/发布...") break else: baijiahao_logger.info("等待封面生成...") await asyncio.sleep(3) await self.publish_video(page, self.publish_date) await page.wait_for_timeout(2000) if await page.locator('div.passMod_dialog-container >> text=百度安全验证:visible').count(): baijiahao_logger.error("出现验证,退出") raise Exception("出现验证,退出") await page.wait_for_url("https://baijiahao.baidu.com/builder/rc/clue**", timeout=5000) baijiahao_logger.success("视频发布成功") await context.storage_state(path=self.account_file) # 保存cookie baijiahao_logger.info('cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() @async_retry(timeout=300) # 例如,最多重试3次,超时时间为180秒 async def uploading_video(self, page): while True: upload_failed = await page.locator('div .cover-overlay:has-text("上传失败")').count() if upload_failed: baijiahao_logger.error("发现上传出错了...") # await self.handle_upload_error(page) # 假设这是处理上传错误的函数 return False uploading = await page.locator('div .cover-overlay:has-text("上传中")').count() if uploading: baijiahao_logger.info("正在上传视频中...") await asyncio.sleep(2) # 等待2秒再次检查 continue # 检查上传是否成功 if not uploading and not upload_failed: baijiahao_logger.success("视频上传完毕") return True async def set_schedule_publish(self, page, publish_date): while True: schedule_element = page.locator("div.op-btn-outter-content >> text=定时发布").locator("..").locator( 'button') try: await schedule_element.click() await page.wait_for_selector('div.select-wrap:visible', timeout=3000) await page.wait_for_timeout(timeout=2000) baijiahao_logger.info("开始点击发布定时...") await self.set_schedule_time(page, publish_date) break except Exception as e: baijiahao_logger.error(f"定时发布失败: {e}") raise # 重新抛出异常,让重试装饰器捕获 @async_retry(timeout=300) # 例如,最多重试3次,超时时间为180秒 async def publish_video(self, page: Page, publish_date): if publish_date != 0: # 定时发布 await self.set_schedule_publish(page, publish_date) else: # 立即发布 await self.direct_publish(page) async def direct_publish(self, page): try: publish_button = page.locator("button >> text=发布") if await publish_button.count(): await publish_button.click() except Exception as e: baijiahao_logger.error(f"直接发布视频失败: {e}") raise # 重新抛出异常,让重试装饰器捕获 async def add_title_tags(self, page): title_container = page.get_by_placeholder('添加标题获得更多推荐') if len(self.title) <= 8: self.title += " 你不知道的" await title_container.fill(self.title[:30]) async def main(self): async with async_playwright() as playwright: await self.upload(playwright) # 使用 AI成片 功能 async def ai2video(self, playwright: Playwright) -> None: # 使用 Chromium 浏览器启动一个浏览器实例 browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path, proxy=self.proxy_setting) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context( viewport={"width": 1600, "height": 900}, storage_state=f"{self.account_file}", user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.4324.150 Safari/537.36' ) # context = await set_init_script(context) await context.grant_permissions(['geolocation']) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://aigc.baidu.com/make", timeout=60000) # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 baijiahao_logger.info('正在打开主页...') await page.wait_for_url("https://aigc.baidu.com/make", timeout=60000) # 点击"全网"标签 await page.locator('div.rounded-lg.border:has-text("全网")').click() await asyncio.sleep(1) # 这里延迟是为了方便眼睛直观的观看 # 点击 "上传视频" 按钮 # await page.locator("div[class^='video-main-container'] input").set_input_files(self.file_path) # region 操作处 # 生成日期时间键名(格式:ai2video_YYYYMMDDHHMM) now = datetime.now() datetime_str = now.strftime("%Y%m%d%H%M") processed_key = "ai2video_processed_titles" batch_key = f"ai2video_{datetime_str}" # 初始化LocalStorage await page.evaluate(f""" if (!localStorage.getItem("{processed_key}")) {{ localStorage.setItem("{processed_key}", JSON.stringify([])); }} if (!localStorage.getItem("{batch_key}")) {{ localStorage.setItem("{batch_key}", JSON.stringify([])); }} """) # 定位新闻列表容器(转义特殊CSS字符) container_selector = '.overflow-auto.flex-grow.h-0.saas-scrollbar.mt\-\[-4px\].pl\-\[24px\].pr\-\[10px\].pb\-\[18px\]' news_items = await page.locator(container_selector).locator('div.py\-\[6px\].group.cursor-pointer').all() for item in news_items: try: # 获取新闻标题 title_elem = item.locator('div.flex.text-gray-darker.items-center.relative.pr\-\[56px\] > span') title = await title_elem.text_content() if not title: continue # 检查是否已处理过 is_processed = await page.evaluate( f"""title => {{ const processedList = JSON.parse(localStorage.getItem("{processed_key}") || "[]"); return processedList.includes(title); }}""", title ) if is_processed: print(f"[跳过] {title}") continue # 悬停显示按钮(根据HTML结构,按钮在悬停时显示) await item.hover() # 点击生成文案按钮 button = item.locator('button:has-text("生成文案")') await button.click() print(f"[点击] {title}") # 等待30秒 # await page.wait_for_timeout(30000) print(f"[等待完成] {title}") # 监听"一键成片"按钮 print(f"[开始监听] 一键成片按钮") should_exit_while_loop = False # 添加标志变量 while True: # 定位"一键成片"按钮 one_key_button = page.locator("button:has-text('一键成片')") # 检查按钮是否存在 if await one_key_button.count() > 0: # 检查按钮是否有disabled属性 is_disabled = await one_key_button.get_attribute("disabled") if is_disabled is None: # 按钮不再被禁用,点击它 print(f"[发现可点击按钮] 一键成片") await one_key_button.click() # 先点击一键成片按钮 # 等待可能出现的"温馨提示"窗口 print(f"[检查] 是否出现温馨提示窗口") await page.wait_for_timeout(2000) # 等待2秒,让窗口有时间显示 try: # 检查是否存在"温馨提示"窗口,设置较短的超时时间 tip_window = page.locator("div:has-text('温馨提示') >> visible=true") if await tip_window.count() > 0: print(f"[发现] 温馨提示窗口") # 定位并点击"知道了"按钮,设置较短的超时时间 know_button = page.locator("button:has-text('知道了')") if await know_button.count() > 0: try: # 设置较短的超时时间进行点击 await know_button.click(timeout=5000) print(f"[已点击] 知道了按钮") except Exception as e: print(f"[警告] 点击知道了按钮时出错: {str(e)}") else: print(f"[警告] 未找到知道了按钮") else: print(f"[信息] 未出现温馨提示窗口,继续执行") except Exception as e: print(f"[警告] 处理温馨提示窗口时出错: {str(e)}") # 继续执行,不要因为这个错误中断流程 # 记录到LocalStorage前打印日志 print(f"[开始记录] 准备将标题 '{title}' 记录到LocalStorage") # 记录到LocalStorage await page.evaluate( f""" (title, processedKey, batchKey) => {{ // 更新已处理列表 const processedList = JSON.parse(localStorage.getItem(processedKey) || "[]"); if (!processedList.includes(title)) {{ processedList.push(title); localStorage.setItem(processedKey, JSON.stringify(processedList)); }} // 更新当前批次记录 const batchList = JSON.parse(localStorage.getItem(batchKey) || "[]"); if (!batchList.includes(title)) {{ batchList.push(title); localStorage.setItem(batchKey, JSON.stringify(batchList)); }} }} """, title, processed_key, batch_key ) # 记录完成后打印日志 print(f"[记录完成] 标题 '{title}' 已成功记录到LocalStorage") print(f"[记录完成] {title}") # 监听新打开的标签页 print(f"[监听] 等待新标签页打开") # 获取当前所有页面 current_pages = context.pages current_page_count = len(current_pages) # 等待新标签页打开(最多等待10秒) new_page = None max_wait_time = 10 # 最大等待时间(秒) start_time = time.time() while time.time() - start_time < max_wait_time: # 获取最新的页面列表 pages = context.pages # 如果页面数量增加,说明新标签页已打开 if len(pages) > current_page_count: # 获取最新打开的页面(通常是列表中的最后一个) new_page = pages[-1] print(f"[发现] 新标签页已打开") break # 短暂等待后再次检查 await asyncio.sleep(0.5) # 如果找到新标签页,获取其标题和URL并保存 if new_page: # 等待页面加载完成 try: await new_page.wait_for_load_state("domcontentloaded", timeout=5000) # 获取页面标题和URL page_title = await new_page.title() page_url = new_page.url print(f"[获取] 标题: {page_title}") print(f"[获取] URL: {page_url}") # 将标题和URL保存到url.txt文件 with open("url.txt", "a", encoding="utf-8") as f: f.write(f"{page_title}\n{page_url}\n\n") print(f"[保存] 标题和URL已保存到url.txt") # 等待5秒后关闭新标签页 print(f"[等待] 5秒后将关闭新标签页") await asyncio.sleep(5) await new_page.close() print(f"[关闭] 新标签页已关闭") except Exception as e: print(f"[错误] 处理新标签页时出错: {str(e)}") try: # 尝试关闭页面,即使出错 await new_page.close() print(f"[关闭] 新标签页已关闭(出错后)") except: pass else: print(f"[警告] 未检测到新标签页打开") # 跳出整个while循环 print(f"[操作] 跳出所有循环,不再处理其他新闻") should_exit_while_loop = True # 设置标志变量 break # 跳出while循环 # 检查是否需要跳出while循环 if should_exit_while_loop: break # 每秒检查一次按钮状态 await page.wait_for_timeout(1000) # 检查是否需要跳出for循环 if should_exit_while_loop: print(f"[操作] 跳出for循环,完全结束处理") break # 跳出for循环 except Exception as e: print(f"处理新闻时出错: {str(e)}") continue # endregion 操作处 print(f"[循环完成] 准备关闭浏览器") # 暂停 1000s await asyncio.sleep(1000) # 这里延迟是为了方便眼睛直观的观看 # 退出前保存 storage 信息 await context.storage_state(path=self.account_file) # 保存cookie baijiahao_logger.info('cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() async def mainAi(self): async with async_playwright() as playwright: await self.ai2video(playwright) ================================================ FILE: uploader/bilibili_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "bilibili_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/bilibili_uploader/main.py ================================================ import json import pathlib import random from biliup.plugins.bili_webup import BiliBili, Data from utils.log import bilibili_logger def extract_keys_from_json(data): """Extract specified keys from the provided JSON data.""" keys_to_extract = ["SESSDATA", "bili_jct", "DedeUserID__ckMd5", "DedeUserID", "access_token"] extracted_data = {} # Extracting cookie data for cookie in data['cookie_info']['cookies']: if cookie['name'] in keys_to_extract: extracted_data[cookie['name']] = cookie['value'] # Extracting access_token if "access_token" in data['token_info']: extracted_data['access_token'] = data['token_info']['access_token'] return extracted_data def read_cookie_json_file(filepath: pathlib.Path): with open(filepath, 'r', encoding='utf-8') as file: content = json.load(file) return content def random_emoji(): emoji_list = ["🍏", "🍎", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒", "🍑", "🍍", "🥭", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥒", "🥬", "🌶", "🌽", "🥕", "🥔", "🍠", "🥐", "🍞", "🥖", "🥨", "🥯", "🧀", "🥚", "🍳", "🥞", "🥓", "🥩", "🍗", "🍖", "🌭", "🍔", "🍟", "🍕", "🥪", "🥙", "🌮", "🌯", "🥗", "🥘", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣", "🍱", "🥟", "🍤", "🍙", "🍚", "🍘", "🍥", "🥮", "🥠", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🍰", "🎂", "🍮", "🍭", "🍬", "🍫", "🍿", "🧂", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕️", "🍵", "🥤", "🍶", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹", "🍾", "🥄", "🍴", "🍽", "🥣", "🥡", "🥢"] return random.choice(emoji_list) class BilibiliUploader(object): def __init__(self, cookie_data, file: pathlib.Path, title, desc, tid, tags, dtime): self.upload_thread_num = 3 self.copyright = 1 self.lines = 'AUTO' self.cookie_data = cookie_data self.file = file self.title = title self.desc = desc self.tid = tid self.tags = tags self.dtime = dtime self._init_data() def _init_data(self): self.data = Data() self.data.copyright = self.copyright self.data.title = self.title self.data.desc = self.desc self.data.tid = self.tid self.data.set_tag(self.tags) self.data.dtime = self.dtime def upload(self): with BiliBili(self.data) as bili: bili.login_by_cookies(self.cookie_data) bili.access_token = self.cookie_data.get('access_token') video_part = bili.upload_file(str(self.file), lines=self.lines, tasks=self.upload_thread_num) # 上传视频,默认线路AUTO自动选择,线程数量3。 video_part['title'] = self.title self.data.append(video_part) ret = bili.submit() # 提交视频 if ret.get('code') == 0: bilibili_logger.success(f'[+] {self.file.name}上传 成功') return True else: bilibili_logger.error(f'[-] {self.file.name}上传 失败, error messge: {ret.get("message")}') return False ================================================ FILE: uploader/douyin_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "douyin_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/douyin_uploader/main.py ================================================ # -*- coding: utf-8 -*- from datetime import datetime from playwright.async_api import Playwright, async_playwright, Page import os import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.log import douyin_logger async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.douyin.com/creator-micro/content/upload") try: await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) except: print("[+] 等待5秒 cookie 失效") await context.close() await browser.close() return False # 2024.06.17 抖音创作者中心改版 if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): print("[+] 等待5秒 cookie 失效") return False else: print("[+] cookie 有效") return True async def douyin_setup(account_file, handle=False): if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: # Todo alert message return False douyin_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') await douyin_cookie_gen(account_file) return True async def douyin_cookie_gen(account_file): async with async_playwright() as playwright: options = { 'headless': LOCAL_CHROME_HEADLESS } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://creator.douyin.com/") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) class DouYinVideo(object): def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None, productLink='', productTitle=''): self.title = title # 视频标题 self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.date_format = '%Y年%m月%d日 %H:%M' self.local_executable_path = LOCAL_CHROME_PATH self.headless = LOCAL_CHROME_HEADLESS self.thumbnail_path = thumbnail_path self.productLink = productLink self.productTitle = productTitle async def set_schedule_time_douyin(self, page, publish_date): # 选择包含特定文本内容的 label 元素 label_element = page.locator("[class^='radio']:has-text('定时发布')") # 在选中的 label 元素下点击 checkbox await label_element.click() await asyncio.sleep(1) publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M") await asyncio.sleep(1) await page.locator('.semi-input[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) async def handle_upload_error(self, page): douyin_logger.info('视频出错了,重新上传中') await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) async def upload(self, playwright: Playwright) -> None: # 使用 Chromium 浏览器启动一个浏览器实例 if self.local_executable_path: browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path) else: browser = await playwright.chromium.launch(headless=self.headless) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context(storage_state=f"{self.account_file}") context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.douyin.com/creator-micro/content/upload") douyin_logger.info(f'[+]正在上传-------{self.title}.mp4') # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 douyin_logger.info(f'[-] 正在打开主页...') await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload") # 点击 "上传视频" 按钮 await page.locator("div[class^='container'] input").set_input_files(self.file_path) # 等待页面跳转到指定的 URL 2025.01.08修改在原有基础上兼容两种页面 while True: try: # 尝试等待第一个 URL await page.wait_for_url( "https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page", timeout=3000) douyin_logger.info("[+] 成功进入version_1发布页面!") break # 成功进入页面后跳出循环 except Exception: try: # 如果第一个 URL 超时,再尝试等待第二个 URL await page.wait_for_url( "https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page", timeout=3000) douyin_logger.info("[+] 成功进入version_2发布页面!") break # 成功进入页面后跳出循环 except: print(" [-] 超时未进入视频发布页面,重新尝试...") await asyncio.sleep(0.5) # 等待 0.5 秒后重新尝试 # 填充标题和话题 # 检查是否存在包含输入框的元素 # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素 await asyncio.sleep(1) douyin_logger.info(f' [-] 正在填充标题和话题...') title_container = page.get_by_text('作品标题').locator("..").locator("xpath=following-sibling::div[1]").locator("input") if await title_container.count(): await title_container.fill(self.title[:30]) else: titlecontainer = page.locator(".notranslate") await titlecontainer.click() await page.keyboard.press("Backspace") await page.keyboard.press("Control+KeyA") await page.keyboard.press("Delete") await page.keyboard.type(self.title) await page.keyboard.press("Enter") css_selector = ".zone-container" for index, tag in enumerate(self.tags, start=1): await page.type(css_selector, "#" + tag) await page.press(css_selector, "Space") douyin_logger.info(f'总共添加{len(self.tags)}个话题') while True: # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 try: # 新版:定位重新上传 number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count() if number > 0: douyin_logger.success(" [-]视频上传完毕") break else: douyin_logger.info(" [-] 正在上传视频中...") await asyncio.sleep(2) if await page.locator('div.progress-div > div:has-text("上传失败")').count(): douyin_logger.error(" [-] 发现上传出错了... 准备重试") await self.handle_upload_error(page) except: douyin_logger.info(" [-] 正在上传视频中...") await asyncio.sleep(2) if self.productLink and self.productTitle: douyin_logger.info(f' [-] 正在设置商品链接...') await self.set_product_link(page, self.productLink, self.productTitle) douyin_logger.info(f' [+] 完成设置商品链接...') #上传视频封面 await self.set_thumbnail(page, self.thumbnail_path) # 更换可见元素 await self.set_location(page, "") # 頭條/西瓜 third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' # 定位是否有第三方平台 if await page.locator(third_part_element).count(): # 检测是否是已选中状态 if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'): await page.locator(third_part_element).locator('input.semi-switch-native-control').click() if self.publish_date != 0: await self.set_schedule_time_douyin(page, self.publish_date) # 判断视频是否发布成功 while True: # 判断视频是否发布成功 try: publish_button = page.get_by_role('button', name="发布", exact=True) if await publish_button.count(): await publish_button.click() await page.wait_for_url("https://creator.douyin.com/creator-micro/content/manage**", timeout=3000) # 如果自动跳转到作品页面,则代表发布成功 douyin_logger.success(" [-]视频发布成功") break except: # 尝试处理封面问题 await self.handle_auto_video_cover(page) douyin_logger.info(" [-] 视频正在发布中...") await page.screenshot(full_page=True) await asyncio.sleep(0.5) await context.storage_state(path=self.account_file) # 保存cookie douyin_logger.success(' [-]cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() async def handle_auto_video_cover(self, page): """ 处理必须设置封面的情况,点击推荐封面的第一个 """ # 1. 判断是否出现 "请设置封面后再发布" 的提示 # 必须确保提示是可见的 (is_visible),因为 DOM 中可能存在隐藏的历史提示 if await page.get_by_text("请设置封面后再发布").first.is_visible(): print(" [-] 检测到需要设置封面提示...") # 2. 定位“智能推荐封面”区域下的第一个封面 # 使用 class^= 前缀匹配,避免 hash 变化导致失效 recommend_cover = page.locator('[class^="recommendCover-"]').first if await recommend_cover.count(): print(" [-] 正在选择第一个推荐封面...") try: await recommend_cover.click() await asyncio.sleep(1) # 等待选中生效 # 3. 处理可能的确认弹窗 "是否确认应用此封面?" # 并不一定每次都会出现,健壮性判断:如果出现弹窗,则点击确定 confirm_text = "是否确认应用此封面?" if await page.get_by_text(confirm_text).first.is_visible(): print(f" [-] 检测到确认弹窗: {confirm_text}") # 直接点击“确定”按钮,不依赖脆弱的 CSS 类名 await page.get_by_role("button", name="确定").click() print(" [-] 已点击确认应用封面") await asyncio.sleep(1) print(" [-] 已完成封面选择流程") return True except Exception as e: print(f" [-] 选择封面失败: {e}") return False async def set_thumbnail(self, page: Page, thumbnail_path: str): if thumbnail_path: douyin_logger.info(' [-] 正在设置视频封面...') await page.click('text="选择封面"') await page.wait_for_selector("div.dy-creator-content-modal") await page.click('text="设置竖封面"') await page.wait_for_timeout(2000) # 等待2秒 # 定位到上传区域并点击 await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path) await page.wait_for_timeout(2000) # 等待2秒 await page.locator("div#tooltip-container button:visible:has-text('完成')").click() # finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')") # if await finish_confirm_element.count(): # await finish_confirm_element.click() # await page.locator("div[class^='footer'] button:has-text('完成')").click() douyin_logger.info(' [+] 视频封面设置完成!') # 等待封面设置对话框关闭 await page.wait_for_selector("div.extractFooter", state='detached') async def set_location(self, page: Page, location: str = ""): if not location: return # todo supoort location later # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator( # "div.semi-select-single").nth(0).click() await page.locator('div.semi-select span:has-text("输入地理位置")').click() await page.keyboard.press("Backspace") await page.wait_for_timeout(2000) await page.keyboard.type(location) await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000) await page.locator('div[role="listbox"] [role="option"]').first.click() async def handle_product_dialog(self, page: Page, product_title: str): """处理商品编辑弹窗""" await page.wait_for_timeout(2000) await page.wait_for_selector('input[placeholder="请输入商品短标题"]', timeout=10000) short_title_input = page.locator('input[placeholder="请输入商品短标题"]') if not await short_title_input.count(): douyin_logger.error("[-] 未找到商品短标题输入框") return False product_title = product_title[:10] await short_title_input.fill(product_title) # 等待一下让界面响应 await page.wait_for_timeout(1000) finish_button = page.locator('button:has-text("完成编辑")') if 'disabled' not in await finish_button.get_attribute('class'): await finish_button.click() douyin_logger.debug("[+] 成功点击'完成编辑'按钮") # 等待对话框关闭 await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000) return True else: douyin_logger.error("[-] '完成编辑'按钮处于禁用状态,尝试直接关闭对话框") # 如果按钮禁用,尝试点击取消或关闭按钮 cancel_button = page.locator('button:has-text("取消")') if await cancel_button.count(): await cancel_button.click() else: # 点击右上角的关闭按钮 close_button = page.locator('.semi-modal-close') await close_button.click() await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000) return False async def set_product_link(self, page: Page, product_link: str, product_title: str): """设置商品链接功能""" await page.wait_for_timeout(2000) # 等待2秒 try: # 定位"添加标签"文本,然后向上导航到容器,再找到下拉框 await page.wait_for_selector('text=添加标签', timeout=10000) dropdown = page.get_by_text('添加标签').locator("..").locator("..").locator("..").locator(".semi-select").first if not await dropdown.count(): douyin_logger.error("[-] 未找到标签下拉框") return False douyin_logger.debug("[-] 找到标签下拉框,准备选择'购物车'") await dropdown.click() ## 等待下拉选项出现 await page.wait_for_selector('[role="listbox"]', timeout=5000) ## 选择"购物车"选项 await page.locator('[role="option"]:has-text("购物车")').click() douyin_logger.debug("[+] 成功选择'购物车'") # 输入商品链接 ## 等待商品链接输入框出现 await page.wait_for_selector('input[placeholder="粘贴商品链接"]', timeout=5000) # 输入 input_field = page.locator('input[placeholder="粘贴商品链接"]') await input_field.fill(product_link) douyin_logger.debug(f"[+] 已输入商品链接: {product_link}") # 点击"添加链接"按钮 add_button = page.locator('span:has-text("添加链接")') ## 检查按钮是否可用(没有disable类) button_class = await add_button.get_attribute('class') if 'disable' in button_class: douyin_logger.error("[-] '添加链接'按钮不可用") return False await add_button.click() douyin_logger.debug("[+] 成功点击'添加链接'按钮") ## 如果链接不可用 await page.wait_for_timeout(2000) error_modal = page.locator('text=未搜索到对应商品') if await error_modal.count(): confirm_button = page.locator('button:has-text("确定")') await confirm_button.click() # await page.wait_for_selector('.semi-modal-content', state='hidden', timeout=5000) douyin_logger.error("[-] 商品链接无效") return False # 填写商品短标题 if not await self.handle_product_dialog(page, product_title): return False # 等待链接添加完成 douyin_logger.debug("[+] 成功设置商品链接") return True except Exception as e: douyin_logger.error(f"[-] 设置商品链接时出错: {str(e)}") return False async def main(self): async with async_playwright() as playwright: await self.upload(playwright) ================================================ FILE: uploader/ks_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "ks_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/ks_uploader/main.py ================================================ # -*- coding: utf-8 -*- from datetime import datetime from playwright.async_api import Playwright, async_playwright import os import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.files_times import get_absolute_path from utils.log import kuaishou_logger async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://cp.kuaishou.com/article/publish/video") try: await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒 kuaishou_logger.info("[+] 等待5秒 cookie 失效") return False except: kuaishou_logger.success("[+] cookie 有效") return True async def ks_setup(account_file, handle=False): account_file = get_absolute_path(account_file, "ks_uploader") if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: return False kuaishou_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') await get_ks_cookie(account_file) return True async def get_ks_cookie(account_file): async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://cp.kuaishou.com") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) class KSVideo(object): def __init__(self, title, file_path, tags, publish_date: datetime, account_file): self.title = title # 视频标题 self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.date_format = '%Y-%m-%d %H:%M' self.local_executable_path = LOCAL_CHROME_PATH self.headless = LOCAL_CHROME_HEADLESS async def handle_upload_error(self, page): kuaishou_logger.error("视频出错了,重新上传中") await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) async def upload(self, playwright: Playwright) -> None: # 使用 Chromium 浏览器启动一个浏览器实例 print(self.local_executable_path) if self.local_executable_path: browser = await playwright.chromium.launch( headless=self.headless, executable_path=self.local_executable_path, ) else: browser = await playwright.chromium.launch( headless=self.headless ) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context(storage_state=f"{self.account_file}") context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://cp.kuaishou.com/article/publish/video") kuaishou_logger.info('正在上传-------{}.mp4'.format(self.title)) # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 kuaishou_logger.info('正在打开主页...') await page.wait_for_url("https://cp.kuaishou.com/article/publish/video") # 点击 "上传视频" 按钮 upload_button = page.locator("button[class^='_upload-btn']") await upload_button.wait_for(state='visible') # 确保按钮可见 async with page.expect_file_chooser() as fc_info: await upload_button.click() file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) await asyncio.sleep(2) # if not await page.get_by_text("封面编辑").count(): # raise Exception("似乎没有跳转到到编辑页面") await asyncio.sleep(1) # 等待按钮可交互 new_feature_button = page.locator('button[type="button"] span:text("我知道了")') if await new_feature_button.count() > 0: await new_feature_button.click() kuaishou_logger.info("正在填充标题和话题...") await page.get_by_text("描述").locator("xpath=following-sibling::div").click() kuaishou_logger.info("clear existing title") await page.keyboard.press("Backspace") await page.keyboard.press("Control+KeyA") await page.keyboard.press("Delete") kuaishou_logger.info("filling new title") await page.keyboard.type(self.title) await page.keyboard.press("Enter") # 快手只能添加3个话题 for index, tag in enumerate(self.tags[:3], start=1): kuaishou_logger.info("正在添加第%s个话题" % index) await page.keyboard.type(f"#{tag} ") await asyncio.sleep(2) max_retries = 60 # 设置最大重试次数,最大等待时间为 2 分钟 retry_count = 0 while retry_count < max_retries: try: # 获取包含 '上传中' 文本的元素数量 number = await page.locator("text=上传中").count() if number == 0: kuaishou_logger.success("视频上传完毕") break else: if retry_count % 5 == 0: kuaishou_logger.info("正在上传视频中...") await asyncio.sleep(2) except Exception as e: kuaishou_logger.error(f"检查上传状态时发生错误: {e}") await asyncio.sleep(2) # 等待 2 秒后重试 retry_count += 1 if retry_count == max_retries: kuaishou_logger.warning("超过最大重试次数,视频上传可能未完成。") # 定时任务 if self.publish_date != 0: await self.set_schedule_time(page, self.publish_date) # 判断视频是否发布成功 while True: try: publish_button = page.get_by_text("发布", exact=True) if await publish_button.count() > 0: await publish_button.click() await asyncio.sleep(1) confirm_button = page.get_by_text("确认发布") if await confirm_button.count() > 0: await confirm_button.click() # 等待页面跳转,确认发布成功 await page.wait_for_url( "https://cp.kuaishou.com/article/manage/video?status=2&from=publish", timeout=5000, ) kuaishou_logger.success("视频发布成功") break except Exception as e: kuaishou_logger.info(f"视频正在发布中... 错误: {e}") await page.screenshot(full_page=True) await asyncio.sleep(1) await context.storage_state(path=self.account_file) # 保存cookie kuaishou_logger.info('cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() async def main(self): async with async_playwright() as playwright: await self.upload(playwright) async def set_schedule_time(self, page, publish_date): kuaishou_logger.info("click schedule") publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M:%S") await page.locator("label:text('发布时间')").locator('xpath=following-sibling::div').locator( '.ant-radio-input').nth(1).click() await asyncio.sleep(1) await page.locator('div.ant-picker-input input[placeholder="选择日期时间"]').click() await asyncio.sleep(1) await page.keyboard.press("Control+KeyA") await page.keyboard.type(str(publish_date_hour)) await page.keyboard.press("Enter") await asyncio.sleep(1) ================================================ FILE: uploader/tencent_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "tencent_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/tencent_uploader/main.py ================================================ # -*- coding: utf-8 -*- from datetime import datetime from playwright.async_api import Playwright, async_playwright import os import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.files_times import get_absolute_path from utils.log import tencent_logger def format_str_for_short_title(origin_title: str) -> str: # 定义允许的特殊字符 allowed_special_chars = "《》“”:+?%°" # 移除不允许的特殊字符 filtered_chars = [char if char.isalnum() or char in allowed_special_chars else ' ' if char == ',' else '' for char in origin_title] formatted_string = ''.join(filtered_chars) # 调整字符串长度 if len(formatted_string) > 16: # 截断字符串 formatted_string = formatted_string[:16] elif len(formatted_string) < 6: # 使用空格来填充字符串 formatted_string += ' ' * (6 - len(formatted_string)) return formatted_string async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://channels.weixin.qq.com/platform/post/create") try: await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒 tencent_logger.error("[+] 等待5秒 cookie 失效") return False except: tencent_logger.success("[+] cookie 有效") return True async def get_tencent_cookie(account_file): async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB' ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options # Pause the page, and start recording manually. context = await set_init_script(context) page = await context.new_page() await page.goto("https://channels.weixin.qq.com") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) async def weixin_setup(account_file, handle=False): account_file = get_absolute_path(account_file, "tencent_uploader") if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: # Todo alert message return False tencent_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') await get_tencent_cookie(account_file) return True class TencentVideo(object): def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None, is_draft=False): self.title = title # 视频标题 self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.category = category self.headless = LOCAL_CHROME_HEADLESS self.is_draft = is_draft # 是否保存为草稿 self.local_executable_path = LOCAL_CHROME_PATH or None async def set_schedule_time_tencent(self, page, publish_date): label_element = page.locator("label").filter(has_text="定时").nth(1) await label_element.click() await page.click('input[placeholder="请选择发表时间"]') str_month = str(publish_date.month) if publish_date.month > 9 else "0" + str(publish_date.month) current_month = str_month + "月" # 获取当前的月份 page_month = await page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")') # 检查当前月份是否与目标月份相同 if page_month != current_month: await page.click('button.weui-desktop-btn__icon__right') # 获取页面元素 elements = await page.query_selector_all('table.weui-desktop-picker__table a') # 遍历元素并点击匹配的元素 for element in elements: if 'weui-desktop-picker__disabled' in await element.evaluate('el => el.className'): continue text = await element.inner_text() if text.strip() == str(publish_date.day): await element.click() break # 输入小时部分(假设选择11小时) await page.click('input[placeholder="请选择时间"]') await page.keyboard.press("Control+KeyA") await page.keyboard.type(str(publish_date.hour)) # 选择标题栏(令定时时间生效) await page.locator("div.input-editor").click() async def handle_upload_error(self, page): tencent_logger.info("视频出错了,重新上传中") await page.locator('div.media-status-content div.tag-inner:has-text("删除")').click() await page.get_by_role('button', name="删除", exact=True).click() file_input = page.locator('input[type="file"]') await file_input.set_input_files(self.file_path) async def upload(self, playwright: Playwright) -> None: # 使用 Chromium (这里使用系统内浏览器,用chromium 会造成h264错误 browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context(storage_state=f"{self.account_file}") context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://channels.weixin.qq.com/platform/post/create") tencent_logger.info(f'[+]正在上传-------{self.title}.mp4') # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 await page.wait_for_url("https://channels.weixin.qq.com/platform/post/create") # await page.wait_for_selector('input[type="file"]', timeout=10000) file_input = page.locator('input[type="file"]') await file_input.set_input_files(self.file_path) # 填充标题和话题 await self.add_title_tags(page) # 添加商品 # await self.add_product(page) # 合集功能 await self.add_collection(page) # 原创选择 await self.add_original(page) # 检测上传状态 await self.detect_upload_status(page) if self.publish_date != 0: await self.set_schedule_time_tencent(page, self.publish_date) # 添加短标题 await self.add_short_title(page) await self.click_publish(page) await context.storage_state(path=f"{self.account_file}") # 保存cookie tencent_logger.success(' [-]cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() async def add_short_title(self, page): short_title_element = page.get_by_text("短标题", exact=True).locator("..").locator( "xpath=following-sibling::div").locator( 'span input[type="text"]') if await short_title_element.count(): short_title = format_str_for_short_title(self.title) await short_title_element.fill(short_title) async def click_publish(self, page): while True: try: if self.is_draft: # 点击"保存草稿"按钮 draft_button = page.locator('div.form-btns button:has-text("保存草稿")') if await draft_button.count(): await draft_button.click() # 等待跳转到草稿箱页面或确认保存成功 await page.wait_for_url("**/post/list**", timeout=5000) # 使用通配符匹配包含post/list的URL tencent_logger.success(" [-]视频草稿保存成功") else: # 点击"发表"按钮 publish_button = page.locator('div.form-btns button:has-text("发表")') if await publish_button.count(): await publish_button.click() await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=5000) tencent_logger.success(" [-]视频发布成功") break except Exception as e: current_url = page.url if self.is_draft: # 检查是否在草稿相关的页面 if "post/list" in current_url or "draft" in current_url: tencent_logger.success(" [-]视频草稿保存成功") break else: # 检查是否在发布列表页面 if "https://channels.weixin.qq.com/platform/post/list" in current_url: tencent_logger.success(" [-]视频发布成功") break tencent_logger.exception(f" [-] Exception: {e}") tencent_logger.info(" [-] 视频正在发布中...") await asyncio.sleep(0.5) async def detect_upload_status(self, page): while True: # 匹配删除按钮,代表视频上传完毕,如果不存在,代表视频正在上传,则等待 try: # 匹配删除按钮,代表视频上传完毕 if "weui-desktop-btn_disabled" not in await page.get_by_role("button", name="发表").get_attribute( 'class'): tencent_logger.info(" [-]视频上传完毕") break else: tencent_logger.info(" [-] 正在上传视频中...") await asyncio.sleep(2) # 出错了视频出错 if await page.locator('div.status-msg.error').count() and await page.locator( 'div.media-status-content div.tag-inner:has-text("删除")').count(): tencent_logger.error(" [-] 发现上传出错了...准备重试") await self.handle_upload_error(page) except: tencent_logger.info(" [-] 正在上传视频中...") await asyncio.sleep(2) async def add_title_tags(self, page): await page.locator("div.input-editor").click() await page.keyboard.type(self.title) await page.keyboard.press("Enter") for index, tag in enumerate(self.tags, start=1): await page.keyboard.type("#" + tag) await page.keyboard.press("Space") tencent_logger.info(f"成功添加hashtag: {len(self.tags)}") async def add_collection(self, page): collection_elements = page.get_by_text("添加到合集").locator("xpath=following-sibling::div").locator( '.option-list-wrap > div') if await collection_elements.count() > 1: await page.get_by_text("添加到合集").locator("xpath=following-sibling::div").click() await collection_elements.first.click() async def add_original(self, page): if await page.get_by_label("视频为原创").count(): await page.get_by_label("视频为原创").check() # 检查 "我已阅读并同意 《视频号原创声明使用条款》" 元素是否存在 label_locator = await page.locator('label:has-text("我已阅读并同意 《视频号原创声明使用条款》")').is_visible() if label_locator: await page.get_by_label("我已阅读并同意 《视频号原创声明使用条款》").check() await page.get_by_role("button", name="声明原创").click() # 2023年11月20日 wechat更新: 可能新账号或者改版账号,出现新的选择页面 if await page.locator('div.label span:has-text("声明原创")').count() and self.category: # 因处罚无法勾选原创,故先判断是否可用 if not await page.locator('div.declare-original-checkbox input.ant-checkbox-input').is_disabled(): await page.locator('div.declare-original-checkbox input.ant-checkbox-input').click() if not await page.locator( 'div.declare-original-dialog label.ant-checkbox-wrapper.ant-checkbox-wrapper-checked:visible').count(): await page.locator('div.declare-original-dialog input.ant-checkbox-input:visible').click() if await page.locator('div.original-type-form > div.form-label:has-text("原创类型"):visible').count(): await page.locator('div.form-content:visible').click() # 下拉菜单 await page.locator( f'div.form-content:visible ul.weui-desktop-dropdown__list li.weui-desktop-dropdown__list-ele:has-text("{self.category}")').first.click() await page.wait_for_timeout(1000) if await page.locator('button:has-text("声明原创"):visible').count(): await page.locator('button:has-text("声明原创"):visible').click() async def main(self): async with async_playwright() as playwright: await self.upload(playwright) ================================================ FILE: uploader/tk_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "tk_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/tk_uploader/main.py ================================================ # -*- coding: utf-8 -*- import re from datetime import datetime from playwright.async_api import Playwright, async_playwright import os import asyncio from uploader.tk_uploader.tk_config import Tk_Locator from utils.base_social_media import set_init_script from utils.files_times import get_absolute_path from utils.log import tiktok_logger from conf import LOCAL_CHROME_HEADLESS async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.firefox.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en") await page.wait_for_load_state('networkidle') try: # 选择所有的 select 元素 select_elements = await page.query_selector_all('select') for element in select_elements: class_name = await element.get_attribute('class') # 使用正则表达式匹配特定模式的 class 名称 if re.match(r'tiktok-.*-SelectFormContainer.*', class_name): tiktok_logger.error("[+] cookie expired") return False tiktok_logger.success("[+] cookie valid") return True except: tiktok_logger.success("[+] cookie valid") return True async def tiktok_setup(account_file, handle=False): account_file = get_absolute_path(account_file, "tk_uploader") if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: return False tiktok_logger.info('[+] cookie file is not existed or expired. Now open the browser auto. Please login with your way(gmail phone, whatever, the cookie file will generated after login') await get_tiktok_cookie(account_file) return True async def get_tiktok_cookie(account_file): async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB', ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.firefox.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://www.tiktok.com/login?lang=en") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) class TiktokVideo(object): def __init__(self, title, file_path, tags, publish_date, account_file): self.title = title self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.headless = LOCAL_CHROME_HEADLESS self.locator_base = None async def set_schedule_time(self, page, publish_date): schedule_input_element = self.locator_base.get_by_label('Schedule') await schedule_input_element.wait_for(state='visible') # 确保按钮可见 await schedule_input_element.click() scheduled_picker = self.locator_base.locator('div.scheduled-picker') await scheduled_picker.locator('div.TUXInputBox').nth(1).click() calendar_month = await self.locator_base.locator('div.calendar-wrapper span.month-title').inner_text() n_calendar_month = datetime.strptime(calendar_month, '%B').month schedule_month = publish_date.month if n_calendar_month != schedule_month: if n_calendar_month < schedule_month: arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1) else: arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0) await arrow.click() # day set valid_days_locator = self.locator_base.locator( 'div.calendar-wrapper span.day.valid') valid_days = await valid_days_locator.count() for i in range(valid_days): day_element = valid_days_locator.nth(i) text = await day_element.inner_text() if text.strip() == str(publish_date.day): await day_element.click() break # time set await scheduled_picker.locator('div.TUXInputBox').nth(0).click() hour_str = publish_date.strftime("%H") correct_minute = int(publish_date.minute / 5) minute_str = f"{correct_minute:02d}" hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')" minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')" # pick hour first await self.locator_base.locator(hour_selector).click() # click time button again # 等待某个特定的元素出现或状态变化,表明UI已更新 await page.wait_for_timeout(1000) # 等待500毫秒 await scheduled_picker.locator('div.TUXInputBox').nth(0).click() # pick minutes after await self.locator_base.locator(minute_selector).click() # click title to remove the focus. await self.locator_base.locator("h1:has-text('Upload video')").click() async def handle_upload_error(self, page): tiktok_logger.info("video upload error retrying.") select_file_button = self.locator_base.locator('button[aria-label="Select file"]') async with page.expect_file_chooser() as fc_info: await select_file_button.click() file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) async def upload(self, playwright: Playwright) -> None: browser = await playwright.firefox.launch(headless=self.headless) context = await browser.new_context(storage_state=f"{self.account_file}") context = await set_init_script(context) page = await context.new_page() await page.goto("https://www.tiktok.com/creator-center/upload") tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4') await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000) try: await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000) tiktok_logger.info("Either iframe or div appeared.") except Exception as e: tiktok_logger.error("Neither iframe nor div appeared within the timeout.") await self.choose_base_locator(page) upload_button = self.locator_base.locator( 'button:has-text("Select video"):visible') await upload_button.wait_for(state='visible') # 确保按钮可见 async with page.expect_file_chooser() as fc_info: await upload_button.click() file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) await self.add_title_tags(page) # detact upload status await self.detect_upload_status(page) if self.publish_date != 0: await self.set_schedule_time(page, self.publish_date) await self.click_publish(page) await context.storage_state(path=f"{self.account_file}") # save cookie tiktok_logger.info(' [-] update cookie!') await asyncio.sleep(2) # close delay for look the video status # close all await context.close() await browser.close() async def add_title_tags(self, page): editor_locator = self.locator_base.locator('div.public-DraftEditor-content') await editor_locator.click() await page.keyboard.press("End") await page.keyboard.press("Control+A") await page.keyboard.press("Delete") await page.keyboard.press("End") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.insert_text(self.title) await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.press("End") await page.keyboard.press("Enter") # tag part for index, tag in enumerate(self.tags, start=1): tiktok_logger.info("Setting the %s tag" % index) await page.keyboard.press("End") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.insert_text("#" + tag + " ") await page.keyboard.press("Space") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.press("Backspace") await page.keyboard.press("End") async def click_publish(self, page): success_flag_div = '#\\:r9\\:' while True: try: publish_button = self.locator_base.locator('div.btn-post') if await publish_button.count(): await publish_button.click() await self.locator_base.locator(success_flag_div).wait_for(state="visible", timeout=3000) tiktok_logger.success(" [-] video published success") break except Exception as e: if await self.locator_base.locator(success_flag_div).count(): tiktok_logger.success(" [-]video published success") break else: tiktok_logger.exception(f" [-] Exception: {e}") tiktok_logger.info(" [-] video publishing") await page.screenshot(full_page=True) await asyncio.sleep(0.5) async def detect_upload_status(self, page): while True: try: if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None: tiktok_logger.info(" [-]video uploaded.") break else: tiktok_logger.info(" [-] video uploading...") await asyncio.sleep(2) if await self.locator_base.locator('button[aria-label="Select file"]').count(): tiktok_logger.info(" [-] found some error while uploading now retry...") await self.handle_upload_error(page) except: tiktok_logger.info(" [-] video uploading...") await asyncio.sleep(2) async def choose_base_locator(self, page): # await page.wait_for_selector('div.upload-container') if await page.locator('iframe[data-tt="Upload_index_iframe"]').count(): self.locator_base = self.locator_base else: self.locator_base = page.locator(Tk_Locator.default) async def main(self): async with async_playwright() as playwright: await self.upload(playwright) ================================================ FILE: uploader/tk_uploader/main_chrome.py ================================================ # -*- coding: utf-8 -*- import re from datetime import datetime from playwright.async_api import Playwright, async_playwright import os import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from uploader.tk_uploader.tk_config import Tk_Locator from utils.base_social_media import set_init_script from utils.files_times import get_absolute_path from utils.log import tiktok_logger async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en") await page.wait_for_load_state('networkidle') try: # 选择所有的 select 元素 select_elements = await page.query_selector_all('select') for element in select_elements: class_name = await element.get_attribute('class') # 使用正则表达式匹配特定模式的 class 名称 if re.match(r'tiktok-.*-SelectFormContainer.*', class_name): tiktok_logger.error("[+] cookie expired") return False tiktok_logger.success("[+] cookie valid") return True except: tiktok_logger.success("[+] cookie valid") return True async def tiktok_setup(account_file, handle=False): account_file = get_absolute_path(account_file, "tk_uploader") if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: return False tiktok_logger.info('[+] cookie file is not existed or expired. Now open the browser auto. Please login with your way(gmail phone, whatever, the cookie file will generated after login') await get_tiktok_cookie(account_file) return True async def get_tiktok_cookie(account_file): async with async_playwright() as playwright: options = { 'args': [ '--lang en-GB', ], 'headless': LOCAL_CHROME_HEADLESS, # Set headless option here } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://www.tiktok.com/login?lang=en") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) class TiktokVideo(object): def __init__(self, title, file_path, tags, publish_date, account_file, thumbnail_path=None): self.title = title self.file_path = file_path self.tags = tags self.publish_date = publish_date self.thumbnail_path = thumbnail_path self.account_file = account_file self.local_executable_path = LOCAL_CHROME_PATH self.headless = LOCAL_CHROME_HEADLESS self.locator_base = None async def set_schedule_time(self, page, publish_date): schedule_input_element = self.locator_base.get_by_label('Schedule') await schedule_input_element.wait_for(state='visible') # 确保按钮可见 await schedule_input_element.click(force=True) if await self.locator_base.locator('div.TUXButton-content >> text=Allow').count(): await self.locator_base.locator('div.TUXButton-content >> text=Allow').click() scheduled_picker = self.locator_base.locator('div.scheduled-picker') await scheduled_picker.locator('div.TUXInputBox').nth(1).click() calendar_month = await self.locator_base.locator( 'div.calendar-wrapper span.month-title').inner_text() n_calendar_month = datetime.strptime(calendar_month, '%B').month schedule_month = publish_date.month if n_calendar_month != schedule_month: if n_calendar_month < schedule_month: arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1) else: arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0) await arrow.click() # day set valid_days_locator = self.locator_base.locator( 'div.calendar-wrapper span.day.valid') valid_days = await valid_days_locator.count() for i in range(valid_days): day_element = valid_days_locator.nth(i) text = await day_element.inner_text() if text.strip() == str(publish_date.day): await day_element.click() break # time set await scheduled_picker.locator('div.TUXInputBox').nth(0).click() hour_str = publish_date.strftime("%H") correct_minute = int(publish_date.minute / 5) minute_str = f"{correct_minute:02d}" hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')" minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')" # pick hour first await page.wait_for_timeout(1000) # 等待500毫秒 await self.locator_base.locator(hour_selector).click() # click time button again await page.wait_for_timeout(1000) # 等待500毫秒 # pick minutes after await self.locator_base.locator(minute_selector).click() # click title to remove the focus. # await self.locator_base.locator("h1:has-text('Upload video')").click() async def handle_upload_error(self, page): tiktok_logger.info("video upload error retrying.") select_file_button = self.locator_base.locator('button[aria-label="Select file"]') async with page.expect_file_chooser() as fc_info: await select_file_button.click() file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) async def upload(self, playwright: Playwright) -> None: browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path) context = await browser.new_context(storage_state=f"{self.account_file}") # context = await set_init_script(context) page = await context.new_page() # change language to eng first await self.change_language(page) await page.goto("https://www.tiktok.com/tiktokstudio/upload") tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4') await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000) try: await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000) tiktok_logger.info("Either iframe or div appeared.") except Exception as e: tiktok_logger.error("Neither iframe nor div appeared within the timeout.") await self.choose_base_locator(page) upload_button = self.locator_base.locator( 'button:has-text("Select video"):visible') await upload_button.wait_for(state='visible') # 确保按钮可见 async with page.expect_file_chooser() as fc_info: await upload_button.click() file_chooser = await fc_info.value await file_chooser.set_files(self.file_path) await self.add_title_tags(page) # detect upload status await self.detect_upload_status(page) if self.thumbnail_path: tiktok_logger.info(f'[+] Uploading thumbnail file {self.title}.png') await self.upload_thumbnails(page) if self.publish_date != 0: await self.set_schedule_time(page, self.publish_date) await self.click_publish(page) tiktok_logger.success(f"video_id: {await self.get_last_video_id(page)}") await context.storage_state(path=f"{self.account_file}") # save cookie tiktok_logger.info(' [-] update cookie!') await asyncio.sleep(2) # close delay for look the video status # close all await context.close() await browser.close() async def add_title_tags(self, page): editor_locator = self.locator_base.locator('div.public-DraftEditor-content') await editor_locator.click() await page.keyboard.press("End") await page.keyboard.press("Control+A") await page.keyboard.press("Delete") await page.keyboard.press("End") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.insert_text(self.title) await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.press("End") await page.keyboard.press("Enter") # tag part for index, tag in enumerate(self.tags, start=1): tiktok_logger.info("Setting the %s tag" % index) await page.keyboard.press("End") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.insert_text("#" + tag + " ") await page.keyboard.press("Space") await page.wait_for_timeout(1000) # 等待1秒 await page.keyboard.press("Backspace") await page.keyboard.press("End") async def upload_thumbnails(self, page): await self.locator_base.locator(".cover-container").click() await self.locator_base.locator(".cover-edit-container >> text=Upload cover").click() async with page.expect_file_chooser() as fc_info: await self.locator_base.locator(".upload-image-upload-area").click() file_chooser = await fc_info.value await file_chooser.set_files(self.thumbnail_path) await self.locator_base.locator('div.cover-edit-panel:not(.hide-panel)').get_by_role( "button", name="Confirm").click() await page.wait_for_timeout(3000) # wait 3s, fix it later async def change_language(self, page): # set the language to english await page.goto("https://www.tiktok.com") await page.wait_for_load_state('domcontentloaded') await page.wait_for_selector('[data-e2e="nav-more-menu"]') # 已经设置为英文, 省略这个步骤 if await page.locator('[data-e2e="nav-more-menu"]').text_content() == "More": return await page.locator('[data-e2e="nav-more-menu"]').click() await page.locator('[data-e2e="language-select"]').click() await page.locator('#creator-tools-selection-menu-header >> text=English (US)').click() async def click_publish(self, page): success_flag_div = 'div.common-modal-confirm-modal' while True: try: publish_button = self.locator_base.locator('div.button-group button').nth(0) if await publish_button.count(): await publish_button.click() await page.wait_for_url("https://www.tiktok.com/tiktokstudio/content", timeout=3000) tiktok_logger.success(" [-] video published success") break except Exception as e: tiktok_logger.exception(f" [-] Exception: {e}") tiktok_logger.info(" [-] video publishing") await asyncio.sleep(0.5) async def get_last_video_id(self, page): await page.wait_for_selector('div[data-tt="components_PostTable_Container"]') video_list_locator = self.locator_base.locator('div[data-tt="components_PostTable_Container"] div[data-tt="components_PostInfoCell_Container"] a') if await video_list_locator.count(): first_video_obj = await video_list_locator.nth(0).get_attribute('href') video_id = re.search(r'video/(\d+)', first_video_obj).group(1) if first_video_obj else None return video_id async def detect_upload_status(self, page): while True: try: # if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None: if await self.locator_base.locator( 'div.button-group > button >> text=Post').get_attribute("disabled") is None: tiktok_logger.info(" [-]video uploaded.") break else: tiktok_logger.info(" [-] video uploading...") await asyncio.sleep(2) if await self.locator_base.locator( 'button[aria-label="Select file"]').count(): tiktok_logger.info(" [-] found some error while uploading now retry...") await self.handle_upload_error(page) except: tiktok_logger.info(" [-] video uploading...") await asyncio.sleep(2) async def choose_base_locator(self, page): # await page.wait_for_selector('div.upload-container') if await page.locator('iframe[data-tt="Upload_index_iframe"]').count(): self.locator_base = page.frame_locator(Tk_Locator.tk_iframe) else: self.locator_base = page.locator(Tk_Locator.default) async def main(self): async with async_playwright() as playwright: await self.upload(playwright) ================================================ FILE: uploader/tk_uploader/tk_config.py ================================================ class Tk_Locator(object): tk_iframe = '[data-tt="Upload_index_iframe"]' default = 'body' ================================================ FILE: uploader/xhs_uploader/__init__.py ================================================ ================================================ FILE: uploader/xhs_uploader/accounts.ini ================================================ [account1] cookies = changeme ================================================ FILE: uploader/xhs_uploader/main.py ================================================ import configparser import json import pathlib from time import sleep import requests from playwright.sync_api import sync_playwright from conf import BASE_DIR, XHS_SERVER, LOCAL_CHROME_HEADLESS config = configparser.RawConfigParser() config.read('accounts.ini') def sign_local(uri, data=None, a1="", web_session=""): for _ in range(10): try: with sync_playwright() as playwright: stealth_js_path = pathlib.Path(BASE_DIR / "utils/stealth.min.js") chromium = playwright.chromium # 如果一直失败可尝试设置成 False 让其打开浏览器,适当添加 sleep 可查看浏览器状态 browser = chromium.launch(headless=LOCAL_CHROME_HEADLESS) browser_context = browser.new_context() browser_context.add_init_script(path=stealth_js_path) context_page = browser_context.new_page() context_page.goto("https://www.xiaohongshu.com") browser_context.add_cookies([ {'name': 'a1', 'value': a1, 'domain': ".xiaohongshu.com", 'path': "/"}] ) context_page.reload() # 这个地方设置完浏览器 cookie 之后,如果这儿不 sleep 一下签名获取就失败了,如果经常失败请设置长一点试试 sleep(2) encrypt_params = context_page.evaluate("([url, data]) => window._webmsxyw(url, data)", [uri, data]) return { "x-s": encrypt_params["X-s"], "x-t": str(encrypt_params["X-t"]) } except Exception: # 这儿有时会出现 window._webmsxyw is not a function 或未知跳转错误,因此加一个失败重试趴 pass raise Exception("重试了这么多次还是无法签名成功,寄寄寄") def sign(uri, data=None, a1="", web_session=""): # 填写自己的 flask 签名服务端口地址 res = requests.post(f"{XHS_SERVER}/sign", json={"uri": uri, "data": data, "a1": a1, "web_session": web_session}) signs = res.json() return { "x-s": signs["x-s"], "x-t": signs["x-t"] } def beauty_print(data: dict): print(json.dumps(data, ensure_ascii=False, indent=2)) ================================================ FILE: uploader/xhs_uploader/xhs_login_qrcode.py ================================================ import datetime import json import qrcode from time import sleep from xhs import XhsClient from uploader.xhs_uploader.main import sign # pip install qrcode if __name__ == '__main__': xhs_client = XhsClient(sign=sign, timeout=60) print(datetime.datetime.now()) qr_res = xhs_client.get_qrcode() qr_id = qr_res["qr_id"] qr_code = qr_res["code"] qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L, box_size=50, border=1) qr.add_data(qr_res["url"]) qr.make() qr.print_ascii() while True: check_qrcode = xhs_client.check_qrcode(qr_id, qr_code) print(check_qrcode) sleep(1) if check_qrcode["code_status"] == 2: print(json.dumps(check_qrcode["login_info"], indent=4)) print("当前 cookie:" + xhs_client.cookie) break print(json.dumps(xhs_client.get_self_info(), indent=4)) ================================================ FILE: uploader/xiaohongshu_uploader/__init__.py ================================================ from pathlib import Path from conf import BASE_DIR Path(BASE_DIR / "cookies" / "xiaohongshu_uploader").mkdir(exist_ok=True) ================================================ FILE: uploader/xiaohongshu_uploader/main.py ================================================ # -*- coding: utf-8 -*- from datetime import datetime from playwright.async_api import Playwright, async_playwright, Page import os import asyncio from conf import LOCAL_CHROME_PATH, LOCAL_CHROME_HEADLESS from utils.base_social_media import set_init_script from utils.log import xiaohongshu_logger async def cookie_auth(account_file): async with async_playwright() as playwright: browser = await playwright.chromium.launch(headless=LOCAL_CHROME_HEADLESS) context = await browser.new_context(storage_state=account_file) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.xiaohongshu.com/creator-micro/content/upload") try: await page.wait_for_url("https://creator.xiaohongshu.com/creator-micro/content/upload", timeout=5000) except: print("[+] 等待5秒 cookie 失效") await context.close() await browser.close() return False # 2024.06.17 抖音创作者中心改版 if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): print("[+] 等待5秒 cookie 失效") return False else: print("[+] cookie 有效") return True async def xiaohongshu_setup(account_file, handle=False): if not os.path.exists(account_file) or not await cookie_auth(account_file): if not handle: # Todo alert message return False xiaohongshu_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') await xiaohongshu_cookie_gen(account_file) return True async def xiaohongshu_cookie_gen(account_file): async with async_playwright() as playwright: options = { 'headless': LOCAL_CHROME_HEADLESS } # Make sure to run headed. browser = await playwright.chromium.launch(**options) # Setup context however you like. context = await browser.new_context() # Pass any options context = await set_init_script(context) # Pause the page, and start recording manually. page = await context.new_page() await page.goto("https://creator.xiaohongshu.com/") await page.pause() # 点击调试器的继续,保存cookie await context.storage_state(path=account_file) class XiaoHongShuVideo(object): def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None): self.title = title # 视频标题 self.file_path = file_path self.tags = tags self.publish_date = publish_date self.account_file = account_file self.date_format = '%Y年%m月%d日 %H:%M' self.local_executable_path = LOCAL_CHROME_PATH self.headless = LOCAL_CHROME_HEADLESS self.thumbnail_path = thumbnail_path async def set_schedule_time_xiaohongshu(self, page, publish_date): print(" [-] 正在设置定时发布时间...") print(f"publish_date: {publish_date}") # 使用文本内容定位元素 # element = await page.wait_for_selector( # 'label:has-text("定时发布")', # timeout=5000 # 5秒超时时间 # ) # await element.click() # # 选择包含特定文本内容的 label 元素 label_element = page.locator("label:has-text('定时发布')") # # 在选中的 label 元素下点击 checkbox await label_element.click() await asyncio.sleep(1) publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M") print(f"publish_date_hour: {publish_date_hour}") await asyncio.sleep(1) 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) async def handle_upload_error(self, page): xiaohongshu_logger.info('视频出错了,重新上传中') await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) async def upload(self, playwright: Playwright) -> None: # 使用 Chromium 浏览器启动一个浏览器实例 if self.local_executable_path: browser = await playwright.chromium.launch(headless=self.headless, executable_path=self.local_executable_path) else: browser = await playwright.chromium.launch(headless=self.headless) # 创建一个浏览器上下文,使用指定的 cookie 文件 context = await browser.new_context( viewport={"width": 1600, "height": 900}, storage_state=f"{self.account_file}" ) context = await set_init_script(context) # 创建一个新的页面 page = await context.new_page() # 访问指定的 URL await page.goto("https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video") xiaohongshu_logger.info(f'[+]正在上传-------{self.title}.mp4') # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 xiaohongshu_logger.info(f'[-] 正在打开主页...') await page.wait_for_url("https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video") # 点击 "上传视频" 按钮 await page.locator("div[class^='upload-content'] input[class='upload-input']").set_input_files(self.file_path) # 等待页面跳转到指定的 URL 2025.01.08修改在原有基础上兼容两种页面 while True: try: # 等待upload-input元素出现 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: # 在preview-new元素中查找包含"上传成功"的stage元素 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: xiaohongshu_logger.info("[+] 检测到上传成功标识!") break # 成功检测到上传成功后跳出循环 else: print(" [-] 未找到上传成功标识,继续等待...") else: print(" [-] 未找到预览元素,继续等待...") await asyncio.sleep(1) except Exception as e: print(f" [-] 检测过程出错: {str(e)},重新尝试...") await asyncio.sleep(0.5) # 等待0.5秒后重新尝试 # 填充标题和话题 # 检查是否存在包含输入框的元素 # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素 await asyncio.sleep(1) xiaohongshu_logger.info(f' [-] 正在填充标题和话题...') title_container = page.locator('div.plugin.title-container').locator('input.d-text') if await title_container.count(): await title_container.fill(self.title[:30]) else: titlecontainer = page.locator(".notranslate") await titlecontainer.click() await page.keyboard.press("Backspace") await page.keyboard.press("Control+KeyA") await page.keyboard.press("Delete") await page.keyboard.type(self.title) await page.keyboard.press("Enter") css_selector = ".ql-editor" # 不能加上 .ql-blank 属性,这样只能获取第一次非空状态 for index, tag in enumerate(self.tags, start=1): await page.type(css_selector, "#" + tag) await page.press(css_selector, "Space") xiaohongshu_logger.info(f'总共添加{len(self.tags)}个话题') # while True: # # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 # try: # # 新版:定位重新上传 # number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count() # if number > 0: # xiaohongshu_logger.success(" [-]视频上传完毕") # break # else: # xiaohongshu_logger.info(" [-] 正在上传视频中...") # await asyncio.sleep(2) # if await page.locator('div.progress-div > div:has-text("上传失败")').count(): # xiaohongshu_logger.error(" [-] 发现上传出错了... 准备重试") # await self.handle_upload_error(page) # except: # xiaohongshu_logger.info(" [-] 正在上传视频中...") # await asyncio.sleep(2) # 上传视频封面 # await self.set_thumbnail(page, self.thumbnail_path) # 更换可见元素 # await self.set_location(page, "青岛市") # # 頭條/西瓜 # third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' # # 定位是否有第三方平台 # if await page.locator(third_part_element).count(): # # 检测是否是已选中状态 # if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'): # await page.locator(third_part_element).locator('input.semi-switch-native-control').click() if self.publish_date != 0: await self.set_schedule_time_xiaohongshu(page, self.publish_date) # 判断视频是否发布成功 while True: try: # 等待包含"定时发布"文本的button元素出现并点击 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 ) # 如果自动跳转到作品页面,则代表发布成功 xiaohongshu_logger.success(" [-]视频发布成功") break except: xiaohongshu_logger.info(" [-] 视频正在发布中...") await page.screenshot(full_page=True) await asyncio.sleep(0.5) await context.storage_state(path=self.account_file) # 保存cookie xiaohongshu_logger.success(' [-]cookie更新完毕!') await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 # 关闭浏览器上下文和浏览器实例 await context.close() await browser.close() async def set_thumbnail(self, page: Page, thumbnail_path: str): if thumbnail_path: await page.click('text="选择封面"') await page.wait_for_selector("div.semi-modal-content:visible") await page.click('text="设置竖封面"') await page.wait_for_timeout(2000) # 等待2秒 # 定位到上传区域并点击 await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path) await page.wait_for_timeout(2000) # 等待2秒 await page.locator("div[class^='extractFooter'] button:visible:has-text('完成')").click() # finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')") # if await finish_confirm_element.count(): # await finish_confirm_element.click() # await page.locator("div[class^='footer'] button:has-text('完成')").click() async def set_location(self, page: Page, location: str = "青岛市"): print(f"开始设置位置: {location}") # 点击地点输入框 print("等待地点输入框加载...") loc_ele = await page.wait_for_selector('div.d-text.d-select-placeholder.d-text-ellipsis.d-text-nowrap') print(f"已定位到地点输入框: {loc_ele}") await loc_ele.click() print("点击地点输入框完成") # 输入位置名称 print(f"等待1秒后输入位置名称: {location}") await page.wait_for_timeout(1000) await page.keyboard.type(location) print(f"位置名称输入完成: {location}") # 等待下拉列表加载 print("等待下拉列表加载...") dropdown_selector = 'div.d-popover.d-popover-default.d-dropdown.--size-min-width-large' await page.wait_for_timeout(3000) try: await page.wait_for_selector(dropdown_selector, timeout=3000) print("下拉列表已加载") except: print("下拉列表未按预期显示,可能结构已变化") # 增加等待时间以确保内容加载完成 print("额外等待1秒确保内容渲染完成...") await page.wait_for_timeout(1000) # 尝试更灵活的XPath选择器 print("尝试使用更灵活的XPath选择器...") flexible_xpath = ( f'//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' f'//div[contains(@class, "d-options-wrapper")]' f'//div[contains(@class, "d-grid") and contains(@class, "d-options")]' f'//div[contains(@class, "name") and text()="{location}"]' ) await page.wait_for_timeout(3000) # 尝试定位元素 print(f"尝试定位包含'{location}'的选项...") try: # 先尝试使用更灵活的选择器 location_option = await page.wait_for_selector( flexible_xpath, timeout=3000 ) if location_option: print(f"使用灵活选择器定位成功: {location_option}") else: # 如果灵活选择器失败,再尝试原选择器 print("灵活选择器未找到元素,尝试原始选择器...") location_option = await page.wait_for_selector( f'//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' f'//div[contains(@class, "d-options-wrapper")]' f'//div[contains(@class, "d-grid") and contains(@class, "d-options")]' f'/div[1]//div[contains(@class, "name") and text()="{location}"]', timeout=2000 ) # 滚动到元素并点击 print("滚动到目标选项...") await location_option.scroll_into_view_if_needed() print("元素已滚动到视图内") # 增加元素可见性检查 is_visible = await location_option.is_visible() print(f"目标选项是否可见: {is_visible}") # 点击元素 print("准备点击目标选项...") await location_option.click() print(f"成功选择位置: {location}") return True except Exception as e: print(f"定位位置失败: {e}") # 打印更多调试信息 print("尝试获取下拉列表中的所有选项...") try: all_options = await page.query_selector_all( '//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' '//div[contains(@class, "d-options-wrapper")]' '//div[contains(@class, "d-grid") and contains(@class, "d-options")]' '/div' ) print(f"找到 {len(all_options)} 个选项") # 打印前3个选项的文本内容 for i, option in enumerate(all_options[:3]): option_text = await option.inner_text() print(f"选项 {i+1}: {option_text.strip()[:50]}...") except Exception as e: print(f"获取选项列表失败: {e}") # 截图保存(取消注释使用) # await page.screenshot(path=f"location_error_{location}.png") return False async def main(self): async with async_playwright() as playwright: await self.upload(playwright) ================================================ FILE: utils/__init__.py ================================================ ================================================ FILE: utils/base_social_media.py ================================================ from pathlib import Path from typing import List from conf import BASE_DIR SOCIAL_MEDIA_DOUYIN = "douyin" SOCIAL_MEDIA_TENCENT = "tencent" SOCIAL_MEDIA_TIKTOK = "tiktok" SOCIAL_MEDIA_BILIBILI = "bilibili" SOCIAL_MEDIA_KUAISHOU = "kuaishou" def get_supported_social_media() -> List[str]: return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU] def get_cli_action() -> List[str]: return ["upload", "login", "watch"] async def set_init_script(context): stealth_js_path = Path(BASE_DIR / "utils/stealth.min.js") await context.add_init_script(path=stealth_js_path) return context ================================================ FILE: utils/browser_hook.py ================================================ from conf import LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH def get_browser_options(): options = { 'headless': LOCAL_CHROME_HEADLESS, 'args': [ '--disable-blink-features=AutomationControlled', '--lang=zh-CN', '--disable-infobars', '--start-maximized', '--no-sandbox', '--disable-web-security' ] } if LOCAL_CHROME_PATH: options['executable_path'] = LOCAL_CHROME_PATH return options ================================================ FILE: utils/constant.py ================================================ import enum class TencentZoneTypes(enum.Enum): LIFESTYLE = '生活' CUTE_KIDS = '萌娃' MUSIC = '音乐' KNOWLEDGE = '知识' EMOTION = '情感' TRAVEL_SCENERY = '旅行风景' FASHION = '时尚' FOOD = '美食' LIFE_HACKS = '生活技巧' DANCE = '舞蹈' MOVIES_TV_SHOWS = '影视综艺' SPORTS = '运动' FUNNY = '搞笑' CELEBRITIES = '明星名人' NEWS_INFO = '新闻资讯' GAMING = '游戏' AUTOMOTIVE = '车' ANIME = '二次元' TALENT = '才艺' CUTE_PETS = '萌宠' INDUSTRY_MACHINERY_CONSTRUCTION = '机械' ANIMALS = '动物' PARENTING = '育儿' TECHNOLOGY = '科技' class VideoZoneTypes(enum.Enum): """ 所有分区枚举 - MAINPAGE: 主页 - ANIME: 番剧 - ANIME_SERIAL: 连载中番剧 - ANIME_FINISH: 已完结番剧 - ANIME_INFORMATION: 资讯 - ANIME_OFFICAL: 官方延伸 - MOVIE: 电影 - GUOCHUANG: 国创 - GUOCHUANG_CHINESE: 国产动画 - GUOCHUANG_ORIGINAL: 国产原创相关 - GUOCHUANG_PUPPETRY: 布袋戏 - GUOCHUANG_MOTIONCOMIC: 动态漫·广播剧 - GUOCHUANG_INFORMATION: 资讯 - TELEPLAY: 电视剧 - DOCUMENTARY: 纪录片 - DOUGA: 动画 - DOUGA_MAD: MAD·AMV - DOUGA_MMD: MMD·3D - DOUGA_VOICE: 短片·手书·配音 - DOUGA_GARAGE_KIT: 手办·模玩 - DOUGA_TOKUSATSU: 特摄 - DOUGA_ACGNTALKS: 动漫杂谈 - DOUGA_OTHER: 综合 - GAME: 游戏 - GAME_STAND_ALONE: 单机游戏 - GAME_ESPORTS: 电子竞技 - GAME_MOBILE: 手机游戏 - GAME_ONLINE: 网络游戏 - GAME_BOARD: 桌游棋牌 - GAME_GMV: GMV - GAME_MUSIC: 音游 - GAME_MUGEN: Mugen - KICHIKU: 鬼畜 - KICHIKU_GUIDE: 鬼畜调教 - KICHIKU_MAD: 音MAD - KICHIKU_MANUAL_VOCALOID: 人力VOCALOID - KICHIKU_THEATRE: 鬼畜剧场 - KICHIKU_COURSE: 教程演示 - MUSIC: 音乐 - MUSIC_ORIGINAL: 原创音乐 - MUSIC_COVER: 翻唱 - MUSIC_PERFORM: 演奏 - MUSIC_VOCALOID: VOCALOID·UTAU - MUSIC_LIVE: 音乐现场 - MUSIC_MV: MV - MUSIC_COMMENTARY: 乐评盘点 - MUSIC_TUTORIAL: 音乐教学 - MUSIC_OTHER: 音乐综合 - DANCE: 舞蹈 - DANCE_OTAKU: 宅舞 - DANCE_HIPHOP: 街舞 - DANCE_STAR: 明星舞蹈 - DANCE_CHINA: 中国舞 - DANCE_THREE_D: 舞蹈综合 - DANCE_DEMO: 舞蹈教程 - CINEPHILE: 影视 - CINEPHILE_CINECISM: 影视杂谈 - CINEPHILE_MONTAGE: 影视剪辑 - CINEPHILE_SHORTFILM: 小剧场 - CINEPHILE_TRAILER_INFO: 预告·资讯 - ENT: 娱乐 - ENT_VARIETY: 综艺 - ENT_TALKER: 娱乐杂谈 - ENT_FANS: 粉丝创作 - ENT_CELEBRITY: 明星综合 - KNOWLEDGE: 知识 - KNOWLEDGE_SCIENCE: 科学科普 - KNOWLEDGE_SOCIAL_SCIENCE: 社科·法律·心理 - KNOWLEDGE_HUMANITY_HISTORY: 人文历史 - KNOWLEDGE_BUSINESS: 财经商业 - KNOWLEDGE_CAMPUS: 校园学习 - KNOWLEDGE_CAREER: 职业职场 - KNOWLEDGE_DESIGN: 设计·创意 - KNOWLEDGE_SKILL: 野生技能协会 - TECH: 科技 - TECH_DIGITAL: 数码 - TECH_APPLICATION: 软件应用 - TECH_COMPUTER_TECH: 计算机技术 - TECH_INDUSTRY: 科工机械 - INFORMATION: 资讯 - INFORMATION_HOTSPOT: 热点 - INFORMATION_GLOBAL: 环球 - INFORMATION_SOCIAL: 社会 - INFORMATION_MULTIPLE: 综合 - FOOD: 美食 - FOOD_MAKE: 美食制作 - FOOD_DETECTIVE: 美食侦探 - FOOD_MEASUREMENT: 美食测评 - FOOD_RURAL: 田园美食 - FOOD_RECORD: 美食记录 - LIFE: 生活 - LIFE_FUNNY: 搞笑 - LIFE_TRAVEL: 出行 - LIFE_RURALLIFE: 三农 - LIFE_HOME: 家居房产 - LIFE_HANDMAKE: 手工 - LIFE_PAINTING: 绘画 - LIFE_DAILY: 日常 - CAR: 汽车 - CAR_RACING: 赛车 - CAR_MODIFIEDVEHICLE: 改装玩车 - CAR_NEWENERGYVEHICLE: 新能源车 - CAR_TOURINGCAR: 房车 - CAR_MOTORCYCLE: 摩托车 - CAR_STRATEGY: 购车攻略 - CAR_LIFE: 汽车生活 - FASHION: 时尚 - FASHION_MAKEUP: 美妆护肤 - FASHION_COS: 仿妆cos - FASHION_CLOTHING: 穿搭 - FASHION_TREND: 时尚潮流 - SPORTS: 运动 - SPORTS_BASKETBALL: 篮球 - SPORTS_FOOTBALL: 足球 - SPORTS_AEROBICS: 健身 - SPORTS_ATHLETIC: 竞技体育 - SPORTS_CULTURE: 运动文化 - SPORTS_COMPREHENSIVE: 运动综合 - ANIMAL: 动物圈 - ANIMAL_CAT: 喵星人 - ANIMAL_DOG: 汪星人 - ANIMAL_PANDA: 大熊猫 - ANIMAL_WILD_ANIMAL: 野生动物 - ANIMAL_REPTILES: 爬宠 - ANIMAL_COMPOSITE: 动物综合 - VLOG: VLOG """ MAINPAGE = 0 ANIME = 13 ANIME_SERIAL = 33 ANIME_FINISH = 32 ANIME_INFORMATION = 51 ANIME_OFFICAL = 152 MOVIE = 23 GUOCHUANG = 167 GUOCHUANG_CHINESE = 153 GUOCHUANG_ORIGINAL = 168 GUOCHUANG_PUPPETRY = 169 GUOCHUANG_MOTIONCOMIC = 195 GUOCHUANG_INFORMATION = 170 TELEPLAY = 11 DOCUMENTARY = 177 DOUGA = 1 DOUGA_MAD = 24 DOUGA_MMD = 25 DOUGA_VOICE = 47 DOUGA_GARAGE_KIT = 210 DOUGA_TOKUSATSU = 86 DOUGA_ACGNTALKS = 253 DOUGA_OTHER = 27 GAME = 4 GAME_STAND_ALONE = 17 GAME_ESPORTS = 171 GAME_MOBILE = 172 GAME_ONLINE = 65 GAME_BOARD = 173 GAME_GMV = 121 GAME_MUSIC = 136 GAME_MUGEN = 19 KICHIKU = 119 KICHIKU_GUIDE = 22 KICHIKU_MAD = 26 KICHIKU_MANUAL_VOCALOID = 126 KICHIKU_THEATRE = 216 KICHIKU_COURSE = 127 MUSIC = 3 MUSIC_ORIGINAL = 28 MUSIC_COVER = 31 MUSIC_PERFORM = 59 MUSIC_VOCALOID = 30 MUSIC_LIVE = 29 MUSIC_MV = 193 MUSIC_COMMENTARY = 243 MUSIC_TUTORIAL = 244 MUSIC_OTHER = 130 DANCE = 129 DANCE_OTAKU = 20 DANCE_HIPHOP = 198 DANCE_STAR = 199 DANCE_CHINA = 200 DANCE_THREE_D = 154 DANCE_DEMO = 156 CINEPHILE = 181 CINEPHILE_CINECISM = 182 CINEPHILE_MONTAGE = 183 CINEPHILE_SHORTFILM = 85 CINEPHILE_TRAILER_INFO = 184 ENT = 5 ENT_VARIETY = 71 ENT_TALKER = 241 ENT_FANS = 242 ENT_CELEBRITY = 137 KNOWLEDGE = 36 KNOWLEDGE_SCIENCE = 201 KNOWLEDGE_SOCIAL_SCIENCE = 124 KNOWLEDGE_HUMANITY_HISTORY = 228 KNOWLEDGE_BUSINESS = 207 KNOWLEDGE_CAMPUS = 208 KNOWLEDGE_CAREER = 209 KNOWLEDGE_DESIGN = 229 KNOWLEDGE_SKILL = 122 TECH = 188 TECH_DIGITAL = 95 TECH_APPLICATION = 230 TECH_COMPUTER_TECH = 231 TECH_INDUSTRY = 232 INFORMATION = 202 INFORMATION_HOTSPOT = 203 INFORMATION_GLOBAL = 204 INFORMATION_SOCIAL = 205 INFORMATION_MULTIPLE = 206 FOOD = 211 FOOD_MAKE = 76 FOOD_DETECTIVE = 212 FOOD_MEASUREMENT = 213 FOOD_RURAL = 214 FOOD_RECORD = 215 LIFE = 160 LIFE_FUNNY = 138 LIFE_TRAVEL = 250 LIFE_RURALLIFE = 251 LIFE_HOME = 239 LIFE_HANDMAKE = 161 LIFE_PAINTING = 162 LIFE_DAILY = 21 CAR = 223 CAR_RACING = 245 CAR_MODIFIEDVEHICLE = 246 CAR_NEWENERGYVEHICLE = 247 CAR_TOURINGCAR = 248 CAR_MOTORCYCLE = 240 CAR_STRATEGY = 227 CAR_LIFE = 176 FASHION = 155 FASHION_MAKEUP = 157 FASHION_COS = 252 FASHION_CLOTHING = 158 FASHION_TREND = 159 SPORTS = 234 SPORTS_BASKETBALL = 235 SPORTS_FOOTBALL = 249 SPORTS_AEROBICS = 164 SPORTS_ATHLETIC = 236 SPORTS_CULTURE = 237 SPORTS_COMPREHENSIVE = 238 ANIMAL = 217 ANIMAL_CAT = 218 ANIMAL_DOG = 219 ANIMAL_PANDA = 220 ANIMAL_WILD_ANIMAL = 221 ANIMAL_REPTILES = 222 ANIMAL_COMPOSITE = 75 VLOG = 19 ================================================ FILE: utils/files_times.py ================================================ from datetime import timedelta from datetime import datetime from pathlib import Path from conf import BASE_DIR def get_absolute_path(relative_path: str, base_dir: str = None) -> str: # Convert the relative path to an absolute path absolute_path = Path(BASE_DIR) / base_dir / relative_path return str(absolute_path) def get_title_and_hashtags(filename): """ 获取视频标题和 hashtag Args: filename: 视频文件名 Returns: 视频标题和 hashtag 列表 """ # 获取视频标题和 hashtag txt 文件名 txt_filename = filename.replace(".mp4", ".txt") # 读取 txt 文件 with open(txt_filename, "r", encoding="utf-8") as f: content = f.read() # 获取标题和 hashtag splite_str = content.strip().split("\n") title = splite_str[0] hashtags = splite_str[1].replace("#", "").split(" ") return title, hashtags def generate_schedule_time_next_day(total_videos, videos_per_day = 1, daily_times=None, timestamps=False, start_days=0): """ Generate a schedule for video uploads, starting from the next day. Args: - total_videos: Total number of videos to be uploaded. - videos_per_day: Number of videos to be uploaded each day. - daily_times: Optional list of specific times of the day to publish the videos. - timestamps: Boolean to decide whether to return timestamps or datetime objects. - start_days: Start from after start_days. Returns: - A list of scheduling times for the videos, either as timestamps or datetime objects. """ if videos_per_day <= 0: raise ValueError("videos_per_day should be a positive integer") if daily_times is None: # Default times to publish videos if not provided daily_times = [6, 11, 14, 16, 22] if videos_per_day > len(daily_times): raise ValueError("videos_per_day should not exceed the length of daily_times") # Generate timestamps schedule = [] current_time = datetime.now() for video in range(total_videos): day = video // videos_per_day + start_days + 1 # +1 to start from the next day daily_video_index = video % videos_per_day # Calculate the time for the current video hour = daily_times[daily_video_index] time_offset = timedelta(days=day, hours=hour - current_time.hour, minutes=-current_time.minute, seconds=-current_time.second, microseconds=-current_time.microsecond) timestamp = current_time + time_offset schedule.append(timestamp) if timestamps: schedule = [int(time.timestamp()) for time in schedule] return schedule ================================================ FILE: utils/log.py ================================================ from pathlib import Path from sys import stdout from loguru import logger from conf import BASE_DIR def log_formatter(record: dict) -> str: """ Formatter for log records. :param dict record: Log object containing log metadata & message. :returns: str """ colors = { "TRACE": "#cfe2f3", "INFO": "#9cbfdd", "DEBUG": "#8598ea", "WARNING": "#dcad5a", "SUCCESS": "#3dd08d", "ERROR": "#ae2c2c" } color = colors.get(record["level"].name, "#b3cfe7") return f"{{time:YYYY-MM-DD HH:mm:ss}} | {{level}}: {{message}}\n" def create_logger(log_name: str, file_path: str): """ Create custom logger for different business modules. :param str log_name: name of log :param str file_path: Optional path to log file :returns: Configured logger """ def filter_record(record): return record["extra"].get("business_name") == log_name Path(BASE_DIR / file_path).parent.mkdir(exist_ok=True) logger.add(Path(BASE_DIR / file_path), filter=filter_record, level="INFO", rotation="10 MB", retention="10 days", backtrace=True, diagnose=True) return logger.bind(business_name=log_name) # Remove all existing handlers logger.remove() # Add a standard console handler logger.add(stdout, colorize=True, format=log_formatter) douyin_logger = create_logger('douyin', 'logs/douyin.log') tencent_logger = create_logger('tencent', 'logs/tencent.log') xhs_logger = create_logger('xhs', 'logs/xhs.log') tiktok_logger = create_logger('tiktok', 'logs/tiktok.log') 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') ================================================ FILE: utils/network.py ================================================ import asyncio import time from functools import wraps def async_retry(timeout=60, max_retries=None): def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): start_time = time.time() attempts = 0 while True: try: return await func(*args, **kwargs) except Exception as e: attempts += 1 if max_retries is not None and attempts >= max_retries: print(f"Reached maximum retries of {max_retries}.") raise Exception(f"Failed after {max_retries} retries.") from e if time.time() - start_time > timeout: print(f"Function timeout after {timeout} seconds.") raise TimeoutError(f"Function execution exceeded {timeout} seconds timeout.") from e print(f"Attempt {attempts} failed: {e}. Retrying...") await asyncio.sleep(1) # Sleep to avoid tight loop or provide backoff logic here return wrapper return decorator ================================================ FILE: videos/demo.txt ================================================ 男子为了心爱之人每天坚守❤️‍🩹 #坚持不懈 #爱情执着 #奋斗使者 #短视频