├── .gitignore ├── 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 ├── media ├── 20231009111131.png ├── 20231009111214.png ├── QR.png ├── edan-qrcode.png ├── get_bili_cookie.png ├── group-qr.png ├── mp.jpg ├── show │ ├── pdf3.gif │ └── tkupload.gif ├── tk_login.png └── xhs_error_cookie.png ├── myUtils ├── __init__.py ├── auth.py ├── login.py └── postVideo.py ├── requirements.txt ├── sau_backend.py ├── sau_backend └── README.md ├── sau_frontend ├── .env.development ├── .env.production ├── README.md ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── api │ │ ├── account.js │ │ ├── index.js │ │ ├── material.js │ │ └── user.js │ ├── assets │ │ └── vue.svg │ ├── components │ │ └── helloworld.vue │ ├── 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 │ │ ├── Home.vue │ │ ├── MaterialManagement.vue │ │ └── PublishCenter.vue └── vite.config.js ├── uploader ├── __init__.py ├── baijiahao_uploader │ ├── __init__.py │ └── main.py ├── bilibili_uploader │ ├── __init__.py │ ├── biliup.exe │ └── 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 ├── constant.py ├── files_times.py ├── log.py ├── network.py └── stealth.min.js └── videos ├── demo.mp4 ├── demo.png └── demo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | .DS_Store 163 | 164 | # ignore cookie file 165 | tencent_uploader/*.json 166 | youtube_uploader/*.json 167 | douyin_uploader/*.json 168 | bilibili_uploader/*.json 169 | tk_uploader/*.json 170 | 171 | # 配置文件 172 | conf.py 173 | 174 | # 账号文件 175 | cookies 176 | 177 | # Frontend 178 | .vite/ 179 | dist/ 180 | node_modules/ 181 | package-lock.json 182 | 183 | # database 184 | db/database.db 185 | 186 | # 临时文件夹 187 | cookiesFile 188 | uploadFile 189 | videoFile 190 | 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # social-auto-upload 2 | 3 | `social-auto-upload` 是一个强大的自动化工具,旨在帮助内容创作者和运营者高效地将视频内容一键发布到多个国内外主流社交媒体平台。 4 | 项目实现了对 `抖音`、`Bilibili`、`小红书`、`快手`、`视频号`、`百家号` 以及 `TikTok` 等平台的视频上传、定时发布等功能。 5 | 结合各平台 `uploader` 模块,您可以轻松配置和扩展支持的平台,并通过示例脚本快速上手。 6 | 7 | tiktok show 8 | 9 | ## 目录 10 | 11 | - [💡 功能特性](#💡功能特性) 12 | - [🚀 支持的平台](#🚀支持的平台) 13 | - [💾 安装指南](#💾安装指南) 14 | - [🏁 快速开始](#🏁快速开始) 15 | - [🐇 项目背景](#🐇项目背景) 16 | - [📃 详细文档](#📃详细文档) 17 | - [🐾 交流与支持](#🐾交流与支持) 18 | - [🤝 贡献指南](#🤝贡献指南) 19 | - [📜 许可证](#📜许可证) 20 | - [⭐ Star History](#⭐Star-History) 21 | 22 | ## 💡功能特性 23 | 24 | ### 已支持平台 25 | 26 | - **国内平台**: 27 | - [x] 抖音 28 | - [x] 视频号 29 | - [x] Bilibili 30 | - [x] 小红书 31 | - [x] 快手 32 | - [x] 百家号 33 | - **国外平台**: 34 | - [x] TikTok 35 | 36 | ### 核心功能 37 | 38 | - [x] 定时上传 (Cron Job / Scheduled Upload) 39 | - [ ] Cookie 管理 (部分实现,持续优化中) 40 | - [ ] 国外平台 Proxy 设置 (部分实现) 41 | 42 | ### 计划支持与开发中 43 | 44 | - **平台扩展**: 45 | - [ ] QQ视频 46 | - [ ] YouTube 47 | - **功能增强**: 48 | - [x] 更易用的版本 (GUI / CLI 交互优化) 49 | - [x] API 封装 50 | - [ ] Docker 部署 51 | - [ ] 自动化上传 (更智能的调度策略) 52 | - [ ] 多线程/异步上传优化 53 | - [ ] Slack/消息推送通知 54 | 55 | --- 56 | 57 | ## 🚀支持的平台 58 | 59 | 本项目通过各平台对应的 `uploader` 模块实现视频上传功能。您可以在 `examples` 目录下找到各个平台的使用示例脚本。 60 | 61 | 每个示例脚本展示了如何配置和调用相应的 uploader。 62 | 63 | ## 💾安装指南 64 | 65 | 1. **克隆项目**: 66 | ```bash 67 | git clone https://github.com/dreammis/social-auto-upload.git 68 | cd social-auto-upload 69 | ``` 70 | 71 | 2. **安装依赖**: 72 | 建议在虚拟环境中安装依赖。 73 | ```bash 74 | conda create -n social-auto-upload python=3.10 75 | conda activate social-auto-upload 76 | # 挂载清华镜像 or 命令行代理 77 | pip install -r requirements.txt 78 | ``` 79 | 80 | 3. **安装 Playwright 浏览器驱动**: 81 | ```bash 82 | playwright install chromium firefox 83 | ``` 84 | 根据您的需求,至少需要安装 `chromium`。`firefox` 主要用于 TikTok 上传(旧版)。 85 | 86 | 4. **修改配置文件**: 87 | 复制 `conf.example.py` 并重命名为 `conf.py`。 88 | 在 `conf.py` 中,您需要配置以下内容: 89 | - `LOCAL_CHROME_PATH`: 本地 Chrome 浏览器的路径,比如 `C:\Program Files\Google\Chrome\Application\chrome.exe` 保存。 90 | 91 | **临时解决方案** 92 | 93 | 需要在根目录创建 `cookiesFile` 和 `videoFile` 两个文件夹,分别是 存储cookie文件 和 存储上传文件 的文件夹 94 | 95 | 5. **配置数据库**: 96 | 如果 db/database.db 文件不存在,您可以运行以下命令来初始化数据库: 97 | ```bash 98 | cd db 99 | python createTable.py 100 | ``` 101 | 此命令将初始化 SQLite 数据库。 102 | 103 | 6. **启动后端项目**: 104 | ```bash 105 | python sau_backend.py 106 | ``` 107 | 后端项目将在 `http://localhost:5409` 启动。 108 | 109 | 7. **启动前端项目**: 110 | ```bash 111 | cd sau_frontend 112 | npm install 113 | npm run dev 114 | ``` 115 | 前端项目将在 `http://localhost:5173` 启动,在浏览器中打开此链接即可访问。 116 | 117 | 118 | > 非程序员用户可以参考:[新手级教程](https://juejin.cn/post/7372114027840208911) 119 | 120 | 121 | ## 🏁快速开始 122 | 123 | 1. **准备 Cookie**: 124 | 大多数平台需要登录后的 Cookie 信息才能进行操作。请参照 examples 目录下各 `get_xxx_cookie.py` 脚本(例如 get_douyin_cookie.py, get_ks_cookie.py)的说明,运行脚本以生成并保存 Cookie 文件(通常在 `cookies/[PLATFORM]_uploader/account.json`)。 125 | 126 | 2. **准备视频文件**: 127 | 将需要上传的视频文件(通常为 `.mp4` 格式)放置在 videos 目录下。 128 | 部分平台支持视频封面,可以将封面图片(例如 `.png` 格式,与视频同名)也放在此目录。 129 | 如果需要上传标题及标签,请在视频文件旁边创建一个同名的 `.txt` 文件,内容为标题和标签,以换行分隔。 130 | 131 | 3. **修改并运行示例脚本**: 132 | 打开 examples 目录中您想使用的平台的上传脚本(例如 upload_video_to_douyin.py)。 133 | - 根据脚本内的注释和说明,确认 Cookie 文件路径、视频文件路径等配置是否正确。 134 | - 您可以修改脚本以适应您的具体需求,例如批量上传、自定义标题、标签等。 135 | 136 | 4. **执行上传**: 137 | 运行修改后的示例脚本,例如: 138 | ```bash 139 | python examples/upload_video_to_douyin.py 140 | ``` 141 | 142 | ## 🐇项目背景 143 | 144 | 该项目最初是我个人用于自动化管理社交媒体视频发布的工具。我的主要发布策略是提前一天设置定时发布,因此项目中很多定时发布相关的逻辑是基于“第二天”的时间进行计算的。 145 | 146 | 如果您需要立即发布或其他定制化的发布策略,欢迎研究源码或在社区提问。 147 | 148 | ## 📃详细文档 149 | 150 | 更详细的文档和说明,请查看:[social-auto-upload 官方文档](https://sap-doc.nasdaddy.com/) 151 | 152 | ## 🐾交流与支持 153 | 154 | [☕ Donate as u like](https://www.buymeacoffee.com/hysn2001m) - 如果您觉得这个项目对您有帮助,可以考虑赞助。 155 | 156 | 如果您也是独立开发者、技术爱好者,对 #技术变现 #AI创业 #跨境电商 #自动化工具 #视频创作 等话题感兴趣,欢迎加入社群交流。 157 | 158 | ### Creator 159 | 160 | 161 | 172 | 183 |
162 | 163 | NasDaddy公众号 164 |
165 | 微信公众号 166 |
167 |
168 | 💻 169 |
170 | 关注公众号,后台回复 `上传` 获取加群方式 171 |
173 | 174 | 赞赏码/入群引导 175 |
176 | 交流群 (通过公众号获取) 177 |
178 |
179 | 📖 180 |
181 | 如果您觉得项目有用,可以考虑打赏支持一下 182 |
184 | 185 | ### Active Core Team 186 | 187 | 188 | 202 |
189 | 190 | Edan Lee 191 |
192 | Edan Lee 193 |
194 |
195 | 💻 196 | 📖 197 |
198 | 封装了 api 接口和 web 前端管理界面 199 |
200 | (请注明来意:进群、学习、企业咨询等) 201 |
203 | 204 | ## 🤝贡献指南 205 | 206 | 欢迎各种形式的贡献,包括但不限于: 207 | 208 | - 提交 Bug报告 和 Feature请求。 209 | - 改进代码、文档。 210 | - 分享使用经验和教程。 211 | 212 | 如果您希望贡献代码,请遵循以下步骤: 213 | 214 | 1. Fork 本仓库。 215 | 2. 创建一个新的分支 (`git checkout -b feature/YourFeature` 或 `bugfix/YourBugfix`)。 216 | 3. 提交您的更改 (`git commit -m 'Add some feature'`)。 217 | 4. Push到您的分支 (`git push origin feature/YourFeature`)。 218 | 5. 创建一个 Pull Request。 219 | 220 | ## 📜许可证 221 | 222 | 本项目暂时采用 [MIT License](LICENSE) 开源许可证。 223 | 224 | ## ⭐Star-History 225 | 226 | > 如果这个项目对您有帮助,请给一个 ⭐ Star 以表示支持! 227 | 228 | [![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) 229 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/__init__.py -------------------------------------------------------------------------------- /cli_main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | from datetime import datetime 4 | from os.path import exists 5 | from pathlib import Path 6 | 7 | from conf import BASE_DIR 8 | from uploader.douyin_uploader.main import douyin_setup, DouYinVideo 9 | from uploader.ks_uploader.main import ks_setup, KSVideo 10 | from uploader.tencent_uploader.main import weixin_setup, TencentVideo 11 | from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo 12 | from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \ 13 | SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU 14 | from utils.constant import TencentZoneTypes 15 | from utils.files_times import get_title_and_hashtags 16 | 17 | 18 | def parse_schedule(schedule_raw): 19 | if schedule_raw: 20 | schedule = datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M') 21 | else: 22 | schedule = None 23 | return schedule 24 | 25 | 26 | async def main(): 27 | # 主解析器 28 | parser = argparse.ArgumentParser(description="Upload video to multiple social-media.") 29 | parser.add_argument("platform", metavar='platform', choices=get_supported_social_media(), help="Choose social-media platform: douyin tencent tiktok kuaishou") 30 | 31 | parser.add_argument("account_name", type=str, help="Account name for the platform: xiaoA") 32 | subparsers = parser.add_subparsers(dest="action", metavar='action', help="Choose action", required=True) 33 | 34 | actions = get_cli_action() 35 | for action in actions: 36 | action_parser = subparsers.add_parser(action, help=f'{action} operation') 37 | if action == 'login': 38 | # Login 不需要额外参数 39 | continue 40 | elif action == 'upload': 41 | action_parser.add_argument("video_file", help="Path to the Video file") 42 | action_parser.add_argument("-pt", "--publish_type", type=int, choices=[0, 1], 43 | help="0 for immediate, 1 for scheduled", default=0) 44 | action_parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format') 45 | 46 | # 解析命令行参数 47 | args = parser.parse_args() 48 | # 参数校验 49 | if args.action == 'upload': 50 | if not exists(args.video_file): 51 | raise FileNotFoundError(f'Could not find the video file at {args["video_file"]}') 52 | if args.publish_type == 1 and not args.schedule: 53 | parser.error("The schedule must must be specified for scheduled publishing.") 54 | 55 | account_file = Path(BASE_DIR / "cookies" / f"{args.platform}_{args.account_name}.json") 56 | account_file.parent.mkdir(exist_ok=True) 57 | 58 | # 根据 action 处理不同的逻辑 59 | if args.action == 'login': 60 | print(f"Logging in with account {args.account_name} on platform {args.platform}") 61 | if args.platform == SOCIAL_MEDIA_DOUYIN: 62 | await douyin_setup(str(account_file), handle=True) 63 | elif args.platform == SOCIAL_MEDIA_TIKTOK: 64 | await tiktok_setup(str(account_file), handle=True) 65 | elif args.platform == SOCIAL_MEDIA_TENCENT: 66 | await weixin_setup(str(account_file), handle=True) 67 | elif args.platform == SOCIAL_MEDIA_KUAISHOU: 68 | await ks_setup(str(account_file), handle=True) 69 | elif args.action == 'upload': 70 | title, tags = get_title_and_hashtags(args.video_file) 71 | video_file = args.video_file 72 | 73 | if args.publish_type == 0: 74 | print("Uploading immediately...") 75 | publish_date = 0 76 | else: 77 | print("Scheduling videos...") 78 | publish_date = parse_schedule(args.schedule) 79 | 80 | if args.platform == SOCIAL_MEDIA_DOUYIN: 81 | await douyin_setup(account_file, handle=False) 82 | app = DouYinVideo(title, video_file, tags, publish_date, account_file) 83 | elif args.platform == SOCIAL_MEDIA_TIKTOK: 84 | await tiktok_setup(account_file, handle=True) 85 | app = TiktokVideo(title, video_file, tags, publish_date, account_file) 86 | elif args.platform == SOCIAL_MEDIA_TENCENT: 87 | await weixin_setup(account_file, handle=True) 88 | category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传 89 | app = TencentVideo(title, video_file, tags, publish_date, account_file, category) 90 | elif args.platform == SOCIAL_MEDIA_KUAISHOU: 91 | await ks_setup(account_file, handle=True) 92 | app = KSVideo(title, video_file, tags, publish_date, account_file) 93 | else: 94 | print("Wrong platform, please check your input") 95 | exit() 96 | 97 | await app.main() 98 | 99 | 100 | if __name__ == "__main__": 101 | asyncio.run(main()) 102 | -------------------------------------------------------------------------------- /conf.example.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | BASE_DIR = Path(__file__).parent.resolve() 4 | XHS_SERVER = "http://127.0.0.1:11901" 5 | LOCAL_CHROME_PATH = "" # change me necessary! for example C:/Program Files/Google/Chrome/Application/chrome.exe 6 | -------------------------------------------------------------------------------- /db/createTable.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | import os 4 | 5 | # 数据库文件路径(如果不存在会自动创建) 6 | db_file = './database.db' 7 | 8 | # 如果数据库已存在,则删除旧的表(可选) 9 | # if os.path.exists(db_file): 10 | # os.remove(db_file) 11 | 12 | # 连接到SQLite数据库(如果文件不存在则会自动创建) 13 | conn = sqlite3.connect(db_file) 14 | cursor = conn.cursor() 15 | 16 | # 创建账号记录表 17 | cursor.execute(''' 18 | CREATE TABLE IF NOT EXISTS user_info ( 19 | id INTEGER PRIMARY KEY AUTOINCREMENT, 20 | type INTEGER NOT NULL, 21 | filePath TEXT NOT NULL, -- 存储文件路径 22 | userName TEXT NOT NULL, 23 | status INTEGER DEFAULT 0 24 | ) 25 | ''') 26 | 27 | # 创建文件记录表 28 | cursor.execute('''CREATE TABLE IF NOT EXISTS file_records ( 29 | id INTEGER PRIMARY KEY AUTOINCREMENT, -- 唯一标识每条记录 30 | filename TEXT NOT NULL, -- 文件名 31 | filesize REAL, -- 文件大小(单位:MB) 32 | upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 上传时间,默认当前时间 33 | file_path TEXT -- 文件路径 34 | ) 35 | ''') 36 | 37 | 38 | # 提交更改 39 | conn.commit() 40 | print("✅ 表创建成功") 41 | # 关闭连接 42 | conn.close() -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/examples/__init__.py -------------------------------------------------------------------------------- /examples/get_baijiahao_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.baijiahao_uploader.main import baijiahao_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(baijiahao_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/get_bilibili_cookie.py: -------------------------------------------------------------------------------- 1 | # cd uploader/bilibili_uploader 2 | # biliup.exe -u account.json login 3 | -------------------------------------------------------------------------------- /examples/get_douyin_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.douyin_uploader.main import douyin_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(douyin_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/get_kuaishou_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.ks_uploader.main import ks_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(ks_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/get_tencent_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.tencent_uploader.main import weixin_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(weixin_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/get_tk_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.tk_uploader.main_chrome import tiktok_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(tiktok_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/get_xiaohongshu_cookie.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.xiaohongshu_uploader.main import xiaohongshu_setup 6 | 7 | if __name__ == '__main__': 8 | account_file = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "account.json") 9 | account_file.parent.mkdir(exist_ok=True) 10 | cookie_setup = asyncio.run(xiaohongshu_setup(str(account_file), handle=True)) 11 | -------------------------------------------------------------------------------- /examples/upload_video_to_baijiahao.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.baijiahao_uploader.main import baijiahao_setup, BaiJiaHaoVideo 6 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 7 | 8 | 9 | if __name__ == '__main__': 10 | filepath = Path(BASE_DIR) / "videos" 11 | account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json") 12 | # 获取视频目录 13 | folder_path = Path(filepath) 14 | # 获取文件夹中的所有文件 15 | files = list(folder_path.glob("*.mp4")) 16 | file_num = len(files) 17 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 18 | cookie_setup = asyncio.run(baijiahao_setup(account_file, handle=False)) 19 | for index, file in enumerate(files): 20 | title, tags = get_title_and_hashtags(str(file)) 21 | thumbnail_path = file.with_suffix('.png') 22 | # 打印视频文件名、标题和 hashtag 23 | print(f"视频文件名:{file}") 24 | print(f"标题:{title}") 25 | print(f"Hashtag:{tags}") 26 | app = BaiJiaHaoVideo(title, file, tags, publish_datetimes[index], account_file) 27 | asyncio.run(app.main(), debug=False) 28 | -------------------------------------------------------------------------------- /examples/upload_video_to_bilibili.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | 4 | from uploader.bilibili_uploader.main import read_cookie_json_file, extract_keys_from_json, random_emoji, BilibiliUploader 5 | from conf import BASE_DIR 6 | from utils.constant import VideoZoneTypes 7 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 8 | 9 | if __name__ == '__main__': 10 | filepath = Path(BASE_DIR) / "videos" 11 | # how to get cookie, see the file of get_bilibili_cookie.py. 12 | account_file = Path(BASE_DIR / "cookies" / "bilibili_uploader" / "account.json") 13 | if not account_file.exists(): 14 | print(f"{account_file.name} 配置文件不存在") 15 | exit() 16 | cookie_data = read_cookie_json_file(account_file) 17 | cookie_data = extract_keys_from_json(cookie_data) 18 | 19 | tid = VideoZoneTypes.SPORTS_FOOTBALL.value # 设置分区id 20 | # 获取视频目录 21 | folder_path = Path(filepath) 22 | # 获取文件夹中的所有文件 23 | files = list(folder_path.glob("*.mp4")) 24 | file_num = len(files) 25 | timestamps = generate_schedule_time_next_day(file_num, 1, daily_times=[16], timestamps=True) 26 | 27 | for index, file in enumerate(files): 28 | title, tags = get_title_and_hashtags(str(file)) 29 | # just avoid error, bilibili don't allow same title of video. 30 | title += random_emoji() 31 | tags_str = ','.join([tag for tag in tags]) 32 | # 打印视频文件名、标题和 hashtag 33 | print(f"视频文件名:{file}") 34 | print(f"标题:{title}") 35 | print(f"Hashtag:{tags}") 36 | # I set desc same as title, do what u like. 37 | desc = title 38 | bili_uploader = BilibiliUploader(cookie_data, file, title, desc, tid, tags, timestamps[index]) 39 | bili_uploader.upload() 40 | 41 | # life is beautiful don't so rush. be kind be patience 42 | time.sleep(30) 43 | -------------------------------------------------------------------------------- /examples/upload_video_to_douyin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.douyin_uploader.main import douyin_setup, DouYinVideo 6 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 7 | 8 | 9 | if __name__ == '__main__': 10 | filepath = Path(BASE_DIR) / "videos" 11 | account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json") 12 | # 获取视频目录 13 | folder_path = Path(filepath) 14 | # 获取文件夹中的所有文件 15 | files = list(folder_path.glob("*.mp4")) 16 | file_num = len(files) 17 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 18 | cookie_setup = asyncio.run(douyin_setup(account_file, handle=False)) 19 | for index, file in enumerate(files): 20 | title, tags = get_title_and_hashtags(str(file)) 21 | thumbnail_path = file.with_suffix('.png') 22 | # 打印视频文件名、标题和 hashtag 23 | print(f"视频文件名:{file}") 24 | print(f"标题:{title}") 25 | print(f"Hashtag:{tags}") 26 | # 暂时没有时间修复封面上传,故先隐藏掉该功能 27 | # if thumbnail_path.exists(): 28 | # app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path) 29 | # else: 30 | app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file) 31 | asyncio.run(app.main(), debug=False) 32 | -------------------------------------------------------------------------------- /examples/upload_video_to_kuaishou.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.ks_uploader.main import ks_setup, KSVideo 6 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 7 | 8 | 9 | if __name__ == '__main__': 10 | filepath = Path(BASE_DIR) / "videos" 11 | account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json") 12 | # 获取视频目录 13 | folder_path = Path(filepath) 14 | # 获取文件夹中的所有文件 15 | files = list(folder_path.glob("*.mp4")) 16 | file_num = len(files) 17 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 18 | cookie_setup = asyncio.run(ks_setup(account_file, handle=False)) 19 | for index, file in enumerate(files): 20 | title, tags = get_title_and_hashtags(str(file)) 21 | # 打印视频文件名、标题和 hashtag 22 | print(f"视频文件名:{file}") 23 | print(f"标题:{title}") 24 | print(f"Hashtag:{tags}") 25 | app = KSVideo(title, file, tags, publish_datetimes[index], account_file) 26 | asyncio.run(app.main(), debug=False) 27 | -------------------------------------------------------------------------------- /examples/upload_video_to_tencent.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.tencent_uploader.main import weixin_setup, TencentVideo 6 | from utils.constant import TencentZoneTypes 7 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 8 | 9 | 10 | if __name__ == '__main__': 11 | filepath = Path(BASE_DIR) / "videos" 12 | account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json") 13 | # 获取视频目录 14 | folder_path = Path(filepath) 15 | # 获取文件夹中的所有文件 16 | files = list(folder_path.glob("*.mp4")) 17 | file_num = len(files) 18 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 19 | cookie_setup = asyncio.run(weixin_setup(account_file, handle=True)) 20 | category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传 21 | for index, file in enumerate(files): 22 | title, tags = get_title_and_hashtags(str(file)) 23 | # 打印视频文件名、标题和 hashtag 24 | print(f"视频文件名:{file}") 25 | print(f"标题:{title}") 26 | print(f"Hashtag:{tags}") 27 | app = TencentVideo(title, file, tags, publish_datetimes[index], account_file, category) 28 | asyncio.run(app.main(), debug=False) 29 | -------------------------------------------------------------------------------- /examples/upload_video_to_tiktok.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | # from tk_uploader.main import tiktok_setup, TiktokVideo 6 | from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo 7 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 8 | 9 | 10 | if __name__ == '__main__': 11 | filepath = Path(BASE_DIR) / "videos" 12 | account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json") 13 | folder_path = Path(filepath) 14 | # get video files from folder 15 | files = list(folder_path.glob("*.mp4")) 16 | file_num = len(files) 17 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 18 | cookie_setup = asyncio.run(tiktok_setup(account_file, handle=True)) 19 | for index, file in enumerate(files): 20 | title, tags = get_title_and_hashtags(str(file)) 21 | thumbnail_path = file.with_suffix('.png') 22 | print(f"video_file_name:{file}") 23 | print(f"video_title:{title}") 24 | print(f"video_hashtag:{tags}") 25 | if thumbnail_path.exists(): 26 | print(f"thumbnail_file_name:{thumbnail_path}") 27 | app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path) 28 | else: 29 | app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file) 30 | asyncio.run(app.main(), debug=False) 31 | -------------------------------------------------------------------------------- /examples/upload_video_to_xhs.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | from pathlib import Path 3 | from time import sleep 4 | 5 | from xhs import XhsClient 6 | 7 | from conf import BASE_DIR 8 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 9 | from uploader.xhs_uploader.main import sign_local, beauty_print 10 | 11 | config = configparser.RawConfigParser() 12 | config.read(Path(BASE_DIR / "uploader" / "xhs_uploader" / "accounts.ini")) 13 | 14 | 15 | if __name__ == '__main__': 16 | filepath = Path(BASE_DIR) / "videos" 17 | # 获取视频目录 18 | folder_path = Path(filepath) 19 | # 获取文件夹中的所有文件 20 | files = list(folder_path.glob("*.mp4")) 21 | file_num = len(files) 22 | 23 | cookies = config['account1']['cookies'] 24 | xhs_client = XhsClient(cookies, sign=sign_local, timeout=60) 25 | # auth cookie 26 | # 注意:该校验cookie方式可能并没那么准确 27 | try: 28 | xhs_client.get_video_first_frame_image_id("3214") 29 | except: 30 | print("cookie 失效") 31 | exit() 32 | 33 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 34 | 35 | for index, file in enumerate(files): 36 | title, tags = get_title_and_hashtags(str(file)) 37 | # 加入到标题 补充标题(xhs 可以填1000字不写白不写) 38 | tags_str = ' '.join(['#' + tag for tag in tags]) 39 | hash_tags_str = '' 40 | hash_tags = [] 41 | 42 | # 打印视频文件名、标题和 hashtag 43 | print(f"视频文件名:{file}") 44 | print(f"标题:{title}") 45 | print(f"Hashtag:{tags}") 46 | 47 | topics = [] 48 | # 获取hashtag 49 | for i in tags[:3]: 50 | topic_official = xhs_client.get_suggest_topic(i) 51 | if topic_official: 52 | topic_official[0]['type'] = 'topic' 53 | topic_one = topic_official[0] 54 | hash_tag_name = topic_one['name'] 55 | hash_tags.append(hash_tag_name) 56 | topics.append(topic_one) 57 | 58 | hash_tags_str = ' ' + ' '.join(['#' + tag + '[话题]#' for tag in hash_tags]) 59 | 60 | note = xhs_client.create_video_note(title=title[:20], video_path=str(file), 61 | desc=title + tags_str + hash_tags_str, 62 | topics=topics, 63 | is_private=False, 64 | post_time=publish_datetimes[index].strftime("%Y-%m-%d %H:%M:%S")) 65 | 66 | beauty_print(note) 67 | # 强制休眠30s,避免风控(必要) 68 | sleep(30) 69 | -------------------------------------------------------------------------------- /examples/upload_video_to_xiaohongshu.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.xiaohongshu_uploader.main import xiaohongshu_setup, XiaoHongShuVideo 6 | from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags 7 | 8 | 9 | if __name__ == '__main__': 10 | filepath = Path(BASE_DIR) / "videos" 11 | account_file = Path(BASE_DIR / "cookies" / "xiaohongshu_uploader" / "58a391ba-4082-11f0-a321-44e51723d63c.json") 12 | # 获取视频目录 13 | folder_path = Path(filepath) 14 | # 获取文件夹中的所有文件 15 | files = list(folder_path.glob("*.mp4")) 16 | file_num = len(files) 17 | publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16]) 18 | cookie_setup = asyncio.run(xiaohongshu_setup(account_file, handle=False)) 19 | for index, file in enumerate(files): 20 | title, tags = get_title_and_hashtags(str(file)) 21 | thumbnail_path = file.with_suffix('.png') 22 | # 打印视频文件名、标题和 hashtag 23 | print(f"视频文件名:{file}") 24 | print(f"标题:{title}") 25 | print(f"Hashtag:{tags}") 26 | # 暂时没有时间修复封面上传,故先隐藏掉该功能 27 | # if thumbnail_path.exists(): 28 | # app = XiaoHongShuVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path) 29 | # else: 30 | app = XiaoHongShuVideo(title, file, tags, 0, account_file) 31 | asyncio.run(app.main(), debug=False) 32 | -------------------------------------------------------------------------------- /media/20231009111131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/20231009111131.png -------------------------------------------------------------------------------- /media/20231009111214.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/20231009111214.png -------------------------------------------------------------------------------- /media/QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/QR.png -------------------------------------------------------------------------------- /media/edan-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/edan-qrcode.png -------------------------------------------------------------------------------- /media/get_bili_cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/get_bili_cookie.png -------------------------------------------------------------------------------- /media/group-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/group-qr.png -------------------------------------------------------------------------------- /media/mp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/mp.jpg -------------------------------------------------------------------------------- /media/show/pdf3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/show/pdf3.gif -------------------------------------------------------------------------------- /media/show/tkupload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/show/tkupload.gif -------------------------------------------------------------------------------- /media/tk_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/tk_login.png -------------------------------------------------------------------------------- /media/xhs_error_cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/media/xhs_error_cookie.png -------------------------------------------------------------------------------- /myUtils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/myUtils/__init__.py -------------------------------------------------------------------------------- /myUtils/auth.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import configparser 3 | import os 4 | 5 | from playwright.async_api import async_playwright 6 | from xhs import XhsClient 7 | 8 | from conf import BASE_DIR 9 | from utils.base_social_media import set_init_script 10 | from utils.log import tencent_logger, kuaishou_logger 11 | from pathlib import Path 12 | from uploader.xhs_uploader.main import sign_local 13 | 14 | async def cookie_auth_douyin(account_file): 15 | async with async_playwright() as playwright: 16 | browser = await playwright.chromium.launch(headless=True) 17 | context = await browser.new_context(storage_state=account_file) 18 | context = await set_init_script(context) 19 | # 创建一个新的页面 20 | page = await context.new_page() 21 | # 访问指定的 URL 22 | await page.goto("https://creator.douyin.com/creator-micro/content/upload") 23 | try: 24 | await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) 25 | except: 26 | print("[+] 等待5秒 cookie 失效") 27 | await context.close() 28 | await browser.close() 29 | return False 30 | # 2024.06.17 抖音创作者中心改版 31 | if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): 32 | print("[+] 等待5秒 cookie 失效") 33 | return False 34 | else: 35 | print("[+] cookie 有效") 36 | return True 37 | 38 | async def cookie_auth_tencent(account_file): 39 | async with async_playwright() as playwright: 40 | browser = await playwright.chromium.launch(headless=True) 41 | context = await browser.new_context(storage_state=account_file) 42 | context = await set_init_script(context) 43 | # 创建一个新的页面 44 | page = await context.new_page() 45 | # 访问指定的 URL 46 | await page.goto("https://channels.weixin.qq.com/platform/post/create") 47 | try: 48 | await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒 49 | tencent_logger.error("[+] 等待5秒 cookie 失效") 50 | return False 51 | except: 52 | tencent_logger.success("[+] cookie 有效") 53 | return True 54 | 55 | async def cookie_auth_ks(account_file): 56 | async with async_playwright() as playwright: 57 | browser = await playwright.chromium.launch(headless=True) 58 | context = await browser.new_context(storage_state=account_file) 59 | context = await set_init_script(context) 60 | # 创建一个新的页面 61 | page = await context.new_page() 62 | # 访问指定的 URL 63 | await page.goto("https://cp.kuaishou.com/article/publish/video") 64 | try: 65 | await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒 66 | 67 | kuaishou_logger.info("[+] 等待5秒 cookie 失效") 68 | return False 69 | except: 70 | kuaishou_logger.success("[+] cookie 有效") 71 | return True 72 | 73 | 74 | async def cookie_auth_xhs(account_file): 75 | async with async_playwright() as playwright: 76 | browser = await playwright.chromium.launch(headless=True) 77 | context = await browser.new_context(storage_state=account_file) 78 | context = await set_init_script(context) 79 | # 创建一个新的页面 80 | page = await context.new_page() 81 | # 访问指定的 URL 82 | await page.goto("https://creator.xiaohongshu.com/creator-micro/content/upload") 83 | try: 84 | await page.wait_for_url("https://creator.xiaohongshu.com/creator-micro/content/upload", timeout=5000) 85 | except: 86 | print("[+] 等待5秒 cookie 失效") 87 | await context.close() 88 | await browser.close() 89 | return False 90 | # 2024.06.17 抖音创作者中心改版 91 | if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): 92 | print("[+] 等待5秒 cookie 失效") 93 | return False 94 | else: 95 | print("[+] cookie 有效") 96 | return True 97 | 98 | 99 | async def check_cookie(type,file_path): 100 | match type: 101 | # 小红书 102 | case 1: 103 | return await cookie_auth_xhs(Path(BASE_DIR / "cookiesFile" / file_path)) 104 | # 视频号 105 | case 2: 106 | return await cookie_auth_tencent(Path(BASE_DIR / "cookiesFile" / file_path)) 107 | # 抖音 108 | case 3: 109 | return await cookie_auth_douyin(Path(BASE_DIR / "cookiesFile" / file_path)) 110 | # 快手 111 | case 4: 112 | return await cookie_auth_ks(Path(BASE_DIR / "cookiesFile" / file_path)) 113 | case _: 114 | return False 115 | 116 | # a = asyncio.run(check_cookie(1,"3a6cfdc0-3d51-11f0-8507-44e51723d63c.json")) 117 | # print(a) -------------------------------------------------------------------------------- /myUtils/login.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sqlite3 3 | 4 | from playwright.async_api import async_playwright 5 | 6 | from myUtils.auth import check_cookie 7 | from utils.base_social_media import set_init_script 8 | import uuid 9 | from pathlib import Path 10 | from conf import BASE_DIR 11 | 12 | # 抖音登录 13 | async def douyin_cookie_gen(id,status_queue): 14 | url_changed_event = asyncio.Event() 15 | async def on_url_change(): 16 | # 检查是否是主框架的变化 17 | if page.url != original_url: 18 | url_changed_event.set() 19 | async with async_playwright() as playwright: 20 | options = { 21 | 'headless': False 22 | } 23 | # Make sure to run headed. 24 | browser = await playwright.chromium.launch(**options) 25 | # Setup context however you like. 26 | context = await browser.new_context() # Pass any options 27 | context = await set_init_script(context) 28 | # Pause the page, and start recording manually. 29 | page = await context.new_page() 30 | await page.goto("https://creator.douyin.com/") 31 | original_url = page.url 32 | img_locator = page.get_by_role("img", name="二维码") 33 | # 获取 src 属性值 34 | src = await img_locator.get_attribute("src") 35 | print("✅ 图片地址:", src) 36 | status_queue.put(src) 37 | # 监听页面的 'framenavigated' 事件,只关注主框架的变化 38 | page.on('framenavigated', 39 | lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) 40 | try: 41 | # 等待 URL 变化或超时 42 | await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 43 | print("监听页面跳转成功") 44 | except asyncio.TimeoutError: 45 | print("监听页面跳转超时") 46 | await page.close() 47 | await context.close() 48 | await browser.close() 49 | status_queue.put("500") 50 | return None 51 | uuid_v1 = uuid.uuid1() 52 | print(f"UUID v1: {uuid_v1}") 53 | await context.storage_state(path=Path(BASE_DIR / "cookiesFile" / f"{uuid_v1}.json")) 54 | result = await check_cookie(3, f"{uuid_v1}.json") 55 | if not result: 56 | status_queue.put("500") 57 | await page.close() 58 | await context.close() 59 | await browser.close() 60 | return None 61 | await page.close() 62 | await context.close() 63 | await browser.close() 64 | with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: 65 | cursor = conn.cursor() 66 | cursor.execute(''' 67 | INSERT INTO user_info (type, filePath, userName, status) 68 | VALUES (?, ?, ?, ?) 69 | ''', (3, f"{uuid_v1}.json", id, 1)) 70 | conn.commit() 71 | print("✅ 用户状态已记录") 72 | status_queue.put("200") 73 | 74 | 75 | # 视频号登录 76 | async def get_tencent_cookie(id,status_queue): 77 | url_changed_event = asyncio.Event() 78 | async def on_url_change(): 79 | # 检查是否是主框架的变化 80 | if page.url != original_url: 81 | url_changed_event.set() 82 | 83 | async with async_playwright() as playwright: 84 | options = { 85 | 'args': [ 86 | '--lang en-GB' 87 | ], 88 | 'headless': False, # Set headless option here 89 | } 90 | # Make sure to run headed. 91 | browser = await playwright.chromium.launch(**options) 92 | # Setup context however you like. 93 | context = await browser.new_context() # Pass any options 94 | # Pause the page, and start recording manually. 95 | context = await set_init_script(context) 96 | page = await context.new_page() 97 | await page.goto("https://channels.weixin.qq.com") 98 | original_url = page.url 99 | 100 | # 监听页面的 'framenavigated' 事件,只关注主框架的变化 101 | page.on('framenavigated', 102 | lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) 103 | 104 | # 等待 iframe 出现(最多等 60 秒) 105 | iframe_locator = page.frame_locator("iframe").first 106 | 107 | # 获取 iframe 中的第一个 img 元素 108 | img_locator = iframe_locator.get_by_role("img").first 109 | 110 | # 获取 src 属性值 111 | src = await img_locator.get_attribute("src") 112 | print("✅ 图片地址:", src) 113 | status_queue.put(src) 114 | 115 | try: 116 | # 等待 URL 变化或超时 117 | await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 118 | print("监听页面跳转成功") 119 | except asyncio.TimeoutError: 120 | status_queue.put("500") 121 | print("监听页面跳转超时") 122 | await page.close() 123 | await context.close() 124 | await browser.close() 125 | return None 126 | uuid_v1 = uuid.uuid1() 127 | print(f"UUID v1: {uuid_v1}") 128 | await context.storage_state(path=Path(BASE_DIR / "cookiesFile" / f"{uuid_v1}.json")) 129 | result = await check_cookie(2,f"{uuid_v1}.json") 130 | if not result: 131 | status_queue.put("500") 132 | await page.close() 133 | await context.close() 134 | await browser.close() 135 | return None 136 | await page.close() 137 | await context.close() 138 | await browser.close() 139 | 140 | with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: 141 | cursor = conn.cursor() 142 | cursor.execute(''' 143 | INSERT INTO user_info (type, filePath, userName, status) 144 | VALUES (?, ?, ?, ?) 145 | ''', (2, f"{uuid_v1}.json", id, 1)) 146 | conn.commit() 147 | print("✅ 用户状态已记录") 148 | status_queue.put("200") 149 | 150 | # 快手登录 151 | async def get_ks_cookie(id,status_queue): 152 | url_changed_event = asyncio.Event() 153 | async def on_url_change(): 154 | # 检查是否是主框架的变化 155 | if page.url != original_url: 156 | url_changed_event.set() 157 | async with async_playwright() as playwright: 158 | options = { 159 | 'args': [ 160 | '--lang en-GB' 161 | ], 162 | 'headless': False, # Set headless option here 163 | } 164 | # Make sure to run headed. 165 | browser = await playwright.chromium.launch(**options) 166 | # Setup context however you like. 167 | context = await browser.new_context() # Pass any options 168 | context = await set_init_script(context) 169 | # Pause the page, and start recording manually. 170 | page = await context.new_page() 171 | await page.goto("https://cp.kuaishou.com") 172 | 173 | # 定位并点击“立即登录”按钮(类型为 link) 174 | await page.get_by_role("link", name="立即登录").click() 175 | await page.get_by_text("扫码登录").click() 176 | img_locator = page.get_by_role("img", name="qrcode") 177 | # 获取 src 属性值 178 | src = await img_locator.get_attribute("src") 179 | original_url = page.url 180 | print("✅ 图片地址:", src) 181 | status_queue.put(src) 182 | # 监听页面的 'framenavigated' 事件,只关注主框架的变化 183 | page.on('framenavigated', 184 | lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) 185 | 186 | try: 187 | # 等待 URL 变化或超时 188 | await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 189 | print("监听页面跳转成功") 190 | except asyncio.TimeoutError: 191 | status_queue.put("500") 192 | print("监听页面跳转超时") 193 | await page.close() 194 | await context.close() 195 | await browser.close() 196 | return None 197 | uuid_v1 = uuid.uuid1() 198 | print(f"UUID v1: {uuid_v1}") 199 | await context.storage_state(path=Path(BASE_DIR / "cookiesFile" / f"{uuid_v1}.json")) 200 | result = await check_cookie(4, f"{uuid_v1}.json") 201 | if not result: 202 | status_queue.put("500") 203 | await page.close() 204 | await context.close() 205 | await browser.close() 206 | return None 207 | await page.close() 208 | await context.close() 209 | await browser.close() 210 | 211 | with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: 212 | cursor = conn.cursor() 213 | cursor.execute(''' 214 | INSERT INTO user_info (type, filePath, userName, status) 215 | VALUES (?, ?, ?, ?) 216 | ''', (4, f"{uuid_v1}.json", id, 1)) 217 | conn.commit() 218 | print("✅ 用户状态已记录") 219 | status_queue.put("200") 220 | 221 | # 小红书登录 222 | async def xiaohongshu_cookie_gen(id,status_queue): 223 | url_changed_event = asyncio.Event() 224 | 225 | async def on_url_change(): 226 | # 检查是否是主框架的变化 227 | if page.url != original_url: 228 | url_changed_event.set() 229 | 230 | async with async_playwright() as playwright: 231 | options = { 232 | 'args': [ 233 | '--lang en-GB' 234 | ], 235 | 'headless': False, # Set headless option here 236 | } 237 | # Make sure to run headed. 238 | browser = await playwright.chromium.launch(**options) 239 | # Setup context however you like. 240 | context = await browser.new_context() # Pass any options 241 | context = await set_init_script(context) 242 | # Pause the page, and start recording manually. 243 | page = await context.new_page() 244 | await page.goto("https://creator.xiaohongshu.com/") 245 | await page.locator('img.css-wemwzq').click() 246 | 247 | img_locator = page.get_by_role("img").nth(2) 248 | # 获取 src 属性值 249 | src = await img_locator.get_attribute("src") 250 | original_url = page.url 251 | print("✅ 图片地址:", src) 252 | status_queue.put(src) 253 | # 监听页面的 'framenavigated' 事件,只关注主框架的变化 254 | page.on('framenavigated', 255 | lambda frame: asyncio.create_task(on_url_change()) if frame == page.main_frame else None) 256 | 257 | try: 258 | # 等待 URL 变化或超时 259 | await asyncio.wait_for(url_changed_event.wait(), timeout=200) # 最多等待 200 秒 260 | print("监听页面跳转成功") 261 | except asyncio.TimeoutError: 262 | status_queue.put("500") 263 | print("监听页面跳转超时") 264 | await page.close() 265 | await context.close() 266 | await browser.close() 267 | return None 268 | uuid_v1 = uuid.uuid1() 269 | print(f"UUID v1: {uuid_v1}") 270 | await context.storage_state(path=Path(BASE_DIR / "cookiesFile" / f"{uuid_v1}.json")) 271 | result = await check_cookie(1, f"{uuid_v1}.json") 272 | if not result: 273 | status_queue.put("500") 274 | await page.close() 275 | await context.close() 276 | await browser.close() 277 | return None 278 | await page.close() 279 | await context.close() 280 | await browser.close() 281 | 282 | with sqlite3.connect(Path(BASE_DIR / "db" / "database.db")) as conn: 283 | cursor = conn.cursor() 284 | cursor.execute(''' 285 | INSERT INTO user_info (type, filePath, userName, status) 286 | VALUES (?, ?, ?, ?) 287 | ''', (1, f"{uuid_v1}.json", id, 1)) 288 | conn.commit() 289 | print("✅ 用户状态已记录") 290 | status_queue.put("200") 291 | 292 | # a = asyncio.run(xiaohongshu_cookie_gen(4,None)) 293 | # print(a) 294 | -------------------------------------------------------------------------------- /myUtils/postVideo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | from conf import BASE_DIR 5 | from uploader.douyin_uploader.main import DouYinVideo 6 | from uploader.ks_uploader.main import KSVideo 7 | from uploader.tencent_uploader.main import TencentVideo 8 | from uploader.xiaohongshu_uploader.main import XiaoHongShuVideo 9 | from utils.constant import TencentZoneTypes 10 | from utils.files_times import generate_schedule_time_next_day 11 | 12 | 13 | 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): 14 | # 生成文件的完整路径 15 | account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] 16 | files = [Path(BASE_DIR / "videoFile" / file) for file in files] 17 | if enableTimer: 18 | publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) 19 | else: 20 | publish_datetimes = [0 for i in range(len(files))] 21 | for index, file in enumerate(files): 22 | for cookie in account_file: 23 | print(f"文件路径{str(file)}") 24 | # 打印视频文件名、标题和 hashtag 25 | print(f"视频文件名:{file}") 26 | print(f"标题:{title}") 27 | print(f"Hashtag:{tags}") 28 | app = TencentVideo(title, str(file), tags, publish_datetimes[index], cookie, category) 29 | asyncio.run(app.main(), debug=False) 30 | 31 | 32 | def post_video_DouYin(title,files,tags,account_file,category=TencentZoneTypes.LIFESTYLE.value,enableTimer=False,videos_per_day = 1, daily_times=None,start_days = 0): 33 | # 生成文件的完整路径 34 | account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] 35 | files = [Path(BASE_DIR / "videoFile" / file) for file in files] 36 | if enableTimer: 37 | publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) 38 | else: 39 | publish_datetimes = [0 for i in range(len(files))] 40 | for index, file in enumerate(files): 41 | for cookie in account_file: 42 | print(f"文件路径{str(file)}") 43 | # 打印视频文件名、标题和 hashtag 44 | print(f"视频文件名:{file}") 45 | print(f"标题:{title}") 46 | print(f"Hashtag:{tags}") 47 | app = DouYinVideo(title, str(file), tags, publish_datetimes[index], cookie, category) 48 | asyncio.run(app.main(), debug=False) 49 | 50 | 51 | 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): 52 | # 生成文件的完整路径 53 | account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] 54 | files = [Path(BASE_DIR / "videoFile" / file) for file in files] 55 | if enableTimer: 56 | publish_datetimes = generate_schedule_time_next_day(len(files), videos_per_day, daily_times,start_days) 57 | else: 58 | publish_datetimes = [0 for i in range(len(files))] 59 | for index, file in enumerate(files): 60 | for cookie in account_file: 61 | print(f"文件路径{str(file)}") 62 | # 打印视频文件名、标题和 hashtag 63 | print(f"视频文件名:{file}") 64 | print(f"标题:{title}") 65 | print(f"Hashtag:{tags}") 66 | app = KSVideo(title, str(file), tags, publish_datetimes[index], cookie) 67 | asyncio.run(app.main(), debug=False) 68 | 69 | 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): 70 | # 生成文件的完整路径 71 | account_file = [Path(BASE_DIR / "cookiesFile" / file) for file in account_file] 72 | files = [Path(BASE_DIR / "videoFile" / file) for file in files] 73 | file_num = len(files) 74 | if enableTimer: 75 | publish_datetimes = generate_schedule_time_next_day(file_num, videos_per_day, daily_times,start_days) 76 | else: 77 | publish_datetimes = 0 78 | for index, file in enumerate(files): 79 | for cookie in account_file: 80 | # 打印视频文件名、标题和 hashtag 81 | print(f"视频文件名:{file}") 82 | print(f"标题:{title}") 83 | print(f"Hashtag:{tags}") 84 | app = XiaoHongShuVideo(title, file, tags, publish_datetimes, cookie) 85 | asyncio.run(app.main(), debug=False) 86 | 87 | 88 | 89 | # post_video("333",["demo.mp4"],"d","d") 90 | # post_video_DouYin("333",["demo.mp4"],"d","d") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/requirements.txt -------------------------------------------------------------------------------- /sau_backend/README.md: -------------------------------------------------------------------------------- 1 | ## 启动项目: 2 | python 版本:3.10 3 | 1. 安装依赖 4 | pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 5 | 2. 删除 db 目录下 database.db(如果没有直接运行createTable.py即可),运行 createTable.py 重新建库,避免出现脏数据 6 | 3. 修改 conf.py最下方 LOCAL_CHROME_PATH 为本地 chrome 浏览器地址 7 | 4. 运行根目录的 sau_backend.py 8 | 5. type字段(平台标识) 1 小红书 2 视频号 3 抖音 4 快手 9 | ## 接口说明 10 | 1. /upload post 11 | 上传接口,上传成功会返回文件的唯一id,后期靠这个发布视频 12 | 2. /login id参数 用户名 type参数 平台标识:登录流程,前端和后端建立sse连接,后端获取到图片base64编码后返回给前端,前端接受扫码后后端存库后返回200,前端主动断开连接,然后调取/getValidAccounts获取当前所有可用账号 13 | 3. /getValidAccounts 会获取当前所有可用cookie,时间较慢,会逐个校验cookie,status 1 有效 0 无效cookie 14 | 4. /postVideo 发布视频接口 post json传参 15 | file_list /upload获取的文件唯一标识 16 | account_list /getValidAccounts获取的filePath字段 17 | type 类型字段(平台标识) 18 | title 视频标题 19 | tags 视频tag 列表,不带# 20 | category 原作者说是原创表示,0表示不是原创其他表示为原创,但测试该字段没有效果 21 | enableTimer 是否开启定时发布,默认关闭,开启传True,如果开启,下面三个必传,否则不传 22 | videos_per_day 每天发布几个视频 23 | daily_times 每天发布视频的时间,整形列表,与上面列表长度保持一致 24 | start_days 开始天数,0 代表明天开始定时发布 1 代表明天的明天 25 | 以上三个字段是我的理解,不知道对不对,也不知道原作者为什么要这么设置 26 | ## 数据库说明 27 | 见当前目录下 db目录,py文件是创建脚本,db文件是sqlite数据库 28 | ## 文件说明 29 | cookiesFile文件夹 存储cookie文件 30 | myUtils文件夹 存储自己封装的python模块 31 | videoFile文件夹 文件上传存放位置 32 | web 文件夹 web路由目录 33 | conf.py 全局配置,记得修改配置中 LOCAL_CHROME_PATH 为本机浏览器地址 -------------------------------------------------------------------------------- /sau_frontend/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境配置 2 | NODE_ENV=development 3 | 4 | # API 基础地址 5 | VITE_API_BASE_URL=http://localhost:5409 6 | 7 | # 应用端口 8 | VITE_PORT=5173 9 | 10 | # 是否开启 mock 11 | VITE_USE_MOCK=true -------------------------------------------------------------------------------- /sau_frontend/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境配置 2 | NODE_ENV=production 3 | 4 | # API 基础地址 5 | VITE_API_BASE_URL=http://localhost:5409 6 | 7 | # 是否开启 mock 8 | VITE_USE_MOCK=false -------------------------------------------------------------------------------- /sau_frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue3 + Vite 项目 2 | 3 | 一个基于 Vue3、Vite、Element Plus、Pinia、Vue Router 和 Axios 的现代化前端项目模板。 4 | 5 | ## 🚀 特性 6 | 7 | - ⚡️ **Vite** - 极速的构建工具 8 | - 🖖 **Vue 3** - 渐进式 JavaScript 框架 9 | - 🎨 **Element Plus** - 基于 Vue 3 的组件库 10 | - 🗂 **Vue Router** - 官方路由管理器(WebHash 模式) 11 | - 📦 **Pinia** - 新一代状态管理 12 | - 🔗 **Axios** - HTTP 请求库(已封装) 13 | - 🎯 **Sass** - CSS 预处理器 14 | - 📁 **规范化目录结构** - views 存放页面,components 存放组件 15 | - 🔧 **完整配置** - 包含开发和生产环境配置 16 | 17 | ## 📦 安装 18 | 19 | ```bash 20 | # 安装依赖 21 | npm install 22 | 23 | # 启动开发服务器 24 | npm run dev 25 | 26 | # 构建生产版本 27 | npm run build 28 | 29 | # 预览生产构建 30 | npm run preview 31 | ``` 32 | 33 | ## 📁 项目结构 34 | 35 | ``` 36 | src/ 37 | ├── api/ # API 接口 38 | │ ├── index.js # API 统一导出 39 | │ └── user.js # 用户相关 API 40 | ├── components/ # 公共组件 41 | │ └── HelloWorld.vue # 示例组件 42 | ├── router/ # 路由配置 43 | │ └── index.js # 路由主文件 44 | ├── stores/ # 状态管理 45 | │ ├── index.js # Pinia 配置 46 | │ └── user.js # 用户状态 47 | ├── styles/ # 样式文件 48 | │ ├── index.scss # 主样式文件 49 | │ ├── reset.scss # 重置样式 50 | │ └── variables.scss # 样式变量 51 | ├── utils/ # 工具函数 52 | │ └── request.js # HTTP 请求封装 53 | ├── views/ # 页面组件 54 | │ ├── Home.vue # 首页 55 | │ └── About.vue # 关于页面 56 | ├── App.vue # 根组件 57 | └── main.js # 入口文件 58 | ``` 59 | 60 | ## 🔧 配置说明 61 | 62 | ### 环境变量 63 | 64 | - `.env` - 通用环境变量 65 | - `.env.development` - 开发环境变量 66 | - `.env.production` - 生产环境变量 67 | 68 | ### 路由配置 69 | 70 | 项目使用 Vue Router 4,配置为 WebHash 模式,路由文件位于 `src/router/index.js`。 71 | 72 | ### 状态管理 73 | 74 | 使用 Pinia 进行状态管理,store 文件位于 `src/stores/` 目录。 75 | 76 | ### HTTP 请求 77 | 78 | Axios 已经过封装,包含: 79 | - 请求/响应拦截器 80 | - 错误处理 81 | - Token 自动添加 82 | - 统一的响应格式处理 83 | 84 | 使用方式: 85 | ```javascript 86 | import { http } from '@/utils/request' 87 | 88 | // GET 请求 89 | const data = await http.get('/api/users') 90 | 91 | // POST 请求 92 | const result = await http.post('/api/users', { name: 'John' }) 93 | ``` 94 | 95 | ### 样式系统 96 | 97 | - 使用 Sass 作为 CSS 预处理器 98 | - 已删除所有浏览器默认样式 99 | - 提供了完整的样式变量和工具类 100 | - 支持 Element Plus 主题定制 101 | 102 | ## 🎨 组件库 103 | 104 | 项目集成了 Element Plus,所有组件都可以直接使用: 105 | 106 | ```vue 107 | 111 | ``` 112 | 113 | ## 📝 开发规范 114 | 115 | 1. **页面组件** 放在 `src/views/` 目录 116 | 2. **公共组件** 放在 `src/components/` 目录 117 | 3. **使用 setup 语法糖** 编写组件 118 | 4. **样式使用 Sass** 并遵循 BEM 命名规范 119 | 5. **API 请求** 统一放在 `src/api/` 目录 120 | 6. **状态管理** 按模块划分,放在 `src/stores/` 目录 121 | 122 | ## 🚀 部署 123 | 124 | ```bash 125 | # 构建生产版本 126 | npm run build 127 | 128 | # 构建完成后,dist 目录包含所有静态文件 129 | # 可以部署到任何静态文件服务器 130 | ``` 131 | 132 | ## 📄 许可证 133 | 134 | MIT License 135 | -------------------------------------------------------------------------------- /sau_frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SAU自媒体自动化运营系统 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sau_frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sau-admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "author": "Edan.Lee", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@element-plus/icons-vue": "^2.3.1", 14 | "axios": "^1.9.0", 15 | "element-plus": "^2.9.11", 16 | "pinia": "^3.0.2", 17 | "sass": "^1.89.1", 18 | "vue": "^3.5.13", 19 | "vue-router": "^4.5.1" 20 | }, 21 | "devDependencies": { 22 | "@vitejs/plugin-vue": "^5.2.3", 23 | "vite": "^6.3.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sau_frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sau_frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 88 | 89 | 203 | -------------------------------------------------------------------------------- /sau_frontend/src/api/account.js: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/request' 2 | 3 | // 账号管理相关API 4 | export const accountApi = { 5 | // 获取有效账号列表 6 | getValidAccounts() { 7 | return http.get('/getValidAccounts') 8 | }, 9 | 10 | // 添加账号 11 | addAccount(data) { 12 | return http.post('/account', data) 13 | }, 14 | 15 | // 更新账号 16 | updateAccount(data) { 17 | return http.post('/updateUserinfo', data) 18 | }, 19 | 20 | // 删除账号 21 | deleteAccount(id) { 22 | return http.get(`/deleteAccount?id=${id}`) 23 | } 24 | } -------------------------------------------------------------------------------- /sau_frontend/src/api/index.js: -------------------------------------------------------------------------------- 1 | // API 统一导出 2 | export * from './user' 3 | export * from './account' 4 | export * from './material' 5 | 6 | // 可以在这里添加其他API模块的导出 7 | // export * from './product' 8 | // export * from './order' 9 | // export * from './common' -------------------------------------------------------------------------------- /sau_frontend/src/api/material.js: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/request' 2 | 3 | // 素材管理API 4 | export const materialApi = { 5 | // 获取所有素材 6 | getAllMaterials: () => { 7 | return http.get('/getFiles') 8 | }, 9 | 10 | // 上传素材 11 | uploadMaterial: (formData) => { 12 | // 使用http.upload方法,它已经配置了正确的Content-Type 13 | return http.upload('/uploadSave', formData) 14 | }, 15 | 16 | // 删除素材 17 | deleteMaterial: (id) => { 18 | return http.get(`/deleteFile?id=${id}`) 19 | }, 20 | 21 | // 下载素材 22 | downloadMaterial: (filePath) => { 23 | return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/download/${filePath}` 24 | }, 25 | 26 | // 获取素材预览URL 27 | getMaterialPreviewUrl: (filename) => { 28 | return `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409'}/getFile?filename=${filename}` 29 | } 30 | } -------------------------------------------------------------------------------- /sau_frontend/src/api/user.js: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/request' 2 | 3 | // 用户相关API 4 | export const userApi = { 5 | // 获取用户信息 6 | getUserInfo(id) { 7 | return http.get(`/user/${id}`) 8 | }, 9 | 10 | // 获取用户列表 11 | getUserList(params) { 12 | return http.get('/user/list', params) 13 | }, 14 | 15 | // 创建用户 16 | createUser(data) { 17 | return http.post('/user', data) 18 | }, 19 | 20 | // 更新用户信息 21 | updateUser(id, data) { 22 | return http.put(`/user/${id}`, data) 23 | }, 24 | 25 | // 删除用户 26 | deleteUser(id) { 27 | return http.delete(`/user/${id}`) 28 | }, 29 | 30 | // 用户登录 31 | login(data) { 32 | return http.post('/auth/login', data) 33 | }, 34 | 35 | // 用户注册 36 | register(data) { 37 | return http.post('/auth/register', data) 38 | }, 39 | 40 | // 用户登出 41 | logout() { 42 | return http.post('/auth/logout') 43 | }, 44 | 45 | // 刷新token 46 | refreshToken() { 47 | return http.post('/auth/refresh') 48 | } 49 | } -------------------------------------------------------------------------------- /sau_frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sau_frontend/src/components/helloworld.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | 36 | 67 | -------------------------------------------------------------------------------- /sau_frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import pinia from './stores' 5 | import ElementPlus from 'element-plus' 6 | import 'element-plus/dist/index.css' 7 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 8 | import './styles/index.scss' 9 | 10 | const app = createApp(App) 11 | 12 | // 注册 Element Plus 图标 13 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 14 | app.component(key, component) 15 | } 16 | 17 | app.use(router) 18 | app.use(pinia) 19 | app.use(ElementPlus) 20 | app.mount('#app') 21 | -------------------------------------------------------------------------------- /sau_frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import Dashboard from '../views/Dashboard.vue' 3 | import AccountManagement from '../views/AccountManagement.vue' 4 | import MaterialManagement from '../views/MaterialManagement.vue' 5 | import PublishCenter from '../views/PublishCenter.vue' 6 | import About from '../views/About.vue' 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Dashboard', 12 | component: Dashboard 13 | }, 14 | { 15 | path: '/account-management', 16 | name: 'AccountManagement', 17 | component: AccountManagement 18 | }, 19 | { 20 | path: '/material-management', 21 | name: 'MaterialManagement', 22 | component: MaterialManagement 23 | }, 24 | { 25 | path: '/publish-center', 26 | name: 'PublishCenter', 27 | component: PublishCenter 28 | }, 29 | { 30 | path: '/about', 31 | name: 'About', 32 | component: About 33 | } 34 | ] 35 | 36 | const router = createRouter({ 37 | history: createWebHashHistory(), 38 | routes 39 | }) 40 | 41 | export default router -------------------------------------------------------------------------------- /sau_frontend/src/stores/account.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useAccountStore = defineStore('account', () => { 5 | // 存储所有账号信息 6 | const accounts = ref([]) 7 | 8 | // 平台类型映射 9 | const platformTypes = { 10 | 1: '小红书', 11 | 2: '视频号', 12 | 3: '抖音', 13 | 4: '快手' 14 | } 15 | 16 | // 设置账号列表 17 | const setAccounts = (accountsData) => { 18 | // 转换后端返回的数据格式为前端使用的格式 19 | accounts.value = accountsData.map(item => { 20 | return { 21 | id: item[0], 22 | type: item[1], 23 | filePath: item[2], 24 | name: item[3], 25 | status: item[4] === 1 ? '正常' : '异常', 26 | platform: platformTypes[item[1]] || '未知', 27 | avatar: '/vite.svg' // 默认使用vite.svg作为头像 28 | } 29 | }) 30 | } 31 | 32 | // 添加账号 33 | const addAccount = (account) => { 34 | accounts.value.push(account) 35 | } 36 | 37 | // 更新账号 38 | const updateAccount = (id, updatedAccount) => { 39 | const index = accounts.value.findIndex(acc => acc.id === id) 40 | if (index !== -1) { 41 | accounts.value[index] = { ...accounts.value[index], ...updatedAccount } 42 | } 43 | } 44 | 45 | // 删除账号 46 | const deleteAccount = (id) => { 47 | accounts.value = accounts.value.filter(acc => acc.id !== id) 48 | } 49 | 50 | // 根据平台获取账号 51 | const getAccountsByPlatform = (platform) => { 52 | return accounts.value.filter(acc => acc.platform === platform) 53 | } 54 | 55 | return { 56 | accounts, 57 | setAccounts, 58 | addAccount, 59 | updateAccount, 60 | deleteAccount, 61 | getAccountsByPlatform 62 | } 63 | }) -------------------------------------------------------------------------------- /sau_frontend/src/stores/app.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useAppStore = defineStore('app', () => { 5 | // 是否是第一次进入账号管理页面 6 | const isFirstTimeAccountManagement = ref(true) 7 | 8 | // 是否是第一次进入素材管理页面 9 | const isFirstTimeMaterialManagement = ref(true) 10 | 11 | // 账号管理页面刷新状态 12 | const isAccountRefreshing = ref(false) 13 | 14 | // 素材列表数据 15 | const materials = ref([]) 16 | 17 | // 设置账号管理页面已访问 18 | const setAccountManagementVisited = () => { 19 | isFirstTimeAccountManagement.value = false 20 | } 21 | 22 | // 设置素材管理页面已访问 23 | const setMaterialManagementVisited = () => { 24 | isFirstTimeMaterialManagement.value = false 25 | } 26 | 27 | // 重置所有访问状态(用于重新登录或刷新应用时) 28 | const resetVisitStatus = () => { 29 | isFirstTimeAccountManagement.value = true 30 | isFirstTimeMaterialManagement.value = true 31 | } 32 | 33 | // 更新素材列表 34 | const setMaterials = (materialList) => { 35 | materials.value = materialList 36 | } 37 | 38 | // 添加新素材 39 | const addMaterial = (material) => { 40 | materials.value.push(material) 41 | } 42 | 43 | // 删除素材 44 | const removeMaterial = (materialId) => { 45 | const index = materials.value.findIndex(m => m.id === materialId) 46 | if (index > -1) { 47 | materials.value.splice(index, 1) 48 | } 49 | } 50 | 51 | // 设置账号管理页面刷新状态 52 | const setAccountRefreshing = (status) => { 53 | isAccountRefreshing.value = status 54 | } 55 | 56 | return { 57 | isFirstTimeAccountManagement, 58 | isFirstTimeMaterialManagement, 59 | isAccountRefreshing, 60 | materials, 61 | setAccountManagementVisited, 62 | setMaterialManagementVisited, 63 | resetVisitStatus, 64 | setMaterials, 65 | addMaterial, 66 | removeMaterial, 67 | setAccountRefreshing 68 | } 69 | }) -------------------------------------------------------------------------------- /sau_frontend/src/stores/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { useUserStore } from './user' 3 | import { useAccountStore } from './account' 4 | import { useAppStore } from './app' 5 | 6 | const pinia = createPinia() 7 | 8 | export default pinia 9 | export { useUserStore, useAccountStore, useAppStore } -------------------------------------------------------------------------------- /sau_frontend/src/stores/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useUserStore = defineStore('user', () => { 5 | const userInfo = ref({ 6 | name: '', 7 | email: '' 8 | }) 9 | 10 | const isLoggedIn = ref(false) 11 | 12 | const setUserInfo = (info) => { 13 | userInfo.value = info 14 | isLoggedIn.value = true 15 | } 16 | 17 | const logout = () => { 18 | userInfo.value = { 19 | name: '', 20 | email: '' 21 | } 22 | isLoggedIn.value = false 23 | } 24 | 25 | return { 26 | userInfo, 27 | isLoggedIn, 28 | setUserInfo, 29 | logout 30 | } 31 | }) -------------------------------------------------------------------------------- /sau_frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // 导入重置样式 2 | @use './reset.scss'; 3 | 4 | // 导入变量 5 | @use './variables.scss' as *; 6 | 7 | // 全局样式 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | color: $text-primary; 13 | background-color: $bg-color-page; 14 | } 15 | 16 | #app { 17 | min-height: 100vh; 18 | } 19 | 20 | // 通用工具类 21 | .text-center { 22 | text-align: center; 23 | } 24 | 25 | .text-left { 26 | text-align: left; 27 | } 28 | 29 | .text-right { 30 | text-align: right; 31 | } 32 | 33 | .flex { 34 | display: flex; 35 | } 36 | 37 | .flex-center { 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .flex-between { 44 | display: flex; 45 | justify-content: space-between; 46 | align-items: center; 47 | } 48 | 49 | .flex-column { 50 | display: flex; 51 | flex-direction: column; 52 | } 53 | 54 | .w-full { 55 | width: 100%; 56 | } 57 | 58 | .h-full { 59 | height: 100%; 60 | } 61 | 62 | // 间距工具类 63 | .m-0 { margin: 0; } 64 | .mt-0 { margin-top: 0; } 65 | .mr-0 { margin-right: 0; } 66 | .mb-0 { margin-bottom: 0; } 67 | .ml-0 { margin-left: 0; } 68 | 69 | .m-1 { margin: $spacing-xs; } 70 | .mt-1 { margin-top: $spacing-xs; } 71 | .mr-1 { margin-right: $spacing-xs; } 72 | .mb-1 { margin-bottom: $spacing-xs; } 73 | .ml-1 { margin-left: $spacing-xs; } 74 | 75 | .m-2 { margin: $spacing-sm; } 76 | .mt-2 { margin-top: $spacing-sm; } 77 | .mr-2 { margin-right: $spacing-sm; } 78 | .mb-2 { margin-bottom: $spacing-sm; } 79 | .ml-2 { margin-left: $spacing-sm; } 80 | 81 | .m-3 { margin: $spacing-md; } 82 | .mt-3 { margin-top: $spacing-md; } 83 | .mr-3 { margin-right: $spacing-md; } 84 | .mb-3 { margin-bottom: $spacing-md; } 85 | .ml-3 { margin-left: $spacing-md; } 86 | 87 | .m-4 { margin: $spacing-lg; } 88 | .mt-4 { margin-top: $spacing-lg; } 89 | .mr-4 { margin-right: $spacing-lg; } 90 | .mb-4 { margin-bottom: $spacing-lg; } 91 | .ml-4 { margin-left: $spacing-lg; } 92 | 93 | .p-0 { padding: 0; } 94 | .pt-0 { padding-top: 0; } 95 | .pr-0 { padding-right: 0; } 96 | .pb-0 { padding-bottom: 0; } 97 | .pl-0 { padding-left: 0; } 98 | 99 | .p-1 { padding: $spacing-xs; } 100 | .pt-1 { padding-top: $spacing-xs; } 101 | .pr-1 { padding-right: $spacing-xs; } 102 | .pb-1 { padding-bottom: $spacing-xs; } 103 | .pl-1 { padding-left: $spacing-xs; } 104 | 105 | .p-2 { padding: $spacing-sm; } 106 | .pt-2 { padding-top: $spacing-sm; } 107 | .pr-2 { padding-right: $spacing-sm; } 108 | .pb-2 { padding-bottom: $spacing-sm; } 109 | .pl-2 { padding-left: $spacing-sm; } 110 | 111 | .p-3 { padding: $spacing-md; } 112 | .pt-3 { padding-top: $spacing-md; } 113 | .pr-3 { padding-right: $spacing-md; } 114 | .pb-3 { padding-bottom: $spacing-md; } 115 | .pl-3 { padding-left: $spacing-md; } 116 | 117 | .p-4 { padding: $spacing-lg; } 118 | .pt-4 { padding-top: $spacing-lg; } 119 | .pr-4 { padding-right: $spacing-lg; } 120 | .pb-4 { padding-bottom: $spacing-lg; } 121 | .pl-4 { padding-left: $spacing-lg; } -------------------------------------------------------------------------------- /sau_frontend/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* CSS Reset - 删除浏览器默认样式 */ 2 | 3 | /* 1. Use a more-intuitive box-sizing model */ 4 | *, *::before, *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* 2. Remove default margin and padding */ 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | /* 3. Allow percentage-based heights in the application */ 15 | html, body { 16 | height: 100%; 17 | } 18 | 19 | /* 4. Add accessible line-height and improve text rendering */ 20 | body { 21 | line-height: 1.5; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 25 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 26 | sans-serif; 27 | } 28 | 29 | /* 5. Improve media defaults */ 30 | img, picture, video, canvas, svg { 31 | display: block; 32 | max-width: 100%; 33 | } 34 | 35 | /* 6. Remove built-in form typography styles */ 36 | input, button, textarea, select { 37 | font: inherit; 38 | } 39 | 40 | /* 7. Avoid text overflows */ 41 | p, h1, h2, h3, h4, h5, h6 { 42 | overflow-wrap: break-word; 43 | } 44 | 45 | /* 8. Create a root stacking context */ 46 | #root, #__next, #app { 47 | isolation: isolate; 48 | } 49 | 50 | /* 9. Remove list styles */ 51 | ul, ol { 52 | list-style: none; 53 | } 54 | 55 | /* 10. Remove default button styles */ 56 | button { 57 | background: none; 58 | border: none; 59 | cursor: pointer; 60 | } 61 | 62 | /* 11. Remove default link styles */ 63 | a { 64 | text-decoration: none; 65 | color: inherit; 66 | } 67 | 68 | /* 12. Remove default table styles */ 69 | table { 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | } 73 | 74 | /* 13. Remove default fieldset styles */ 75 | fieldset { 76 | border: none; 77 | } 78 | 79 | /* 14. Remove default legend styles */ 80 | legend { 81 | display: table; 82 | } 83 | 84 | /* 15. Remove default details/summary styles */ 85 | details { 86 | display: block; 87 | } 88 | 89 | summary { 90 | display: list-item; 91 | } 92 | 93 | /* 16. Remove default hr styles */ 94 | hr { 95 | border: none; 96 | height: 1px; 97 | background: #ccc; 98 | } 99 | 100 | /* 17. Remove default blockquote styles */ 101 | blockquote { 102 | quotes: none; 103 | } 104 | 105 | blockquote:before, 106 | blockquote:after { 107 | content: ''; 108 | content: none; 109 | } 110 | 111 | /* 18. Remove default cite styles */ 112 | cite { 113 | font-style: normal; 114 | } 115 | 116 | /* 19. Remove default address styles */ 117 | address { 118 | font-style: normal; 119 | } -------------------------------------------------------------------------------- /sau_frontend/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // 颜色变量 2 | $primary-color: #409eff; 3 | $success-color: #67c23a; 4 | $warning-color: #e6a23c; 5 | $danger-color: #f56c6c; 6 | $info-color: #909399; 7 | 8 | // 文字颜色 9 | $text-primary: #303133; 10 | $text-regular: #606266; 11 | $text-secondary: #909399; 12 | $text-placeholder: #c0c4cc; 13 | 14 | // 边框颜色 15 | $border-base: #dcdfe6; 16 | $border-light: #e4e7ed; 17 | $border-lighter: #ebeef5; 18 | $border-extra-light: #f2f6fc; 19 | 20 | // 背景颜色 21 | $bg-color: #ffffff; 22 | $bg-color-page: #f2f3f5; 23 | $bg-color-overlay: #ffffff; 24 | 25 | // 字体大小 26 | $font-size-extra-large: 20px; 27 | $font-size-large: 18px; 28 | $font-size-medium: 16px; 29 | $font-size-base: 14px; 30 | $font-size-small: 13px; 31 | $font-size-extra-small: 12px; 32 | 33 | // 间距 34 | $spacing-xs: 4px; 35 | $spacing-sm: 8px; 36 | $spacing-md: 16px; 37 | $spacing-lg: 24px; 38 | $spacing-xl: 32px; 39 | 40 | // 圆角 41 | $border-radius-base: 4px; 42 | $border-radius-small: 2px; 43 | $border-radius-round: 20px; 44 | $border-radius-circle: 50%; 45 | 46 | // 阴影 47 | $box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04); 48 | $box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12); 49 | $box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1); 50 | 51 | // 层级 52 | $z-index-normal: 1; 53 | $z-index-top: 1000; 54 | $z-index-popper: 2000; -------------------------------------------------------------------------------- /sau_frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessage } from 'element-plus' 3 | 4 | // 创建axios实例 5 | const request = axios.create({ 6 | baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5409', 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | } 10 | }) 11 | 12 | // 请求拦截器 13 | request.interceptors.request.use( 14 | (config) => { 15 | // 可以在这里添加token等认证信息 16 | const token = localStorage.getItem('token') 17 | if (token) { 18 | config.headers.Authorization = `Bearer ${token}` 19 | } 20 | return config 21 | }, 22 | (error) => { 23 | console.error('请求错误:', error) 24 | return Promise.reject(error) 25 | } 26 | ) 27 | 28 | // 响应拦截器 29 | request.interceptors.response.use( 30 | (response) => { 31 | const { data } = response 32 | 33 | // 根据后端接口规范处理响应 34 | if (data.code === 200 || data.success) { 35 | return data 36 | } else { 37 | ElMessage.error(data.message || '请求失败') 38 | return Promise.reject(new Error(data.message || '请求失败')) 39 | } 40 | }, 41 | (error) => { 42 | console.error('响应错误:', error) 43 | 44 | // 处理HTTP错误状态码 45 | if (error.response) { 46 | const { status } = error.response 47 | switch (status) { 48 | case 401: 49 | ElMessage.error('未授权,请重新登录') 50 | // 可以在这里处理登录跳转 51 | break 52 | case 403: 53 | ElMessage.error('拒绝访问') 54 | break 55 | case 404: 56 | ElMessage.error('请求地址不存在') 57 | break 58 | case 500: 59 | ElMessage.error('服务器内部错误') 60 | break 61 | default: 62 | ElMessage.error('网络错误') 63 | } 64 | } else { 65 | ElMessage.error('网络连接失败') 66 | } 67 | 68 | return Promise.reject(error) 69 | } 70 | ) 71 | 72 | // 封装常用的请求方法 73 | export const http = { 74 | get(url, params) { 75 | return request.get(url, { params }) 76 | }, 77 | 78 | post(url, data, config = {}) { 79 | return request.post(url, data, config) 80 | }, 81 | 82 | put(url, data, config = {}) { 83 | return request.put(url, data, config) 84 | }, 85 | 86 | delete(url, params) { 87 | return request.delete(url, { params }) 88 | }, 89 | 90 | upload(url, formData) { 91 | return request.post(url, formData, { 92 | headers: { 93 | 'Content-Type': 'multipart/form-data' 94 | } 95 | }) 96 | } 97 | } 98 | 99 | export default request -------------------------------------------------------------------------------- /sau_frontend/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sau_frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 205 | 206 | 379 | 380 | -------------------------------------------------------------------------------- /sau_frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /sau_frontend/src/views/MaterialManagement.vue: -------------------------------------------------------------------------------- 1 | 124 | 125 | 334 | 335 | -------------------------------------------------------------------------------- /sau_frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | '@': resolve(__dirname, 'src'), 11 | }, 12 | }, 13 | css: { 14 | preprocessorOptions: { 15 | scss: { 16 | // 移除自动导入,改用@use语法 17 | } 18 | } 19 | }, 20 | server: { 21 | port: 5173, 22 | open: true 23 | }, 24 | build: { 25 | outDir: 'dist', 26 | sourcemap: false, 27 | chunkSizeWarningLimit: 1600, 28 | rollupOptions: { 29 | output: { 30 | manualChunks: { 31 | vue: ['vue', 'vue-router', 'pinia'], 32 | elementPlus: ['element-plus'], 33 | utils: ['axios'] 34 | } 35 | } 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/baijiahao_uploader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/uploader/baijiahao_uploader/__init__.py -------------------------------------------------------------------------------- /uploader/bilibili_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "bilibili_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/bilibili_uploader/biliup.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/uploader/bilibili_uploader/biliup.exe -------------------------------------------------------------------------------- /uploader/bilibili_uploader/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import random 4 | from biliup.plugins.bili_webup import BiliBili, Data 5 | 6 | from utils.log import bilibili_logger 7 | 8 | 9 | def extract_keys_from_json(data): 10 | """Extract specified keys from the provided JSON data.""" 11 | keys_to_extract = ["SESSDATA", "bili_jct", "DedeUserID__ckMd5", "DedeUserID", "access_token"] 12 | extracted_data = {} 13 | 14 | # Extracting cookie data 15 | for cookie in data['cookie_info']['cookies']: 16 | if cookie['name'] in keys_to_extract: 17 | extracted_data[cookie['name']] = cookie['value'] 18 | 19 | # Extracting access_token 20 | if "access_token" in data['token_info']: 21 | extracted_data['access_token'] = data['token_info']['access_token'] 22 | 23 | return extracted_data 24 | 25 | 26 | def read_cookie_json_file(filepath: pathlib.Path): 27 | with open(filepath, 'r', encoding='utf-8') as file: 28 | content = json.load(file) 29 | return content 30 | 31 | 32 | def random_emoji(): 33 | emoji_list = ["🍏", "🍎", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒", "🍑", "🍍", "🥭", "🥥", "🥝", 34 | "🍅", "🍆", "🥑", "🥦", "🥒", "🥬", "🌶", "🌽", "🥕", "🥔", "🍠", "🥐", "🍞", "🥖", "🥨", "🥯", "🧀", "🥚", "🍳", "🥞", 35 | "🥓", "🥩", "🍗", "🍖", "🌭", "🍔", "🍟", "🍕", "🥪", "🥙", "🌮", "🌯", "🥗", "🥘", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣", 36 | "🍱", "🥟", "🍤", "🍙", "🍚", "🍘", "🍥", "🥮", "🥠", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🍰", "🎂", "🍮", "🍭", "🍬", 37 | "🍫", "🍿", "🧂", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕️", "🍵", "🥤", "🍶", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹", 38 | "🍾", "🥄", "🍴", "🍽", "🥣", "🥡", "🥢"] 39 | return random.choice(emoji_list) 40 | 41 | 42 | class BilibiliUploader(object): 43 | def __init__(self, cookie_data, file: pathlib.Path, title, desc, tid, tags, dtime): 44 | self.upload_thread_num = 3 45 | self.copyright = 1 46 | self.lines = 'AUTO' 47 | self.cookie_data = cookie_data 48 | self.file = file 49 | self.title = title 50 | self.desc = desc 51 | self.tid = tid 52 | self.tags = tags 53 | self.dtime = dtime 54 | self._init_data() 55 | 56 | def _init_data(self): 57 | self.data = Data() 58 | self.data.copyright = self.copyright 59 | self.data.title = self.title 60 | self.data.desc = self.desc 61 | self.data.tid = self.tid 62 | self.data.set_tag(self.tags) 63 | self.data.dtime = self.dtime 64 | 65 | def upload(self): 66 | with BiliBili(self.data) as bili: 67 | bili.login_by_cookies(self.cookie_data) 68 | bili.access_token = self.cookie_data.get('access_token') 69 | video_part = bili.upload_file(str(self.file), lines=self.lines, 70 | tasks=self.upload_thread_num) # 上传视频,默认线路AUTO自动选择,线程数量3。 71 | video_part['title'] = self.title 72 | self.data.append(video_part) 73 | ret = bili.submit() # 提交视频 74 | if ret.get('code') == 0: 75 | bilibili_logger.success(f'[+] {self.file.name}上传 成功') 76 | return True 77 | else: 78 | bilibili_logger.error(f'[-] {self.file.name}上传 失败, error messge: {ret.get("message")}') 79 | return False 80 | -------------------------------------------------------------------------------- /uploader/douyin_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "douyin_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/douyin_uploader/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from playwright.async_api import Playwright, async_playwright, Page 5 | import os 6 | import asyncio 7 | 8 | from conf import LOCAL_CHROME_PATH 9 | from utils.base_social_media import set_init_script 10 | from utils.log import douyin_logger 11 | 12 | 13 | async def cookie_auth(account_file): 14 | async with async_playwright() as playwright: 15 | browser = await playwright.chromium.launch(headless=True) 16 | context = await browser.new_context(storage_state=account_file) 17 | context = await set_init_script(context) 18 | # 创建一个新的页面 19 | page = await context.new_page() 20 | # 访问指定的 URL 21 | await page.goto("https://creator.douyin.com/creator-micro/content/upload") 22 | try: 23 | await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000) 24 | except: 25 | print("[+] 等待5秒 cookie 失效") 26 | await context.close() 27 | await browser.close() 28 | return False 29 | # 2024.06.17 抖音创作者中心改版 30 | if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): 31 | print("[+] 等待5秒 cookie 失效") 32 | return False 33 | else: 34 | print("[+] cookie 有效") 35 | return True 36 | 37 | 38 | async def douyin_setup(account_file, handle=False): 39 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 40 | if not handle: 41 | # Todo alert message 42 | return False 43 | douyin_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') 44 | await douyin_cookie_gen(account_file) 45 | return True 46 | 47 | 48 | async def douyin_cookie_gen(account_file): 49 | async with async_playwright() as playwright: 50 | options = { 51 | 'headless': False 52 | } 53 | # Make sure to run headed. 54 | browser = await playwright.chromium.launch(**options) 55 | # Setup context however you like. 56 | context = await browser.new_context() # Pass any options 57 | context = await set_init_script(context) 58 | # Pause the page, and start recording manually. 59 | page = await context.new_page() 60 | await page.goto("https://creator.douyin.com/") 61 | await page.pause() 62 | # 点击调试器的继续,保存cookie 63 | await context.storage_state(path=account_file) 64 | 65 | 66 | class DouYinVideo(object): 67 | def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None): 68 | self.title = title # 视频标题 69 | self.file_path = file_path 70 | self.tags = tags 71 | self.publish_date = publish_date 72 | self.account_file = account_file 73 | self.date_format = '%Y年%m月%d日 %H:%M' 74 | self.local_executable_path = LOCAL_CHROME_PATH 75 | self.thumbnail_path = thumbnail_path 76 | 77 | async def set_schedule_time_douyin(self, page, publish_date): 78 | # 选择包含特定文本内容的 label 元素 79 | label_element = page.locator("[class^='radio']:has-text('定时发布')") 80 | # 在选中的 label 元素下点击 checkbox 81 | await label_element.click() 82 | await asyncio.sleep(1) 83 | publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M") 84 | 85 | await asyncio.sleep(1) 86 | await page.locator('.semi-input[placeholder="日期和时间"]').click() 87 | await page.keyboard.press("Control+KeyA") 88 | await page.keyboard.type(str(publish_date_hour)) 89 | await page.keyboard.press("Enter") 90 | 91 | await asyncio.sleep(1) 92 | 93 | async def handle_upload_error(self, page): 94 | douyin_logger.info('视频出错了,重新上传中') 95 | await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) 96 | 97 | async def upload(self, playwright: Playwright) -> None: 98 | # 使用 Chromium 浏览器启动一个浏览器实例 99 | if self.local_executable_path: 100 | browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path) 101 | else: 102 | browser = await playwright.chromium.launch(headless=False) 103 | # 创建一个浏览器上下文,使用指定的 cookie 文件 104 | context = await browser.new_context(storage_state=f"{self.account_file}") 105 | context = await set_init_script(context) 106 | 107 | # 创建一个新的页面 108 | page = await context.new_page() 109 | # 访问指定的 URL 110 | await page.goto("https://creator.douyin.com/creator-micro/content/upload") 111 | douyin_logger.info(f'[+]正在上传-------{self.title}.mp4') 112 | # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 113 | douyin_logger.info(f'[-] 正在打开主页...') 114 | await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload") 115 | # 点击 "上传视频" 按钮 116 | await page.locator("div[class^='container'] input").set_input_files(self.file_path) 117 | 118 | # 等待页面跳转到指定的 URL 2025.01.08修改在原有基础上兼容两种页面 119 | while True: 120 | try: 121 | # 尝试等待第一个 URL 122 | await page.wait_for_url( 123 | "https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page", timeout=3000) 124 | douyin_logger.info("[+] 成功进入version_1发布页面!") 125 | break # 成功进入页面后跳出循环 126 | except Exception: 127 | try: 128 | # 如果第一个 URL 超时,再尝试等待第二个 URL 129 | await page.wait_for_url( 130 | "https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page", 131 | timeout=3000) 132 | douyin_logger.info("[+] 成功进入version_2发布页面!") 133 | 134 | break # 成功进入页面后跳出循环 135 | except: 136 | print(" [-] 超时未进入视频发布页面,重新尝试...") 137 | await asyncio.sleep(0.5) # 等待 0.5 秒后重新尝试 138 | # 填充标题和话题 139 | # 检查是否存在包含输入框的元素 140 | # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素 141 | await asyncio.sleep(1) 142 | douyin_logger.info(f' [-] 正在填充标题和话题...') 143 | title_container = page.get_by_text('作品标题').locator("..").locator("xpath=following-sibling::div[1]").locator("input") 144 | if await title_container.count(): 145 | await title_container.fill(self.title[:30]) 146 | else: 147 | titlecontainer = page.locator(".notranslate") 148 | await titlecontainer.click() 149 | await page.keyboard.press("Backspace") 150 | await page.keyboard.press("Control+KeyA") 151 | await page.keyboard.press("Delete") 152 | await page.keyboard.type(self.title) 153 | await page.keyboard.press("Enter") 154 | css_selector = ".zone-container" 155 | for index, tag in enumerate(self.tags, start=1): 156 | await page.type(css_selector, "#" + tag) 157 | await page.press(css_selector, "Space") 158 | douyin_logger.info(f'总共添加{len(self.tags)}个话题') 159 | 160 | while True: 161 | # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 162 | try: 163 | # 新版:定位重新上传 164 | number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count() 165 | if number > 0: 166 | douyin_logger.success(" [-]视频上传完毕") 167 | break 168 | else: 169 | douyin_logger.info(" [-] 正在上传视频中...") 170 | await asyncio.sleep(2) 171 | 172 | if await page.locator('div.progress-div > div:has-text("上传失败")').count(): 173 | douyin_logger.error(" [-] 发现上传出错了... 准备重试") 174 | await self.handle_upload_error(page) 175 | except: 176 | douyin_logger.info(" [-] 正在上传视频中...") 177 | await asyncio.sleep(2) 178 | 179 | #上传视频封面 180 | await self.set_thumbnail(page, self.thumbnail_path) 181 | 182 | # 更换可见元素 183 | await self.set_location(page, "杭州市") 184 | 185 | # 頭條/西瓜 186 | third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' 187 | # 定位是否有第三方平台 188 | if await page.locator(third_part_element).count(): 189 | # 检测是否是已选中状态 190 | if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'): 191 | await page.locator(third_part_element).locator('input.semi-switch-native-control').click() 192 | 193 | if self.publish_date != 0: 194 | await self.set_schedule_time_douyin(page, self.publish_date) 195 | 196 | # 判断视频是否发布成功 197 | while True: 198 | # 判断视频是否发布成功 199 | try: 200 | publish_button = page.get_by_role('button', name="发布", exact=True) 201 | if await publish_button.count(): 202 | await publish_button.click() 203 | await page.wait_for_url("https://creator.douyin.com/creator-micro/content/manage**", 204 | timeout=3000) # 如果自动跳转到作品页面,则代表发布成功 205 | douyin_logger.success(" [-]视频发布成功") 206 | break 207 | except: 208 | douyin_logger.info(" [-] 视频正在发布中...") 209 | await page.screenshot(full_page=True) 210 | await asyncio.sleep(0.5) 211 | 212 | await context.storage_state(path=self.account_file) # 保存cookie 213 | douyin_logger.success(' [-]cookie更新完毕!') 214 | await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 215 | # 关闭浏览器上下文和浏览器实例 216 | await context.close() 217 | await browser.close() 218 | 219 | async def set_thumbnail(self, page: Page, thumbnail_path: str): 220 | if thumbnail_path: 221 | await page.click('text="选择封面"') 222 | await page.wait_for_selector("div.semi-modal-content:visible") 223 | await page.click('text="设置竖封面"') 224 | await page.wait_for_timeout(2000) # 等待2秒 225 | # 定位到上传区域并点击 226 | await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path) 227 | await page.wait_for_timeout(2000) # 等待2秒 228 | await page.locator("div[class^='extractFooter'] button:visible:has-text('完成')").click() 229 | # finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')") 230 | # if await finish_confirm_element.count(): 231 | # await finish_confirm_element.click() 232 | # await page.locator("div[class^='footer'] button:has-text('完成')").click() 233 | 234 | async def set_location(self, page: Page, location: str = "杭州市"): 235 | # todo supoort location later 236 | # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator( 237 | # "div.semi-select-single").nth(0).click() 238 | await page.locator('div.semi-select span:has-text("输入地理位置")').click() 239 | await page.keyboard.press("Backspace") 240 | await page.wait_for_timeout(2000) 241 | await page.keyboard.type(location) 242 | await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000) 243 | await page.locator('div[role="listbox"] [role="option"]').first.click() 244 | 245 | async def main(self): 246 | async with async_playwright() as playwright: 247 | await self.upload(playwright) 248 | 249 | 250 | -------------------------------------------------------------------------------- /uploader/ks_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "ks_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/ks_uploader/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from playwright.async_api import Playwright, async_playwright 5 | import os 6 | import asyncio 7 | 8 | from conf import LOCAL_CHROME_PATH 9 | from utils.base_social_media import set_init_script 10 | from utils.files_times import get_absolute_path 11 | from utils.log import kuaishou_logger 12 | 13 | 14 | async def cookie_auth(account_file): 15 | async with async_playwright() as playwright: 16 | browser = await playwright.chromium.launch(headless=True) 17 | context = await browser.new_context(storage_state=account_file) 18 | context = await set_init_script(context) 19 | # 创建一个新的页面 20 | page = await context.new_page() 21 | # 访问指定的 URL 22 | await page.goto("https://cp.kuaishou.com/article/publish/video") 23 | try: 24 | await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒 25 | 26 | kuaishou_logger.info("[+] 等待5秒 cookie 失效") 27 | return False 28 | except: 29 | kuaishou_logger.success("[+] cookie 有效") 30 | return True 31 | 32 | 33 | async def ks_setup(account_file, handle=False): 34 | account_file = get_absolute_path(account_file, "ks_uploader") 35 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 36 | if not handle: 37 | return False 38 | kuaishou_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') 39 | await get_ks_cookie(account_file) 40 | return True 41 | 42 | 43 | async def get_ks_cookie(account_file): 44 | async with async_playwright() as playwright: 45 | options = { 46 | 'args': [ 47 | '--lang en-GB' 48 | ], 49 | 'headless': False, # Set headless option here 50 | } 51 | # Make sure to run headed. 52 | browser = await playwright.chromium.launch(**options) 53 | # Setup context however you like. 54 | context = await browser.new_context() # Pass any options 55 | context = await set_init_script(context) 56 | # Pause the page, and start recording manually. 57 | page = await context.new_page() 58 | await page.goto("https://cp.kuaishou.com") 59 | await page.pause() 60 | # 点击调试器的继续,保存cookie 61 | await context.storage_state(path=account_file) 62 | 63 | 64 | class KSVideo(object): 65 | def __init__(self, title, file_path, tags, publish_date: datetime, account_file): 66 | self.title = title # 视频标题 67 | self.file_path = file_path 68 | self.tags = tags 69 | self.publish_date = publish_date 70 | self.account_file = account_file 71 | self.date_format = '%Y-%m-%d %H:%M' 72 | self.local_executable_path = LOCAL_CHROME_PATH 73 | 74 | async def handle_upload_error(self, page): 75 | kuaishou_logger.error("视频出错了,重新上传中") 76 | await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) 77 | 78 | async def upload(self, playwright: Playwright) -> None: 79 | # 使用 Chromium 浏览器启动一个浏览器实例 80 | print(self.local_executable_path) 81 | if self.local_executable_path: 82 | browser = await playwright.chromium.launch( 83 | headless=False, 84 | executable_path=self.local_executable_path, 85 | ) 86 | else: 87 | browser = await playwright.chromium.launch( 88 | headless=False 89 | ) # 创建一个浏览器上下文,使用指定的 cookie 文件 90 | context = await browser.new_context(storage_state=f"{self.account_file}") 91 | context = await set_init_script(context) 92 | # 创建一个新的页面 93 | page = await context.new_page() 94 | # 访问指定的 URL 95 | await page.goto("https://cp.kuaishou.com/article/publish/video") 96 | kuaishou_logger.info('正在上传-------{}.mp4'.format(self.title)) 97 | # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 98 | kuaishou_logger.info('正在打开主页...') 99 | await page.wait_for_url("https://cp.kuaishou.com/article/publish/video") 100 | # 点击 "上传视频" 按钮 101 | upload_button = page.locator("button[class^='_upload-btn']") 102 | await upload_button.wait_for(state='visible') # 确保按钮可见 103 | 104 | async with page.expect_file_chooser() as fc_info: 105 | await upload_button.click() 106 | file_chooser = await fc_info.value 107 | await file_chooser.set_files(self.file_path) 108 | 109 | await asyncio.sleep(2) 110 | 111 | # if not await page.get_by_text("封面编辑").count(): 112 | # raise Exception("似乎没有跳转到到编辑页面") 113 | 114 | await asyncio.sleep(1) 115 | 116 | # 等待按钮可交互 117 | new_feature_button = page.locator('button[type="button"] span:text("我知道了")') 118 | if await new_feature_button.count() > 0: 119 | await new_feature_button.click() 120 | 121 | kuaishou_logger.info("正在填充标题和话题...") 122 | await page.get_by_text("描述").locator("xpath=following-sibling::div").click() 123 | kuaishou_logger.info("clear existing title") 124 | await page.keyboard.press("Backspace") 125 | await page.keyboard.press("Control+KeyA") 126 | await page.keyboard.press("Delete") 127 | kuaishou_logger.info("filling new title") 128 | await page.keyboard.type(self.title) 129 | await page.keyboard.press("Enter") 130 | 131 | # 快手只能添加3个话题 132 | for index, tag in enumerate(self.tags[:3], start=1): 133 | kuaishou_logger.info("正在添加第%s个话题" % index) 134 | await page.keyboard.type(f"#{tag} ") 135 | await asyncio.sleep(2) 136 | 137 | max_retries = 60 # 设置最大重试次数,最大等待时间为 2 分钟 138 | retry_count = 0 139 | 140 | while retry_count < max_retries: 141 | try: 142 | # 获取包含 '上传中' 文本的元素数量 143 | number = await page.locator("text=上传中").count() 144 | 145 | if number == 0: 146 | kuaishou_logger.success("视频上传完毕") 147 | break 148 | else: 149 | if retry_count % 5 == 0: 150 | kuaishou_logger.info("正在上传视频中...") 151 | await asyncio.sleep(2) 152 | except Exception as e: 153 | kuaishou_logger.error(f"检查上传状态时发生错误: {e}") 154 | await asyncio.sleep(2) # 等待 2 秒后重试 155 | retry_count += 1 156 | 157 | if retry_count == max_retries: 158 | kuaishou_logger.warning("超过最大重试次数,视频上传可能未完成。") 159 | 160 | # 定时任务 161 | if self.publish_date != 0: 162 | await self.set_schedule_time(page, self.publish_date) 163 | 164 | # 判断视频是否发布成功 165 | while True: 166 | try: 167 | publish_button = page.get_by_text("发布", exact=True) 168 | if await publish_button.count() > 0: 169 | await publish_button.click() 170 | 171 | await asyncio.sleep(1) 172 | confirm_button = page.get_by_text("确认发布") 173 | if await confirm_button.count() > 0: 174 | await confirm_button.click() 175 | 176 | # 等待页面跳转,确认发布成功 177 | await page.wait_for_url( 178 | "https://cp.kuaishou.com/article/manage/video?status=2&from=publish", 179 | timeout=5000, 180 | ) 181 | kuaishou_logger.success("视频发布成功") 182 | break 183 | except Exception as e: 184 | kuaishou_logger.info(f"视频正在发布中... 错误: {e}") 185 | await page.screenshot(full_page=True) 186 | await asyncio.sleep(1) 187 | 188 | await context.storage_state(path=self.account_file) # 保存cookie 189 | kuaishou_logger.info('cookie更新完毕!') 190 | await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 191 | # 关闭浏览器上下文和浏览器实例 192 | await context.close() 193 | await browser.close() 194 | 195 | async def main(self): 196 | async with async_playwright() as playwright: 197 | await self.upload(playwright) 198 | 199 | async def set_schedule_time(self, page, publish_date): 200 | kuaishou_logger.info("click schedule") 201 | publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M:%S") 202 | await page.locator("label:text('发布时间')").locator('xpath=following-sibling::div').locator( 203 | '.ant-radio-input').nth(1).click() 204 | await asyncio.sleep(1) 205 | 206 | await page.locator('div.ant-picker-input input[placeholder="选择日期时间"]').click() 207 | await asyncio.sleep(1) 208 | 209 | await page.keyboard.press("Control+KeyA") 210 | await page.keyboard.type(str(publish_date_hour)) 211 | await page.keyboard.press("Enter") 212 | await asyncio.sleep(1) 213 | -------------------------------------------------------------------------------- /uploader/tencent_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "tencent_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/tencent_uploader/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from playwright.async_api import Playwright, async_playwright 5 | import os 6 | import asyncio 7 | 8 | from conf import LOCAL_CHROME_PATH 9 | from utils.base_social_media import set_init_script 10 | from utils.files_times import get_absolute_path 11 | from utils.log import tencent_logger 12 | 13 | 14 | def format_str_for_short_title(origin_title: str) -> str: 15 | # 定义允许的特殊字符 16 | allowed_special_chars = "《》“”:+?%°" 17 | 18 | # 移除不允许的特殊字符 19 | filtered_chars = [char if char.isalnum() or char in allowed_special_chars else ' ' if char == ',' else '' for 20 | char in origin_title] 21 | formatted_string = ''.join(filtered_chars) 22 | 23 | # 调整字符串长度 24 | if len(formatted_string) > 16: 25 | # 截断字符串 26 | formatted_string = formatted_string[:16] 27 | elif len(formatted_string) < 6: 28 | # 使用空格来填充字符串 29 | formatted_string += ' ' * (6 - len(formatted_string)) 30 | 31 | return formatted_string 32 | 33 | 34 | async def cookie_auth(account_file): 35 | async with async_playwright() as playwright: 36 | browser = await playwright.chromium.launch(headless=True) 37 | context = await browser.new_context(storage_state=account_file) 38 | context = await set_init_script(context) 39 | # 创建一个新的页面 40 | page = await context.new_page() 41 | # 访问指定的 URL 42 | await page.goto("https://channels.weixin.qq.com/platform/post/create") 43 | try: 44 | await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒 45 | tencent_logger.error("[+] 等待5秒 cookie 失效") 46 | return False 47 | except: 48 | tencent_logger.success("[+] cookie 有效") 49 | return True 50 | 51 | 52 | async def get_tencent_cookie(account_file): 53 | async with async_playwright() as playwright: 54 | options = { 55 | 'args': [ 56 | '--lang en-GB' 57 | ], 58 | 'headless': False, # Set headless option here 59 | } 60 | # Make sure to run headed. 61 | browser = await playwright.chromium.launch(**options) 62 | # Setup context however you like. 63 | context = await browser.new_context() # Pass any options 64 | # Pause the page, and start recording manually. 65 | context = await set_init_script(context) 66 | page = await context.new_page() 67 | await page.goto("https://channels.weixin.qq.com") 68 | await page.pause() 69 | # 点击调试器的继续,保存cookie 70 | await context.storage_state(path=account_file) 71 | 72 | 73 | async def weixin_setup(account_file, handle=False): 74 | account_file = get_absolute_path(account_file, "tencent_uploader") 75 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 76 | if not handle: 77 | # Todo alert message 78 | return False 79 | tencent_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') 80 | await get_tencent_cookie(account_file) 81 | return True 82 | 83 | 84 | class TencentVideo(object): 85 | def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None): 86 | self.title = title # 视频标题 87 | self.file_path = file_path 88 | self.tags = tags 89 | self.publish_date = publish_date 90 | self.account_file = account_file 91 | self.category = category 92 | self.local_executable_path = LOCAL_CHROME_PATH 93 | 94 | async def set_schedule_time_tencent(self, page, publish_date): 95 | label_element = page.locator("label").filter(has_text="定时").nth(1) 96 | await label_element.click() 97 | 98 | await page.click('input[placeholder="请选择发表时间"]') 99 | 100 | str_month = str(publish_date.month) if publish_date.month > 9 else "0" + str(publish_date.month) 101 | current_month = str_month + "月" 102 | # 获取当前的月份 103 | page_month = await page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")') 104 | 105 | # 检查当前月份是否与目标月份相同 106 | if page_month != current_month: 107 | await page.click('button.weui-desktop-btn__icon__right') 108 | 109 | # 获取页面元素 110 | elements = await page.query_selector_all('table.weui-desktop-picker__table a') 111 | 112 | # 遍历元素并点击匹配的元素 113 | for element in elements: 114 | if 'weui-desktop-picker__disabled' in await element.evaluate('el => el.className'): 115 | continue 116 | text = await element.inner_text() 117 | if text.strip() == str(publish_date.day): 118 | await element.click() 119 | break 120 | 121 | # 输入小时部分(假设选择11小时) 122 | await page.click('input[placeholder="请选择时间"]') 123 | await page.keyboard.press("Control+KeyA") 124 | await page.keyboard.type(str(publish_date.hour)) 125 | 126 | # 选择标题栏(令定时时间生效) 127 | await page.locator("div.input-editor").click() 128 | 129 | async def handle_upload_error(self, page): 130 | tencent_logger.info("视频出错了,重新上传中") 131 | await page.locator('div.media-status-content div.tag-inner:has-text("删除")').click() 132 | await page.get_by_role('button', name="删除", exact=True).click() 133 | file_input = page.locator('input[type="file"]') 134 | await file_input.set_input_files(self.file_path) 135 | 136 | async def upload(self, playwright: Playwright) -> None: 137 | # 使用 Chromium (这里使用系统内浏览器,用chromium 会造成h264错误 138 | browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path) 139 | # 创建一个浏览器上下文,使用指定的 cookie 文件 140 | context = await browser.new_context(storage_state=f"{self.account_file}") 141 | context = await set_init_script(context) 142 | 143 | # 创建一个新的页面 144 | page = await context.new_page() 145 | # 访问指定的 URL 146 | await page.goto("https://channels.weixin.qq.com/platform/post/create") 147 | tencent_logger.info(f'[+]正在上传-------{self.title}.mp4') 148 | # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 149 | await page.wait_for_url("https://channels.weixin.qq.com/platform/post/create") 150 | # await page.wait_for_selector('input[type="file"]', timeout=10000) 151 | file_input = page.locator('input[type="file"]') 152 | await file_input.set_input_files(self.file_path) 153 | # 填充标题和话题 154 | await self.add_title_tags(page) 155 | # 添加商品 156 | # await self.add_product(page) 157 | # 合集功能 158 | await self.add_collection(page) 159 | # 原创选择 160 | await self.add_original(page) 161 | # 检测上传状态 162 | await self.detect_upload_status(page) 163 | if self.publish_date != 0: 164 | await self.set_schedule_time_tencent(page, self.publish_date) 165 | # 添加短标题 166 | await self.add_short_title(page) 167 | 168 | await self.click_publish(page) 169 | 170 | await context.storage_state(path=f"{self.account_file}") # 保存cookie 171 | tencent_logger.success(' [-]cookie更新完毕!') 172 | await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 173 | # 关闭浏览器上下文和浏览器实例 174 | await context.close() 175 | await browser.close() 176 | 177 | async def add_short_title(self, page): 178 | short_title_element = page.get_by_text("短标题", exact=True).locator("..").locator( 179 | "xpath=following-sibling::div").locator( 180 | 'span input[type="text"]') 181 | if await short_title_element.count(): 182 | short_title = format_str_for_short_title(self.title) 183 | await short_title_element.fill(short_title) 184 | 185 | async def click_publish(self, page): 186 | while True: 187 | try: 188 | publish_buttion = page.locator('div.form-btns button:has-text("发表")') 189 | if await publish_buttion.count(): 190 | await publish_buttion.click() 191 | await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=5000) 192 | tencent_logger.success(" [-]视频发布成功") 193 | break 194 | except Exception as e: 195 | current_url = page.url 196 | if "https://channels.weixin.qq.com/platform/post/list" in current_url: 197 | tencent_logger.success(" [-]视频发布成功") 198 | break 199 | else: 200 | tencent_logger.exception(f" [-] Exception: {e}") 201 | tencent_logger.info(" [-] 视频正在发布中...") 202 | await asyncio.sleep(0.5) 203 | 204 | async def detect_upload_status(self, page): 205 | while True: 206 | # 匹配删除按钮,代表视频上传完毕,如果不存在,代表视频正在上传,则等待 207 | try: 208 | # 匹配删除按钮,代表视频上传完毕 209 | if "weui-desktop-btn_disabled" not in await page.get_by_role("button", name="发表").get_attribute( 210 | 'class'): 211 | tencent_logger.info(" [-]视频上传完毕") 212 | break 213 | else: 214 | tencent_logger.info(" [-] 正在上传视频中...") 215 | await asyncio.sleep(2) 216 | # 出错了视频出错 217 | if await page.locator('div.status-msg.error').count() and await page.locator( 218 | 'div.media-status-content div.tag-inner:has-text("删除")').count(): 219 | tencent_logger.error(" [-] 发现上传出错了...准备重试") 220 | await self.handle_upload_error(page) 221 | except: 222 | tencent_logger.info(" [-] 正在上传视频中...") 223 | await asyncio.sleep(2) 224 | 225 | async def add_title_tags(self, page): 226 | await page.locator("div.input-editor").click() 227 | await page.keyboard.type(self.title) 228 | await page.keyboard.press("Enter") 229 | for index, tag in enumerate(self.tags, start=1): 230 | await page.keyboard.type("#" + tag) 231 | await page.keyboard.press("Space") 232 | tencent_logger.info(f"成功添加hashtag: {len(self.tags)}") 233 | 234 | async def add_collection(self, page): 235 | collection_elements = page.get_by_text("添加到合集").locator("xpath=following-sibling::div").locator( 236 | '.option-list-wrap > div') 237 | if await collection_elements.count() > 1: 238 | await page.get_by_text("添加到合集").locator("xpath=following-sibling::div").click() 239 | await collection_elements.first.click() 240 | 241 | async def add_original(self, page): 242 | if await page.get_by_label("视频为原创").count(): 243 | await page.get_by_label("视频为原创").check() 244 | # 检查 "我已阅读并同意 《视频号原创声明使用条款》" 元素是否存在 245 | label_locator = await page.locator('label:has-text("我已阅读并同意 《视频号原创声明使用条款》")').is_visible() 246 | if label_locator: 247 | await page.get_by_label("我已阅读并同意 《视频号原创声明使用条款》").check() 248 | await page.get_by_role("button", name="声明原创").click() 249 | # 2023年11月20日 wechat更新: 可能新账号或者改版账号,出现新的选择页面 250 | if await page.locator('div.label span:has-text("声明原创")').count() and self.category: 251 | # 因处罚无法勾选原创,故先判断是否可用 252 | if not await page.locator('div.declare-original-checkbox input.ant-checkbox-input').is_disabled(): 253 | await page.locator('div.declare-original-checkbox input.ant-checkbox-input').click() 254 | if not await page.locator( 255 | 'div.declare-original-dialog label.ant-checkbox-wrapper.ant-checkbox-wrapper-checked:visible').count(): 256 | await page.locator('div.declare-original-dialog input.ant-checkbox-input:visible').click() 257 | if await page.locator('div.original-type-form > div.form-label:has-text("原创类型"):visible').count(): 258 | await page.locator('div.form-content:visible').click() # 下拉菜单 259 | await page.locator( 260 | f'div.form-content:visible ul.weui-desktop-dropdown__list li.weui-desktop-dropdown__list-ele:has-text("{self.category}")').first.click() 261 | await page.wait_for_timeout(1000) 262 | if await page.locator('button:has-text("声明原创"):visible').count(): 263 | await page.locator('button:has-text("声明原创"):visible').click() 264 | 265 | async def main(self): 266 | async with async_playwright() as playwright: 267 | await self.upload(playwright) 268 | -------------------------------------------------------------------------------- /uploader/tk_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "tk_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/tk_uploader/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | from datetime import datetime 4 | 5 | from playwright.async_api import Playwright, async_playwright 6 | import os 7 | import asyncio 8 | from uploader.tk_uploader.tk_config import Tk_Locator 9 | from utils.base_social_media import set_init_script 10 | from utils.files_times import get_absolute_path 11 | from utils.log import tiktok_logger 12 | 13 | 14 | async def cookie_auth(account_file): 15 | async with async_playwright() as playwright: 16 | browser = await playwright.firefox.launch(headless=True) 17 | context = await browser.new_context(storage_state=account_file) 18 | context = await set_init_script(context) 19 | # 创建一个新的页面 20 | page = await context.new_page() 21 | # 访问指定的 URL 22 | await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en") 23 | await page.wait_for_load_state('networkidle') 24 | try: 25 | # 选择所有的 select 元素 26 | select_elements = await page.query_selector_all('select') 27 | for element in select_elements: 28 | class_name = await element.get_attribute('class') 29 | # 使用正则表达式匹配特定模式的 class 名称 30 | if re.match(r'tiktok-.*-SelectFormContainer.*', class_name): 31 | tiktok_logger.error("[+] cookie expired") 32 | return False 33 | tiktok_logger.success("[+] cookie valid") 34 | return True 35 | except: 36 | tiktok_logger.success("[+] cookie valid") 37 | return True 38 | 39 | 40 | async def tiktok_setup(account_file, handle=False): 41 | account_file = get_absolute_path(account_file, "tk_uploader") 42 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 43 | if not handle: 44 | return False 45 | 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') 46 | await get_tiktok_cookie(account_file) 47 | return True 48 | 49 | 50 | async def get_tiktok_cookie(account_file): 51 | async with async_playwright() as playwright: 52 | options = { 53 | 'args': [ 54 | '--lang en-GB', 55 | ], 56 | 'headless': False, # Set headless option here 57 | } 58 | # Make sure to run headed. 59 | browser = await playwright.firefox.launch(**options) 60 | # Setup context however you like. 61 | context = await browser.new_context() # Pass any options 62 | context = await set_init_script(context) 63 | # Pause the page, and start recording manually. 64 | page = await context.new_page() 65 | await page.goto("https://www.tiktok.com/login?lang=en") 66 | await page.pause() 67 | # 点击调试器的继续,保存cookie 68 | await context.storage_state(path=account_file) 69 | 70 | 71 | class TiktokVideo(object): 72 | def __init__(self, title, file_path, tags, publish_date, account_file): 73 | self.title = title 74 | self.file_path = file_path 75 | self.tags = tags 76 | self.publish_date = publish_date 77 | self.account_file = account_file 78 | self.locator_base = None 79 | 80 | 81 | async def set_schedule_time(self, page, publish_date): 82 | schedule_input_element = self.locator_base.get_by_label('Schedule') 83 | await schedule_input_element.wait_for(state='visible') # 确保按钮可见 84 | 85 | await schedule_input_element.click() 86 | scheduled_picker = self.locator_base.locator('div.scheduled-picker') 87 | await scheduled_picker.locator('div.TUXInputBox').nth(1).click() 88 | 89 | calendar_month = await self.locator_base.locator('div.calendar-wrapper span.month-title').inner_text() 90 | 91 | n_calendar_month = datetime.strptime(calendar_month, '%B').month 92 | 93 | schedule_month = publish_date.month 94 | 95 | if n_calendar_month != schedule_month: 96 | if n_calendar_month < schedule_month: 97 | arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1) 98 | else: 99 | arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0) 100 | await arrow.click() 101 | 102 | # day set 103 | valid_days_locator = self.locator_base.locator( 104 | 'div.calendar-wrapper span.day.valid') 105 | valid_days = await valid_days_locator.count() 106 | for i in range(valid_days): 107 | day_element = valid_days_locator.nth(i) 108 | text = await day_element.inner_text() 109 | if text.strip() == str(publish_date.day): 110 | await day_element.click() 111 | break 112 | # time set 113 | await scheduled_picker.locator('div.TUXInputBox').nth(0).click() 114 | 115 | hour_str = publish_date.strftime("%H") 116 | correct_minute = int(publish_date.minute / 5) 117 | minute_str = f"{correct_minute:02d}" 118 | 119 | hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')" 120 | minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')" 121 | 122 | # pick hour first 123 | await self.locator_base.locator(hour_selector).click() 124 | # click time button again 125 | # 等待某个特定的元素出现或状态变化,表明UI已更新 126 | await page.wait_for_timeout(1000) # 等待500毫秒 127 | await scheduled_picker.locator('div.TUXInputBox').nth(0).click() 128 | # pick minutes after 129 | await self.locator_base.locator(minute_selector).click() 130 | 131 | # click title to remove the focus. 132 | await self.locator_base.locator("h1:has-text('Upload video')").click() 133 | 134 | async def handle_upload_error(self, page): 135 | tiktok_logger.info("video upload error retrying.") 136 | select_file_button = self.locator_base.locator('button[aria-label="Select file"]') 137 | async with page.expect_file_chooser() as fc_info: 138 | await select_file_button.click() 139 | file_chooser = await fc_info.value 140 | await file_chooser.set_files(self.file_path) 141 | 142 | async def upload(self, playwright: Playwright) -> None: 143 | browser = await playwright.firefox.launch(headless=False) 144 | context = await browser.new_context(storage_state=f"{self.account_file}") 145 | context = await set_init_script(context) 146 | page = await context.new_page() 147 | 148 | await page.goto("https://www.tiktok.com/creator-center/upload") 149 | tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4') 150 | 151 | await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000) 152 | 153 | try: 154 | await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000) 155 | tiktok_logger.info("Either iframe or div appeared.") 156 | except Exception as e: 157 | tiktok_logger.error("Neither iframe nor div appeared within the timeout.") 158 | 159 | await self.choose_base_locator(page) 160 | 161 | upload_button = self.locator_base.locator( 162 | 'button:has-text("Select video"):visible') 163 | await upload_button.wait_for(state='visible') # 确保按钮可见 164 | 165 | async with page.expect_file_chooser() as fc_info: 166 | await upload_button.click() 167 | file_chooser = await fc_info.value 168 | await file_chooser.set_files(self.file_path) 169 | 170 | await self.add_title_tags(page) 171 | # detact upload status 172 | await self.detect_upload_status(page) 173 | if self.publish_date != 0: 174 | await self.set_schedule_time(page, self.publish_date) 175 | 176 | await self.click_publish(page) 177 | 178 | await context.storage_state(path=f"{self.account_file}") # save cookie 179 | tiktok_logger.info(' [-] update cookie!') 180 | await asyncio.sleep(2) # close delay for look the video status 181 | # close all 182 | await context.close() 183 | await browser.close() 184 | 185 | async def add_title_tags(self, page): 186 | 187 | editor_locator = self.locator_base.locator('div.public-DraftEditor-content') 188 | await editor_locator.click() 189 | 190 | await page.keyboard.press("End") 191 | 192 | await page.keyboard.press("Control+A") 193 | 194 | await page.keyboard.press("Delete") 195 | 196 | await page.keyboard.press("End") 197 | 198 | await page.wait_for_timeout(1000) # 等待1秒 199 | 200 | await page.keyboard.insert_text(self.title) 201 | await page.wait_for_timeout(1000) # 等待1秒 202 | await page.keyboard.press("End") 203 | 204 | await page.keyboard.press("Enter") 205 | 206 | # tag part 207 | for index, tag in enumerate(self.tags, start=1): 208 | tiktok_logger.info("Setting the %s tag" % index) 209 | await page.keyboard.press("End") 210 | await page.wait_for_timeout(1000) # 等待1秒 211 | await page.keyboard.insert_text("#" + tag + " ") 212 | await page.keyboard.press("Space") 213 | await page.wait_for_timeout(1000) # 等待1秒 214 | 215 | await page.keyboard.press("Backspace") 216 | await page.keyboard.press("End") 217 | 218 | async def click_publish(self, page): 219 | success_flag_div = '#\\:r9\\:' 220 | while True: 221 | try: 222 | publish_button = self.locator_base.locator('div.btn-post') 223 | if await publish_button.count(): 224 | await publish_button.click() 225 | 226 | await self.locator_base.locator(success_flag_div).wait_for(state="visible", timeout=3000) 227 | tiktok_logger.success(" [-] video published success") 228 | break 229 | except Exception as e: 230 | if await self.locator_base.locator(success_flag_div).count(): 231 | tiktok_logger.success(" [-]video published success") 232 | break 233 | else: 234 | tiktok_logger.exception(f" [-] Exception: {e}") 235 | tiktok_logger.info(" [-] video publishing") 236 | await page.screenshot(full_page=True) 237 | await asyncio.sleep(0.5) 238 | 239 | async def detect_upload_status(self, page): 240 | while True: 241 | try: 242 | if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None: 243 | tiktok_logger.info(" [-]video uploaded.") 244 | break 245 | else: 246 | tiktok_logger.info(" [-] video uploading...") 247 | await asyncio.sleep(2) 248 | if await self.locator_base.locator('button[aria-label="Select file"]').count(): 249 | tiktok_logger.info(" [-] found some error while uploading now retry...") 250 | await self.handle_upload_error(page) 251 | except: 252 | tiktok_logger.info(" [-] video uploading...") 253 | await asyncio.sleep(2) 254 | 255 | async def choose_base_locator(self, page): 256 | # await page.wait_for_selector('div.upload-container') 257 | if await page.locator('iframe[data-tt="Upload_index_iframe"]').count(): 258 | self.locator_base = self.locator_base 259 | else: 260 | self.locator_base = page.locator(Tk_Locator.default) 261 | 262 | async def main(self): 263 | async with async_playwright() as playwright: 264 | await self.upload(playwright) 265 | 266 | -------------------------------------------------------------------------------- /uploader/tk_uploader/main_chrome.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | from datetime import datetime 4 | 5 | from playwright.async_api import Playwright, async_playwright 6 | import os 7 | import asyncio 8 | 9 | from conf import LOCAL_CHROME_PATH 10 | from uploader.tk_uploader.tk_config import Tk_Locator 11 | from utils.base_social_media import set_init_script 12 | from utils.files_times import get_absolute_path 13 | from utils.log import tiktok_logger 14 | 15 | 16 | async def cookie_auth(account_file): 17 | async with async_playwright() as playwright: 18 | browser = await playwright.chromium.launch(headless=True) 19 | context = await browser.new_context(storage_state=account_file) 20 | context = await set_init_script(context) 21 | # 创建一个新的页面 22 | page = await context.new_page() 23 | # 访问指定的 URL 24 | await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en") 25 | await page.wait_for_load_state('networkidle') 26 | try: 27 | # 选择所有的 select 元素 28 | select_elements = await page.query_selector_all('select') 29 | for element in select_elements: 30 | class_name = await element.get_attribute('class') 31 | # 使用正则表达式匹配特定模式的 class 名称 32 | if re.match(r'tiktok-.*-SelectFormContainer.*', class_name): 33 | tiktok_logger.error("[+] cookie expired") 34 | return False 35 | tiktok_logger.success("[+] cookie valid") 36 | return True 37 | except: 38 | tiktok_logger.success("[+] cookie valid") 39 | return True 40 | 41 | 42 | async def tiktok_setup(account_file, handle=False): 43 | account_file = get_absolute_path(account_file, "tk_uploader") 44 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 45 | if not handle: 46 | return False 47 | 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') 48 | await get_tiktok_cookie(account_file) 49 | return True 50 | 51 | 52 | async def get_tiktok_cookie(account_file): 53 | async with async_playwright() as playwright: 54 | options = { 55 | 'args': [ 56 | '--lang en-GB', 57 | ], 58 | 'headless': False, # Set headless option here 59 | } 60 | # Make sure to run headed. 61 | browser = await playwright.chromium.launch(**options) 62 | # Setup context however you like. 63 | context = await browser.new_context() # Pass any options 64 | context = await set_init_script(context) 65 | # Pause the page, and start recording manually. 66 | page = await context.new_page() 67 | await page.goto("https://www.tiktok.com/login?lang=en") 68 | await page.pause() 69 | # 点击调试器的继续,保存cookie 70 | await context.storage_state(path=account_file) 71 | 72 | 73 | class TiktokVideo(object): 74 | def __init__(self, title, file_path, tags, publish_date, account_file, thumbnail_path=None): 75 | self.title = title 76 | self.file_path = file_path 77 | self.tags = tags 78 | self.publish_date = publish_date 79 | self.thumbnail_path = thumbnail_path 80 | self.account_file = account_file 81 | self.local_executable_path = LOCAL_CHROME_PATH 82 | self.locator_base = None 83 | 84 | async def set_schedule_time(self, page, publish_date): 85 | schedule_input_element = self.locator_base.get_by_label('Schedule') 86 | await schedule_input_element.wait_for(state='visible') # 确保按钮可见 87 | 88 | await schedule_input_element.click(force=True) 89 | if await self.locator_base.locator('div.TUXButton-content >> text=Allow').count(): 90 | await self.locator_base.locator('div.TUXButton-content >> text=Allow').click() 91 | 92 | scheduled_picker = self.locator_base.locator('div.scheduled-picker') 93 | await scheduled_picker.locator('div.TUXInputBox').nth(1).click() 94 | 95 | calendar_month = await self.locator_base.locator( 96 | 'div.calendar-wrapper span.month-title').inner_text() 97 | 98 | n_calendar_month = datetime.strptime(calendar_month, '%B').month 99 | 100 | schedule_month = publish_date.month 101 | 102 | if n_calendar_month != schedule_month: 103 | if n_calendar_month < schedule_month: 104 | arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1) 105 | else: 106 | arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0) 107 | await arrow.click() 108 | 109 | # day set 110 | valid_days_locator = self.locator_base.locator( 111 | 'div.calendar-wrapper span.day.valid') 112 | valid_days = await valid_days_locator.count() 113 | for i in range(valid_days): 114 | day_element = valid_days_locator.nth(i) 115 | text = await day_element.inner_text() 116 | if text.strip() == str(publish_date.day): 117 | await day_element.click() 118 | break 119 | # time set 120 | await scheduled_picker.locator('div.TUXInputBox').nth(0).click() 121 | 122 | hour_str = publish_date.strftime("%H") 123 | correct_minute = int(publish_date.minute / 5) 124 | minute_str = f"{correct_minute:02d}" 125 | 126 | hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')" 127 | minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')" 128 | 129 | # pick hour first 130 | await page.wait_for_timeout(1000) # 等待500毫秒 131 | await self.locator_base.locator(hour_selector).click() 132 | # click time button again 133 | await page.wait_for_timeout(1000) # 等待500毫秒 134 | # pick minutes after 135 | await self.locator_base.locator(minute_selector).click() 136 | 137 | # click title to remove the focus. 138 | # await self.locator_base.locator("h1:has-text('Upload video')").click() 139 | 140 | async def handle_upload_error(self, page): 141 | tiktok_logger.info("video upload error retrying.") 142 | select_file_button = self.locator_base.locator('button[aria-label="Select file"]') 143 | async with page.expect_file_chooser() as fc_info: 144 | await select_file_button.click() 145 | file_chooser = await fc_info.value 146 | await file_chooser.set_files(self.file_path) 147 | 148 | async def upload(self, playwright: Playwright) -> None: 149 | browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path) 150 | context = await browser.new_context(storage_state=f"{self.account_file}") 151 | # context = await set_init_script(context) 152 | page = await context.new_page() 153 | 154 | # change language to eng first 155 | await self.change_language(page) 156 | await page.goto("https://www.tiktok.com/tiktokstudio/upload") 157 | tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4') 158 | 159 | await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000) 160 | 161 | try: 162 | await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000) 163 | tiktok_logger.info("Either iframe or div appeared.") 164 | except Exception as e: 165 | tiktok_logger.error("Neither iframe nor div appeared within the timeout.") 166 | 167 | await self.choose_base_locator(page) 168 | 169 | upload_button = self.locator_base.locator( 170 | 'button:has-text("Select video"):visible') 171 | await upload_button.wait_for(state='visible') # 确保按钮可见 172 | 173 | async with page.expect_file_chooser() as fc_info: 174 | await upload_button.click() 175 | file_chooser = await fc_info.value 176 | await file_chooser.set_files(self.file_path) 177 | 178 | await self.add_title_tags(page) 179 | # detect upload status 180 | await self.detect_upload_status(page) 181 | if self.thumbnail_path: 182 | tiktok_logger.info(f'[+] Uploading thumbnail file {self.title}.png') 183 | await self.upload_thumbnails(page) 184 | 185 | if self.publish_date != 0: 186 | await self.set_schedule_time(page, self.publish_date) 187 | 188 | await self.click_publish(page) 189 | tiktok_logger.success(f"video_id: {await self.get_last_video_id(page)}") 190 | 191 | await context.storage_state(path=f"{self.account_file}") # save cookie 192 | tiktok_logger.info(' [-] update cookie!') 193 | await asyncio.sleep(2) # close delay for look the video status 194 | # close all 195 | await context.close() 196 | await browser.close() 197 | 198 | async def add_title_tags(self, page): 199 | 200 | editor_locator = self.locator_base.locator('div.public-DraftEditor-content') 201 | await editor_locator.click() 202 | 203 | await page.keyboard.press("End") 204 | 205 | await page.keyboard.press("Control+A") 206 | 207 | await page.keyboard.press("Delete") 208 | 209 | await page.keyboard.press("End") 210 | 211 | await page.wait_for_timeout(1000) # 等待1秒 212 | 213 | await page.keyboard.insert_text(self.title) 214 | await page.wait_for_timeout(1000) # 等待1秒 215 | await page.keyboard.press("End") 216 | 217 | await page.keyboard.press("Enter") 218 | 219 | # tag part 220 | for index, tag in enumerate(self.tags, start=1): 221 | tiktok_logger.info("Setting the %s tag" % index) 222 | await page.keyboard.press("End") 223 | await page.wait_for_timeout(1000) # 等待1秒 224 | await page.keyboard.insert_text("#" + tag + " ") 225 | await page.keyboard.press("Space") 226 | await page.wait_for_timeout(1000) # 等待1秒 227 | 228 | await page.keyboard.press("Backspace") 229 | await page.keyboard.press("End") 230 | 231 | async def upload_thumbnails(self, page): 232 | await self.locator_base.locator(".cover-container").click() 233 | await self.locator_base.locator(".cover-edit-container >> text=Upload cover").click() 234 | async with page.expect_file_chooser() as fc_info: 235 | await self.locator_base.locator(".upload-image-upload-area").click() 236 | file_chooser = await fc_info.value 237 | await file_chooser.set_files(self.thumbnail_path) 238 | await self.locator_base.locator('div.cover-edit-panel:not(.hide-panel)').get_by_role( 239 | "button", name="Confirm").click() 240 | await page.wait_for_timeout(3000) # wait 3s, fix it later 241 | 242 | async def change_language(self, page): 243 | # set the language to english 244 | await page.goto("https://www.tiktok.com") 245 | await page.wait_for_load_state('domcontentloaded') 246 | await page.wait_for_selector('[data-e2e="nav-more-menu"]') 247 | # 已经设置为英文, 省略这个步骤 248 | if await page.locator('[data-e2e="nav-more-menu"]').text_content() == "More": 249 | return 250 | 251 | await page.locator('[data-e2e="nav-more-menu"]').click() 252 | await page.locator('[data-e2e="language-select"]').click() 253 | await page.locator('#creator-tools-selection-menu-header >> text=English (US)').click() 254 | 255 | async def click_publish(self, page): 256 | success_flag_div = 'div.common-modal-confirm-modal' 257 | while True: 258 | try: 259 | publish_button = self.locator_base.locator('div.button-group button').nth(0) 260 | if await publish_button.count(): 261 | await publish_button.click() 262 | 263 | await page.wait_for_url("https://www.tiktok.com/tiktokstudio/content", timeout=3000) 264 | tiktok_logger.success(" [-] video published success") 265 | break 266 | except Exception as e: 267 | tiktok_logger.exception(f" [-] Exception: {e}") 268 | tiktok_logger.info(" [-] video publishing") 269 | await asyncio.sleep(0.5) 270 | 271 | async def get_last_video_id(self, page): 272 | await page.wait_for_selector('div[data-tt="components_PostTable_Container"]') 273 | video_list_locator = self.locator_base.locator('div[data-tt="components_PostTable_Container"] div[data-tt="components_PostInfoCell_Container"] a') 274 | if await video_list_locator.count(): 275 | first_video_obj = await video_list_locator.nth(0).get_attribute('href') 276 | video_id = re.search(r'video/(\d+)', first_video_obj).group(1) if first_video_obj else None 277 | return video_id 278 | 279 | 280 | async def detect_upload_status(self, page): 281 | while True: 282 | try: 283 | # if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None: 284 | if await self.locator_base.locator( 285 | 'div.button-group > button >> text=Post').get_attribute("disabled") is None: 286 | tiktok_logger.info(" [-]video uploaded.") 287 | break 288 | else: 289 | tiktok_logger.info(" [-] video uploading...") 290 | await asyncio.sleep(2) 291 | if await self.locator_base.locator( 292 | 'button[aria-label="Select file"]').count(): 293 | tiktok_logger.info(" [-] found some error while uploading now retry...") 294 | await self.handle_upload_error(page) 295 | except: 296 | tiktok_logger.info(" [-] video uploading...") 297 | await asyncio.sleep(2) 298 | 299 | async def choose_base_locator(self, page): 300 | # await page.wait_for_selector('div.upload-container') 301 | if await page.locator('iframe[data-tt="Upload_index_iframe"]').count(): 302 | self.locator_base = page.frame_locator(Tk_Locator.tk_iframe) 303 | else: 304 | self.locator_base = page.locator(Tk_Locator.default) 305 | 306 | async def main(self): 307 | async with async_playwright() as playwright: 308 | await self.upload(playwright) 309 | -------------------------------------------------------------------------------- /uploader/tk_uploader/tk_config.py: -------------------------------------------------------------------------------- 1 | 2 | class Tk_Locator(object): 3 | tk_iframe = '[data-tt="Upload_index_iframe"]' 4 | default = 'body' 5 | -------------------------------------------------------------------------------- /uploader/xhs_uploader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/uploader/xhs_uploader/__init__.py -------------------------------------------------------------------------------- /uploader/xhs_uploader/accounts.ini: -------------------------------------------------------------------------------- 1 | [account1] 2 | cookies = changeme 3 | -------------------------------------------------------------------------------- /uploader/xhs_uploader/main.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import json 3 | import pathlib 4 | from time import sleep 5 | 6 | import requests 7 | from playwright.sync_api import sync_playwright 8 | 9 | from conf import BASE_DIR, XHS_SERVER 10 | 11 | config = configparser.RawConfigParser() 12 | config.read('accounts.ini') 13 | 14 | 15 | def sign_local(uri, data=None, a1="", web_session=""): 16 | for _ in range(10): 17 | try: 18 | with sync_playwright() as playwright: 19 | stealth_js_path = pathlib.Path(BASE_DIR / "utils/stealth.min.js") 20 | chromium = playwright.chromium 21 | 22 | # 如果一直失败可尝试设置成 False 让其打开浏览器,适当添加 sleep 可查看浏览器状态 23 | browser = chromium.launch(headless=True) 24 | 25 | browser_context = browser.new_context() 26 | browser_context.add_init_script(path=stealth_js_path) 27 | context_page = browser_context.new_page() 28 | context_page.goto("https://www.xiaohongshu.com") 29 | browser_context.add_cookies([ 30 | {'name': 'a1', 'value': a1, 'domain': ".xiaohongshu.com", 'path': "/"}] 31 | ) 32 | context_page.reload() 33 | # 这个地方设置完浏览器 cookie 之后,如果这儿不 sleep 一下签名获取就失败了,如果经常失败请设置长一点试试 34 | sleep(2) 35 | encrypt_params = context_page.evaluate("([url, data]) => window._webmsxyw(url, data)", [uri, data]) 36 | return { 37 | "x-s": encrypt_params["X-s"], 38 | "x-t": str(encrypt_params["X-t"]) 39 | } 40 | except Exception: 41 | # 这儿有时会出现 window._webmsxyw is not a function 或未知跳转错误,因此加一个失败重试趴 42 | pass 43 | raise Exception("重试了这么多次还是无法签名成功,寄寄寄") 44 | 45 | 46 | def sign(uri, data=None, a1="", web_session=""): 47 | # 填写自己的 flask 签名服务端口地址 48 | res = requests.post(f"{XHS_SERVER}/sign", 49 | json={"uri": uri, "data": data, "a1": a1, "web_session": web_session}) 50 | signs = res.json() 51 | return { 52 | "x-s": signs["x-s"], 53 | "x-t": signs["x-t"] 54 | } 55 | 56 | 57 | def beauty_print(data: dict): 58 | print(json.dumps(data, ensure_ascii=False, indent=2)) 59 | -------------------------------------------------------------------------------- /uploader/xhs_uploader/xhs_login_qrcode.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import qrcode 4 | from time import sleep 5 | 6 | from xhs import XhsClient 7 | 8 | from uploader.xhs_uploader.main import sign 9 | 10 | # pip install qrcode 11 | if __name__ == '__main__': 12 | xhs_client = XhsClient(sign=sign, timeout=60) 13 | print(datetime.datetime.now()) 14 | qr_res = xhs_client.get_qrcode() 15 | qr_id = qr_res["qr_id"] 16 | qr_code = qr_res["code"] 17 | 18 | qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L, 19 | box_size=50, 20 | border=1) 21 | qr.add_data(qr_res["url"]) 22 | qr.make() 23 | qr.print_ascii() 24 | 25 | while True: 26 | check_qrcode = xhs_client.check_qrcode(qr_id, qr_code) 27 | print(check_qrcode) 28 | sleep(1) 29 | if check_qrcode["code_status"] == 2: 30 | print(json.dumps(check_qrcode["login_info"], indent=4)) 31 | print("当前 cookie:" + xhs_client.cookie) 32 | break 33 | 34 | print(json.dumps(xhs_client.get_self_info(), indent=4)) -------------------------------------------------------------------------------- /uploader/xiaohongshu_uploader/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conf import BASE_DIR 4 | 5 | Path(BASE_DIR / "cookies" / "xiaohongshu_uploader").mkdir(exist_ok=True) -------------------------------------------------------------------------------- /uploader/xiaohongshu_uploader/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | from playwright.async_api import Playwright, async_playwright, Page 5 | import os 6 | import asyncio 7 | 8 | from conf import LOCAL_CHROME_PATH 9 | from utils.base_social_media import set_init_script 10 | from utils.log import xiaohongshu_logger 11 | 12 | 13 | async def cookie_auth(account_file): 14 | async with async_playwright() as playwright: 15 | browser = await playwright.chromium.launch(headless=True) 16 | context = await browser.new_context(storage_state=account_file) 17 | context = await set_init_script(context) 18 | # 创建一个新的页面 19 | page = await context.new_page() 20 | # 访问指定的 URL 21 | await page.goto("https://creator.xiaohongshu.com/creator-micro/content/upload") 22 | try: 23 | await page.wait_for_url("https://creator.xiaohongshu.com/creator-micro/content/upload", timeout=5000) 24 | except: 25 | print("[+] 等待5秒 cookie 失效") 26 | await context.close() 27 | await browser.close() 28 | return False 29 | # 2024.06.17 抖音创作者中心改版 30 | if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count(): 31 | print("[+] 等待5秒 cookie 失效") 32 | return False 33 | else: 34 | print("[+] cookie 有效") 35 | return True 36 | 37 | 38 | async def xiaohongshu_setup(account_file, handle=False): 39 | if not os.path.exists(account_file) or not await cookie_auth(account_file): 40 | if not handle: 41 | # Todo alert message 42 | return False 43 | xiaohongshu_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') 44 | await xiaohongshu_cookie_gen(account_file) 45 | return True 46 | 47 | 48 | async def xiaohongshu_cookie_gen(account_file): 49 | async with async_playwright() as playwright: 50 | options = { 51 | 'headless': False 52 | } 53 | # Make sure to run headed. 54 | browser = await playwright.chromium.launch(**options) 55 | # Setup context however you like. 56 | context = await browser.new_context() # Pass any options 57 | context = await set_init_script(context) 58 | # Pause the page, and start recording manually. 59 | page = await context.new_page() 60 | await page.goto("https://creator.xiaohongshu.com/") 61 | await page.pause() 62 | # 点击调试器的继续,保存cookie 63 | await context.storage_state(path=account_file) 64 | 65 | 66 | class XiaoHongShuVideo(object): 67 | def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None): 68 | self.title = title # 视频标题 69 | self.file_path = file_path 70 | self.tags = tags 71 | self.publish_date = publish_date 72 | self.account_file = account_file 73 | self.date_format = '%Y年%m月%d日 %H:%M' 74 | self.local_executable_path = LOCAL_CHROME_PATH 75 | self.thumbnail_path = thumbnail_path 76 | 77 | async def set_schedule_time_xiaohongshu(self, page, publish_date): 78 | print(" [-] 正在设置定时发布时间...") 79 | print(f"publish_date: {publish_date}") 80 | 81 | # 使用文本内容定位元素 82 | # element = await page.wait_for_selector( 83 | # 'label:has-text("定时发布")', 84 | # timeout=5000 # 5秒超时时间 85 | # ) 86 | # await element.click() 87 | 88 | # # 选择包含特定文本内容的 label 元素 89 | label_element = page.locator("label:has-text('定时发布')") 90 | # # 在选中的 label 元素下点击 checkbox 91 | await label_element.click() 92 | await asyncio.sleep(1) 93 | publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M") 94 | print(f"publish_date_hour: {publish_date_hour}") 95 | 96 | await asyncio.sleep(1) 97 | await page.locator('.el-input__inner[placeholder="选择日期和时间"]').click() 98 | await page.keyboard.press("Control+KeyA") 99 | await page.keyboard.type(str(publish_date_hour)) 100 | await page.keyboard.press("Enter") 101 | 102 | await asyncio.sleep(1) 103 | 104 | async def handle_upload_error(self, page): 105 | xiaohongshu_logger.info('视频出错了,重新上传中') 106 | await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path) 107 | 108 | async def upload(self, playwright: Playwright) -> None: 109 | # 使用 Chromium 浏览器启动一个浏览器实例 110 | if self.local_executable_path: 111 | browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path) 112 | else: 113 | browser = await playwright.chromium.launch(headless=False) 114 | # 创建一个浏览器上下文,使用指定的 cookie 文件 115 | context = await browser.new_context( 116 | viewport={"width": 1600, "height": 900}, 117 | storage_state=f"{self.account_file}" 118 | ) 119 | context = await set_init_script(context) 120 | 121 | # 创建一个新的页面 122 | page = await context.new_page() 123 | # 访问指定的 URL 124 | await page.goto("https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video") 125 | xiaohongshu_logger.info(f'[+]正在上传-------{self.title}.mp4') 126 | # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 127 | xiaohongshu_logger.info(f'[-] 正在打开主页...') 128 | await page.wait_for_url("https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video") 129 | # 点击 "上传视频" 按钮 130 | await page.locator("div[class^='upload-content'] input[class='upload-input']").set_input_files(self.file_path) 131 | 132 | # 等待页面跳转到指定的 URL 2025.01.08修改在原有基础上兼容两种页面 133 | while True: 134 | try: 135 | # 等待upload-input元素出现 136 | upload_input = await page.wait_for_selector('input.upload-input', timeout=3000) 137 | # 获取下一个兄弟元素 138 | preview_new = await upload_input.query_selector( 139 | 'xpath=following-sibling::div[contains(@class, "preview-new")]') 140 | if preview_new: 141 | # 在preview-new元素中查找包含"上传成功"的stage元素 142 | stage_elements = await preview_new.query_selector_all('div.stage') 143 | upload_success = False 144 | for stage in stage_elements: 145 | text_content = await page.evaluate('(element) => element.textContent', stage) 146 | if '上传成功' in text_content: 147 | upload_success = True 148 | break 149 | if upload_success: 150 | xiaohongshu_logger.info("[+] 检测到上传成功标识!") 151 | break # 成功检测到上传成功后跳出循环 152 | else: 153 | print(" [-] 未找到上传成功标识,继续等待...") 154 | else: 155 | print(" [-] 未找到预览元素,继续等待...") 156 | await asyncio.sleep(1) 157 | except Exception as e: 158 | print(f" [-] 检测过程出错: {str(e)},重新尝试...") 159 | await asyncio.sleep(0.5) # 等待0.5秒后重新尝试 160 | 161 | # 填充标题和话题 162 | # 检查是否存在包含输入框的元素 163 | # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素 164 | await asyncio.sleep(1) 165 | xiaohongshu_logger.info(f' [-] 正在填充标题和话题...') 166 | title_container = page.locator('div.input.titleInput').locator('input.d-text') 167 | if await title_container.count(): 168 | await title_container.fill(self.title[:30]) 169 | else: 170 | titlecontainer = page.locator(".notranslate") 171 | await titlecontainer.click() 172 | await page.keyboard.press("Backspace") 173 | await page.keyboard.press("Control+KeyA") 174 | await page.keyboard.press("Delete") 175 | await page.keyboard.type(self.title) 176 | await page.keyboard.press("Enter") 177 | css_selector = ".ql-editor" # 不能加上 .ql-blank 属性,这样只能获取第一次非空状态 178 | for index, tag in enumerate(self.tags, start=1): 179 | await page.type(css_selector, "#" + tag) 180 | await page.press(css_selector, "Space") 181 | xiaohongshu_logger.info(f'总共添加{len(self.tags)}个话题') 182 | 183 | # while True: 184 | # # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待 185 | # try: 186 | # # 新版:定位重新上传 187 | # number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count() 188 | # if number > 0: 189 | # xiaohongshu_logger.success(" [-]视频上传完毕") 190 | # break 191 | # else: 192 | # xiaohongshu_logger.info(" [-] 正在上传视频中...") 193 | # await asyncio.sleep(2) 194 | 195 | # if await page.locator('div.progress-div > div:has-text("上传失败")').count(): 196 | # xiaohongshu_logger.error(" [-] 发现上传出错了... 准备重试") 197 | # await self.handle_upload_error(page) 198 | # except: 199 | # xiaohongshu_logger.info(" [-] 正在上传视频中...") 200 | # await asyncio.sleep(2) 201 | 202 | # 上传视频封面 203 | # await self.set_thumbnail(page, self.thumbnail_path) 204 | 205 | # 更换可见元素 206 | # await self.set_location(page, "青岛市") 207 | 208 | # # 頭條/西瓜 209 | # third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' 210 | # # 定位是否有第三方平台 211 | # if await page.locator(third_part_element).count(): 212 | # # 检测是否是已选中状态 213 | # if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'): 214 | # await page.locator(third_part_element).locator('input.semi-switch-native-control').click() 215 | 216 | if self.publish_date != 0: 217 | await self.set_schedule_time_xiaohongshu(page, self.publish_date) 218 | 219 | # 判断视频是否发布成功 220 | while True: 221 | try: 222 | # 等待包含"定时发布"文本的button元素出现并点击 223 | if self.publish_date != 0: 224 | await page.locator('button:has-text("定时发布")').click() 225 | else: 226 | await page.locator('button:has-text("发布")').click() 227 | await page.wait_for_url( 228 | "https://creator.xiaohongshu.com/publish/success?**", 229 | timeout=3000 230 | ) # 如果自动跳转到作品页面,则代表发布成功 231 | xiaohongshu_logger.success(" [-]视频发布成功") 232 | break 233 | except: 234 | xiaohongshu_logger.info(" [-] 视频正在发布中...") 235 | await page.screenshot(full_page=True) 236 | await asyncio.sleep(0.5) 237 | 238 | await context.storage_state(path=self.account_file) # 保存cookie 239 | xiaohongshu_logger.success(' [-]cookie更新完毕!') 240 | await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 241 | # 关闭浏览器上下文和浏览器实例 242 | await context.close() 243 | await browser.close() 244 | 245 | async def set_thumbnail(self, page: Page, thumbnail_path: str): 246 | if thumbnail_path: 247 | await page.click('text="选择封面"') 248 | await page.wait_for_selector("div.semi-modal-content:visible") 249 | await page.click('text="设置竖封面"') 250 | await page.wait_for_timeout(2000) # 等待2秒 251 | # 定位到上传区域并点击 252 | await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path) 253 | await page.wait_for_timeout(2000) # 等待2秒 254 | await page.locator("div[class^='extractFooter'] button:visible:has-text('完成')").click() 255 | # finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')") 256 | # if await finish_confirm_element.count(): 257 | # await finish_confirm_element.click() 258 | # await page.locator("div[class^='footer'] button:has-text('完成')").click() 259 | 260 | async def set_location(self, page: Page, location: str = "青岛市"): 261 | print(f"开始设置位置: {location}") 262 | 263 | # 点击地点输入框 264 | print("等待地点输入框加载...") 265 | loc_ele = await page.wait_for_selector('div.d-text.d-select-placeholder.d-text-ellipsis.d-text-nowrap') 266 | print(f"已定位到地点输入框: {loc_ele}") 267 | await loc_ele.click() 268 | print("点击地点输入框完成") 269 | 270 | # 输入位置名称 271 | print(f"等待1秒后输入位置名称: {location}") 272 | await page.wait_for_timeout(1000) 273 | await page.keyboard.type(location) 274 | print(f"位置名称输入完成: {location}") 275 | 276 | # 等待下拉列表加载 277 | print("等待下拉列表加载...") 278 | dropdown_selector = 'div.d-popover.d-popover-default.d-dropdown.--size-min-width-large' 279 | await page.wait_for_timeout(3000) 280 | try: 281 | await page.wait_for_selector(dropdown_selector, timeout=3000) 282 | print("下拉列表已加载") 283 | except: 284 | print("下拉列表未按预期显示,可能结构已变化") 285 | 286 | # 增加等待时间以确保内容加载完成 287 | print("额外等待1秒确保内容渲染完成...") 288 | await page.wait_for_timeout(1000) 289 | 290 | # 尝试更灵活的XPath选择器 291 | print("尝试使用更灵活的XPath选择器...") 292 | flexible_xpath = ( 293 | f'//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' 294 | f'//div[contains(@class, "d-options-wrapper")]' 295 | f'//div[contains(@class, "d-grid") and contains(@class, "d-options")]' 296 | f'//div[contains(@class, "name") and text()="{location}"]' 297 | ) 298 | await page.wait_for_timeout(3000) 299 | 300 | # 尝试定位元素 301 | print(f"尝试定位包含'{location}'的选项...") 302 | try: 303 | # 先尝试使用更灵活的选择器 304 | location_option = await page.wait_for_selector( 305 | flexible_xpath, 306 | timeout=3000 307 | ) 308 | 309 | if location_option: 310 | print(f"使用灵活选择器定位成功: {location_option}") 311 | else: 312 | # 如果灵活选择器失败,再尝试原选择器 313 | print("灵活选择器未找到元素,尝试原始选择器...") 314 | location_option = await page.wait_for_selector( 315 | f'//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' 316 | f'//div[contains(@class, "d-options-wrapper")]' 317 | f'//div[contains(@class, "d-grid") and contains(@class, "d-options")]' 318 | f'/div[1]//div[contains(@class, "name") and text()="{location}"]', 319 | timeout=2000 320 | ) 321 | 322 | # 滚动到元素并点击 323 | print("滚动到目标选项...") 324 | await location_option.scroll_into_view_if_needed() 325 | print("元素已滚动到视图内") 326 | 327 | # 增加元素可见性检查 328 | is_visible = await location_option.is_visible() 329 | print(f"目标选项是否可见: {is_visible}") 330 | 331 | # 点击元素 332 | print("准备点击目标选项...") 333 | await location_option.click() 334 | print(f"成功选择位置: {location}") 335 | return True 336 | 337 | except Exception as e: 338 | print(f"定位位置失败: {e}") 339 | 340 | # 打印更多调试信息 341 | print("尝试获取下拉列表中的所有选项...") 342 | try: 343 | all_options = await page.query_selector_all( 344 | '//div[contains(@class, "d-popover") and contains(@class, "d-dropdown")]' 345 | '//div[contains(@class, "d-options-wrapper")]' 346 | '//div[contains(@class, "d-grid") and contains(@class, "d-options")]' 347 | '/div' 348 | ) 349 | print(f"找到 {len(all_options)} 个选项") 350 | 351 | # 打印前3个选项的文本内容 352 | for i, option in enumerate(all_options[:3]): 353 | option_text = await option.inner_text() 354 | print(f"选项 {i+1}: {option_text.strip()[:50]}...") 355 | 356 | except Exception as e: 357 | print(f"获取选项列表失败: {e}") 358 | 359 | # 截图保存(取消注释使用) 360 | # await page.screenshot(path=f"location_error_{location}.png") 361 | return False 362 | 363 | async def main(self): 364 | async with async_playwright() as playwright: 365 | await self.upload(playwright) 366 | 367 | 368 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/utils/__init__.py -------------------------------------------------------------------------------- /utils/base_social_media.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | 4 | from conf import BASE_DIR 5 | 6 | SOCIAL_MEDIA_DOUYIN = "douyin" 7 | SOCIAL_MEDIA_TENCENT = "tencent" 8 | SOCIAL_MEDIA_TIKTOK = "tiktok" 9 | SOCIAL_MEDIA_BILIBILI = "bilibili" 10 | SOCIAL_MEDIA_KUAISHOU = "kuaishou" 11 | 12 | 13 | def get_supported_social_media() -> List[str]: 14 | return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU] 15 | 16 | 17 | def get_cli_action() -> List[str]: 18 | return ["upload", "login", "watch"] 19 | 20 | 21 | async def set_init_script(context): 22 | stealth_js_path = Path(BASE_DIR / "utils/stealth.min.js") 23 | await context.add_init_script(path=stealth_js_path) 24 | return context 25 | -------------------------------------------------------------------------------- /utils/constant.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class TencentZoneTypes(enum.Enum): 5 | LIFESTYLE = '生活' 6 | CUTE_KIDS = '萌娃' 7 | MUSIC = '音乐' 8 | KNOWLEDGE = '知识' 9 | EMOTION = '情感' 10 | TRAVEL_SCENERY = '旅行风景' 11 | FASHION = '时尚' 12 | FOOD = '美食' 13 | LIFE_HACKS = '生活技巧' 14 | DANCE = '舞蹈' 15 | MOVIES_TV_SHOWS = '影视综艺' 16 | SPORTS = '运动' 17 | FUNNY = '搞笑' 18 | CELEBRITIES = '明星名人' 19 | NEWS_INFO = '新闻资讯' 20 | GAMING = '游戏' 21 | AUTOMOTIVE = '车' 22 | ANIME = '二次元' 23 | TALENT = '才艺' 24 | CUTE_PETS = '萌宠' 25 | INDUSTRY_MACHINERY_CONSTRUCTION = '机械' 26 | ANIMALS = '动物' 27 | PARENTING = '育儿' 28 | TECHNOLOGY = '科技' 29 | 30 | class VideoZoneTypes(enum.Enum): 31 | """ 32 | 所有分区枚举 33 | 34 | - MAINPAGE: 主页 35 | - ANIME: 番剧 36 | - ANIME_SERIAL: 连载中番剧 37 | - ANIME_FINISH: 已完结番剧 38 | - ANIME_INFORMATION: 资讯 39 | - ANIME_OFFICAL: 官方延伸 40 | - MOVIE: 电影 41 | - GUOCHUANG: 国创 42 | - GUOCHUANG_CHINESE: 国产动画 43 | - GUOCHUANG_ORIGINAL: 国产原创相关 44 | - GUOCHUANG_PUPPETRY: 布袋戏 45 | - GUOCHUANG_MOTIONCOMIC: 动态漫·广播剧 46 | - GUOCHUANG_INFORMATION: 资讯 47 | - TELEPLAY: 电视剧 48 | - DOCUMENTARY: 纪录片 49 | - DOUGA: 动画 50 | - DOUGA_MAD: MAD·AMV 51 | - DOUGA_MMD: MMD·3D 52 | - DOUGA_VOICE: 短片·手书·配音 53 | - DOUGA_GARAGE_KIT: 手办·模玩 54 | - DOUGA_TOKUSATSU: 特摄 55 | - DOUGA_ACGNTALKS: 动漫杂谈 56 | - DOUGA_OTHER: 综合 57 | - GAME: 游戏 58 | - GAME_STAND_ALONE: 单机游戏 59 | - GAME_ESPORTS: 电子竞技 60 | - GAME_MOBILE: 手机游戏 61 | - GAME_ONLINE: 网络游戏 62 | - GAME_BOARD: 桌游棋牌 63 | - GAME_GMV: GMV 64 | - GAME_MUSIC: 音游 65 | - GAME_MUGEN: Mugen 66 | - KICHIKU: 鬼畜 67 | - KICHIKU_GUIDE: 鬼畜调教 68 | - KICHIKU_MAD: 音MAD 69 | - KICHIKU_MANUAL_VOCALOID: 人力VOCALOID 70 | - KICHIKU_THEATRE: 鬼畜剧场 71 | - KICHIKU_COURSE: 教程演示 72 | - MUSIC: 音乐 73 | - MUSIC_ORIGINAL: 原创音乐 74 | - MUSIC_COVER: 翻唱 75 | - MUSIC_PERFORM: 演奏 76 | - MUSIC_VOCALOID: VOCALOID·UTAU 77 | - MUSIC_LIVE: 音乐现场 78 | - MUSIC_MV: MV 79 | - MUSIC_COMMENTARY: 乐评盘点 80 | - MUSIC_TUTORIAL: 音乐教学 81 | - MUSIC_OTHER: 音乐综合 82 | - DANCE: 舞蹈 83 | - DANCE_OTAKU: 宅舞 84 | - DANCE_HIPHOP: 街舞 85 | - DANCE_STAR: 明星舞蹈 86 | - DANCE_CHINA: 中国舞 87 | - DANCE_THREE_D: 舞蹈综合 88 | - DANCE_DEMO: 舞蹈教程 89 | - CINEPHILE: 影视 90 | - CINEPHILE_CINECISM: 影视杂谈 91 | - CINEPHILE_MONTAGE: 影视剪辑 92 | - CINEPHILE_SHORTFILM: 小剧场 93 | - CINEPHILE_TRAILER_INFO: 预告·资讯 94 | - ENT: 娱乐 95 | - ENT_VARIETY: 综艺 96 | - ENT_TALKER: 娱乐杂谈 97 | - ENT_FANS: 粉丝创作 98 | - ENT_CELEBRITY: 明星综合 99 | - KNOWLEDGE: 知识 100 | - KNOWLEDGE_SCIENCE: 科学科普 101 | - KNOWLEDGE_SOCIAL_SCIENCE: 社科·法律·心理 102 | - KNOWLEDGE_HUMANITY_HISTORY: 人文历史 103 | - KNOWLEDGE_BUSINESS: 财经商业 104 | - KNOWLEDGE_CAMPUS: 校园学习 105 | - KNOWLEDGE_CAREER: 职业职场 106 | - KNOWLEDGE_DESIGN: 设计·创意 107 | - KNOWLEDGE_SKILL: 野生技能协会 108 | - TECH: 科技 109 | - TECH_DIGITAL: 数码 110 | - TECH_APPLICATION: 软件应用 111 | - TECH_COMPUTER_TECH: 计算机技术 112 | - TECH_INDUSTRY: 科工机械 113 | - INFORMATION: 资讯 114 | - INFORMATION_HOTSPOT: 热点 115 | - INFORMATION_GLOBAL: 环球 116 | - INFORMATION_SOCIAL: 社会 117 | - INFORMATION_MULTIPLE: 综合 118 | - FOOD: 美食 119 | - FOOD_MAKE: 美食制作 120 | - FOOD_DETECTIVE: 美食侦探 121 | - FOOD_MEASUREMENT: 美食测评 122 | - FOOD_RURAL: 田园美食 123 | - FOOD_RECORD: 美食记录 124 | - LIFE: 生活 125 | - LIFE_FUNNY: 搞笑 126 | - LIFE_TRAVEL: 出行 127 | - LIFE_RURALLIFE: 三农 128 | - LIFE_HOME: 家居房产 129 | - LIFE_HANDMAKE: 手工 130 | - LIFE_PAINTING: 绘画 131 | - LIFE_DAILY: 日常 132 | - CAR: 汽车 133 | - CAR_RACING: 赛车 134 | - CAR_MODIFIEDVEHICLE: 改装玩车 135 | - CAR_NEWENERGYVEHICLE: 新能源车 136 | - CAR_TOURINGCAR: 房车 137 | - CAR_MOTORCYCLE: 摩托车 138 | - CAR_STRATEGY: 购车攻略 139 | - CAR_LIFE: 汽车生活 140 | - FASHION: 时尚 141 | - FASHION_MAKEUP: 美妆护肤 142 | - FASHION_COS: 仿妆cos 143 | - FASHION_CLOTHING: 穿搭 144 | - FASHION_TREND: 时尚潮流 145 | - SPORTS: 运动 146 | - SPORTS_BASKETBALL: 篮球 147 | - SPORTS_FOOTBALL: 足球 148 | - SPORTS_AEROBICS: 健身 149 | - SPORTS_ATHLETIC: 竞技体育 150 | - SPORTS_CULTURE: 运动文化 151 | - SPORTS_COMPREHENSIVE: 运动综合 152 | - ANIMAL: 动物圈 153 | - ANIMAL_CAT: 喵星人 154 | - ANIMAL_DOG: 汪星人 155 | - ANIMAL_PANDA: 大熊猫 156 | - ANIMAL_WILD_ANIMAL: 野生动物 157 | - ANIMAL_REPTILES: 爬宠 158 | - ANIMAL_COMPOSITE: 动物综合 159 | - VLOG: VLOG 160 | """ 161 | 162 | MAINPAGE = 0 163 | 164 | ANIME = 13 165 | ANIME_SERIAL = 33 166 | ANIME_FINISH = 32 167 | ANIME_INFORMATION = 51 168 | ANIME_OFFICAL = 152 169 | 170 | MOVIE = 23 171 | 172 | GUOCHUANG = 167 173 | GUOCHUANG_CHINESE = 153 174 | GUOCHUANG_ORIGINAL = 168 175 | GUOCHUANG_PUPPETRY = 169 176 | GUOCHUANG_MOTIONCOMIC = 195 177 | GUOCHUANG_INFORMATION = 170 178 | 179 | TELEPLAY = 11 180 | 181 | DOCUMENTARY = 177 182 | 183 | DOUGA = 1 184 | DOUGA_MAD = 24 185 | DOUGA_MMD = 25 186 | DOUGA_VOICE = 47 187 | DOUGA_GARAGE_KIT = 210 188 | DOUGA_TOKUSATSU = 86 189 | DOUGA_ACGNTALKS = 253 190 | DOUGA_OTHER = 27 191 | 192 | GAME = 4 193 | GAME_STAND_ALONE = 17 194 | GAME_ESPORTS = 171 195 | GAME_MOBILE = 172 196 | GAME_ONLINE = 65 197 | GAME_BOARD = 173 198 | GAME_GMV = 121 199 | GAME_MUSIC = 136 200 | GAME_MUGEN = 19 201 | 202 | KICHIKU = 119 203 | KICHIKU_GUIDE = 22 204 | KICHIKU_MAD = 26 205 | KICHIKU_MANUAL_VOCALOID = 126 206 | KICHIKU_THEATRE = 216 207 | KICHIKU_COURSE = 127 208 | 209 | MUSIC = 3 210 | MUSIC_ORIGINAL = 28 211 | MUSIC_COVER = 31 212 | MUSIC_PERFORM = 59 213 | MUSIC_VOCALOID = 30 214 | MUSIC_LIVE = 29 215 | MUSIC_MV = 193 216 | MUSIC_COMMENTARY = 243 217 | MUSIC_TUTORIAL = 244 218 | MUSIC_OTHER = 130 219 | 220 | DANCE = 129 221 | DANCE_OTAKU = 20 222 | DANCE_HIPHOP = 198 223 | DANCE_STAR = 199 224 | DANCE_CHINA = 200 225 | DANCE_THREE_D = 154 226 | DANCE_DEMO = 156 227 | 228 | CINEPHILE = 181 229 | CINEPHILE_CINECISM = 182 230 | CINEPHILE_MONTAGE = 183 231 | CINEPHILE_SHORTFILM = 85 232 | CINEPHILE_TRAILER_INFO = 184 233 | 234 | ENT = 5 235 | ENT_VARIETY = 71 236 | ENT_TALKER = 241 237 | ENT_FANS = 242 238 | ENT_CELEBRITY = 137 239 | 240 | KNOWLEDGE = 36 241 | KNOWLEDGE_SCIENCE = 201 242 | KNOWLEDGE_SOCIAL_SCIENCE = 124 243 | KNOWLEDGE_HUMANITY_HISTORY = 228 244 | KNOWLEDGE_BUSINESS = 207 245 | KNOWLEDGE_CAMPUS = 208 246 | KNOWLEDGE_CAREER = 209 247 | KNOWLEDGE_DESIGN = 229 248 | KNOWLEDGE_SKILL = 122 249 | 250 | TECH = 188 251 | TECH_DIGITAL = 95 252 | TECH_APPLICATION = 230 253 | TECH_COMPUTER_TECH = 231 254 | TECH_INDUSTRY = 232 255 | 256 | INFORMATION = 202 257 | INFORMATION_HOTSPOT = 203 258 | INFORMATION_GLOBAL = 204 259 | INFORMATION_SOCIAL = 205 260 | INFORMATION_MULTIPLE = 206 261 | 262 | FOOD = 211 263 | FOOD_MAKE = 76 264 | FOOD_DETECTIVE = 212 265 | FOOD_MEASUREMENT = 213 266 | FOOD_RURAL = 214 267 | FOOD_RECORD = 215 268 | 269 | LIFE = 160 270 | LIFE_FUNNY = 138 271 | LIFE_TRAVEL = 250 272 | LIFE_RURALLIFE = 251 273 | LIFE_HOME = 239 274 | LIFE_HANDMAKE = 161 275 | LIFE_PAINTING = 162 276 | LIFE_DAILY = 21 277 | 278 | CAR = 223 279 | CAR_RACING = 245 280 | CAR_MODIFIEDVEHICLE = 246 281 | CAR_NEWENERGYVEHICLE = 247 282 | CAR_TOURINGCAR = 248 283 | CAR_MOTORCYCLE = 240 284 | CAR_STRATEGY = 227 285 | CAR_LIFE = 176 286 | 287 | FASHION = 155 288 | FASHION_MAKEUP = 157 289 | FASHION_COS = 252 290 | FASHION_CLOTHING = 158 291 | FASHION_TREND = 159 292 | 293 | SPORTS = 234 294 | SPORTS_BASKETBALL = 235 295 | SPORTS_FOOTBALL = 249 296 | SPORTS_AEROBICS = 164 297 | SPORTS_ATHLETIC = 236 298 | SPORTS_CULTURE = 237 299 | SPORTS_COMPREHENSIVE = 238 300 | 301 | ANIMAL = 217 302 | ANIMAL_CAT = 218 303 | ANIMAL_DOG = 219 304 | ANIMAL_PANDA = 220 305 | ANIMAL_WILD_ANIMAL = 221 306 | ANIMAL_REPTILES = 222 307 | ANIMAL_COMPOSITE = 75 308 | 309 | VLOG = 19 310 | -------------------------------------------------------------------------------- /utils/files_times.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from datetime import datetime 4 | from pathlib import Path 5 | 6 | from conf import BASE_DIR 7 | 8 | 9 | def get_absolute_path(relative_path: str, base_dir: str = None) -> str: 10 | # Convert the relative path to an absolute path 11 | absolute_path = Path(BASE_DIR) / base_dir / relative_path 12 | return str(absolute_path) 13 | 14 | 15 | def get_title_and_hashtags(filename): 16 | """ 17 | 获取视频标题和 hashtag 18 | 19 | Args: 20 | filename: 视频文件名 21 | 22 | Returns: 23 | 视频标题和 hashtag 列表 24 | """ 25 | 26 | # 获取视频标题和 hashtag txt 文件名 27 | txt_filename = filename.replace(".mp4", ".txt") 28 | 29 | # 读取 txt 文件 30 | with open(txt_filename, "r", encoding="utf-8") as f: 31 | content = f.read() 32 | 33 | # 获取标题和 hashtag 34 | splite_str = content.strip().split("\n") 35 | title = splite_str[0] 36 | hashtags = splite_str[1].replace("#", "").split(" ") 37 | 38 | return title, hashtags 39 | 40 | 41 | def generate_schedule_time_next_day(total_videos, videos_per_day = 1, daily_times=None, timestamps=False, start_days=0): 42 | """ 43 | Generate a schedule for video uploads, starting from the next day. 44 | 45 | Args: 46 | - total_videos: Total number of videos to be uploaded. 47 | - videos_per_day: Number of videos to be uploaded each day. 48 | - daily_times: Optional list of specific times of the day to publish the videos. 49 | - timestamps: Boolean to decide whether to return timestamps or datetime objects. 50 | - start_days: Start from after start_days. 51 | 52 | Returns: 53 | - A list of scheduling times for the videos, either as timestamps or datetime objects. 54 | """ 55 | if videos_per_day <= 0: 56 | raise ValueError("videos_per_day should be a positive integer") 57 | 58 | if daily_times is None: 59 | # Default times to publish videos if not provided 60 | daily_times = [6, 11, 14, 16, 22] 61 | 62 | if videos_per_day > len(daily_times): 63 | raise ValueError("videos_per_day should not exceed the length of daily_times") 64 | 65 | # Generate timestamps 66 | schedule = [] 67 | current_time = datetime.now() 68 | 69 | for video in range(total_videos): 70 | day = video // videos_per_day + start_days + 1 # +1 to start from the next day 71 | daily_video_index = video % videos_per_day 72 | 73 | # Calculate the time for the current video 74 | hour = daily_times[daily_video_index] 75 | time_offset = timedelta(days=day, hours=hour - current_time.hour, minutes=-current_time.minute, 76 | seconds=-current_time.second, microseconds=-current_time.microsecond) 77 | timestamp = current_time + time_offset 78 | 79 | schedule.append(timestamp) 80 | 81 | if timestamps: 82 | schedule = [int(time.timestamp()) for time in schedule] 83 | return schedule 84 | -------------------------------------------------------------------------------- /utils/log.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from sys import stdout 3 | from loguru import logger 4 | 5 | from conf import BASE_DIR 6 | 7 | 8 | def log_formatter(record: dict) -> str: 9 | """ 10 | Formatter for log records. 11 | :param dict record: Log object containing log metadata & message. 12 | :returns: str 13 | """ 14 | colors = { 15 | "TRACE": "#cfe2f3", 16 | "INFO": "#9cbfdd", 17 | "DEBUG": "#8598ea", 18 | "WARNING": "#dcad5a", 19 | "SUCCESS": "#3dd08d", 20 | "ERROR": "#ae2c2c" 21 | } 22 | color = colors.get(record["level"].name, "#b3cfe7") 23 | return f"{{time:YYYY-MM-DD HH:mm:ss}} | {{level}}: {{message}}\n" 24 | 25 | 26 | def create_logger(log_name: str, file_path: str): 27 | """ 28 | Create custom logger for different business modules. 29 | :param str log_name: name of log 30 | :param str file_path: Optional path to log file 31 | :returns: Configured logger 32 | """ 33 | def filter_record(record): 34 | return record["extra"].get("business_name") == log_name 35 | 36 | Path(BASE_DIR / file_path).parent.mkdir(exist_ok=True) 37 | logger.add(Path(BASE_DIR / file_path), filter=filter_record, level="INFO", rotation="10 MB", retention="10 days", backtrace=True, diagnose=True) 38 | return logger.bind(business_name=log_name) 39 | 40 | 41 | # Remove all existing handlers 42 | logger.remove() 43 | # Add a standard console handler 44 | logger.add(stdout, colorize=True, format=log_formatter) 45 | 46 | douyin_logger = create_logger('douyin', 'logs/douyin.log') 47 | tencent_logger = create_logger('tencent', 'logs/tencent.log') 48 | xhs_logger = create_logger('xhs', 'logs/xhs.log') 49 | tiktok_logger = create_logger('tiktok', 'logs/tiktok.log') 50 | bilibili_logger = create_logger('bilibili', 'logs/bilibili.log') 51 | kuaishou_logger = create_logger('kuaishou', 'logs/kuaishou.log') 52 | baijiahao_logger = create_logger('baijiahao', 'logs/baijiahao.log') 53 | xiaohongshu_logger = create_logger('xiaohongshu', 'logs/xiaohongshu.log') 54 | -------------------------------------------------------------------------------- /utils/network.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from functools import wraps 4 | 5 | 6 | def async_retry(timeout=60, max_retries=None): 7 | def decorator(func): 8 | @wraps(func) 9 | async def wrapper(*args, **kwargs): 10 | start_time = time.time() 11 | attempts = 0 12 | while True: 13 | try: 14 | return await func(*args, **kwargs) 15 | except Exception as e: 16 | attempts += 1 17 | if max_retries is not None and attempts >= max_retries: 18 | print(f"Reached maximum retries of {max_retries}.") 19 | raise Exception(f"Failed after {max_retries} retries.") from e 20 | if time.time() - start_time > timeout: 21 | print(f"Function timeout after {timeout} seconds.") 22 | raise TimeoutError(f"Function execution exceeded {timeout} seconds timeout.") from e 23 | print(f"Attempt {attempts} failed: {e}. Retrying...") 24 | await asyncio.sleep(1) # Sleep to avoid tight loop or provide backoff logic here 25 | 26 | return wrapper 27 | 28 | return decorator -------------------------------------------------------------------------------- /videos/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/videos/demo.mp4 -------------------------------------------------------------------------------- /videos/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreammis/social-auto-upload/95217cc9ed52039df1b2e5280788f1c2b2560ca9/videos/demo.png -------------------------------------------------------------------------------- /videos/demo.txt: -------------------------------------------------------------------------------- 1 | 男子为了心爱之人每天坚守❤️‍🩹 2 | #坚持不懈 #爱情执着 #奋斗使者 #短视频 --------------------------------------------------------------------------------