Showing
43 changed files
with
1264 additions
and
2 deletions
.gitignore
0 → 100644
| 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 | + | ||
| 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 | +cookies | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | # xj-marketing | 1 | # xj-marketing |
| 2 | -social-auto-upload 该项目旨在自动化发布视频到各个社交媒体平台 | 2 | +xj-marketing 该项目旨在自动化发布视频到各个社交媒体平台 |
| 3 | 3 | ||
| 4 | -social-auto-upload This project aims to automate the posting of videos to various social media platforms. | 4 | +xj-marketing This project aims to automate the posting of videos to various social media platforms. |
| 5 | 5 | ||
| 6 | 6 | ||
| 7 | ## 💡Feature | 7 | ## 💡Feature | ... | ... |
cli_main.py
0 → 100644
| 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 uploader.wyh_uploader.main import wyh_setup | ||
| 13 | +from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \ | ||
| 14 | + SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU, SOCIAL_MEDIA_WANGYIHAO | ||
| 15 | +from utils.constant import TencentZoneTypes | ||
| 16 | +from utils.files_times import get_title_and_hashtags | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +def parse_schedule(schedule_raw): | ||
| 20 | + if schedule_raw: | ||
| 21 | + schedule = datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M') | ||
| 22 | + else: | ||
| 23 | + schedule = None | ||
| 24 | + return schedule | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +async def main(): | ||
| 28 | + # 主解析器 | ||
| 29 | + parser = argparse.ArgumentParser(description="Upload video to multiple social-media.") | ||
| 30 | + parser.add_argument("platform", metavar='platform', choices=get_supported_social_media(), help="Choose social-media platform: douyin tencent tiktok kuaishou") | ||
| 31 | + | ||
| 32 | + parser.add_argument("account_name", type=str, help="Account name for the platform: xiaoA") | ||
| 33 | + subparsers = parser.add_subparsers(dest="action", metavar='action', help="Choose action", required=True) | ||
| 34 | + | ||
| 35 | + actions = get_cli_action() | ||
| 36 | + for action in actions: | ||
| 37 | + action_parser = subparsers.add_parser(action, help=f'{action} operation') | ||
| 38 | + if action == 'login': | ||
| 39 | + # Login 不需要额外参数 | ||
| 40 | + continue | ||
| 41 | + elif action == 'upload': | ||
| 42 | + action_parser.add_argument("video_file", help="Path to the Video file") | ||
| 43 | + action_parser.add_argument("-pt", "--publish_type", type=int, choices=[0, 1], | ||
| 44 | + help="0 for immediate, 1 for scheduled", default=0) | ||
| 45 | + action_parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format') | ||
| 46 | + | ||
| 47 | + # 解析命令行参数 | ||
| 48 | + args = parser.parse_args() | ||
| 49 | + # 参数校验 | ||
| 50 | + if args.action == 'upload': | ||
| 51 | + if not exists(args.video_file): | ||
| 52 | + raise FileNotFoundError(f'Could not find the video file at {args["video_file"]}') | ||
| 53 | + if args.publish_type == 1 and not args.schedule: | ||
| 54 | + parser.error("The schedule must must be specified for scheduled publishing.") | ||
| 55 | + | ||
| 56 | + account_file = Path(BASE_DIR / "cookies" / f"{args.platform}_{args.account_name}.json") | ||
| 57 | + account_file.parent.mkdir(exist_ok=True) | ||
| 58 | + | ||
| 59 | + print(account_file) | ||
| 60 | + | ||
| 61 | + # 根据 action 处理不同的逻辑 | ||
| 62 | + if args.action == 'login': | ||
| 63 | + print(f"Logging in with account {args.account_name} on platform {args.platform}") | ||
| 64 | + if args.platform == SOCIAL_MEDIA_DOUYIN: | ||
| 65 | + await douyin_setup(str(account_file), handle=True) | ||
| 66 | + elif args.platform == SOCIAL_MEDIA_TIKTOK: | ||
| 67 | + await tiktok_setup(str(account_file), handle=True) | ||
| 68 | + elif args.platform == SOCIAL_MEDIA_TENCENT: | ||
| 69 | + await weixin_setup(str(account_file), handle=True) | ||
| 70 | + elif args.platform == SOCIAL_MEDIA_KUAISHOU: | ||
| 71 | + await ks_setup(str(account_file), handle=True) | ||
| 72 | + elif args.platform == SOCIAL_MEDIA_WANGYIHAO: | ||
| 73 | + await wyh_setup(str(account_file), handle=True) | ||
| 74 | + | ||
| 75 | + elif args.action == 'upload': | ||
| 76 | + title, tags = get_title_and_hashtags(args.video_file) | ||
| 77 | + video_file = args.video_file | ||
| 78 | + | ||
| 79 | + if args.publish_type == 0: | ||
| 80 | + print("Uploading immediately...") | ||
| 81 | + publish_date = 0 | ||
| 82 | + else: | ||
| 83 | + print("Scheduling videos...") | ||
| 84 | + publish_date = parse_schedule(args.schedule) | ||
| 85 | + | ||
| 86 | + if args.platform == SOCIAL_MEDIA_DOUYIN: | ||
| 87 | + await douyin_setup(account_file, handle=False) | ||
| 88 | + app = DouYinVideo(title, video_file, tags, publish_date, account_file) | ||
| 89 | + elif args.platform == SOCIAL_MEDIA_TIKTOK: | ||
| 90 | + await tiktok_setup(account_file, handle=True) | ||
| 91 | + app = TiktokVideo(title, video_file, tags, publish_date, account_file) | ||
| 92 | + elif args.platform == SOCIAL_MEDIA_TENCENT: | ||
| 93 | + await weixin_setup(account_file, handle=True) | ||
| 94 | + category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传 | ||
| 95 | + app = TencentVideo(title, video_file, tags, publish_date, account_file, category) | ||
| 96 | + elif args.platform == SOCIAL_MEDIA_KUAISHOU: | ||
| 97 | + await ks_setup(account_file, handle=True) | ||
| 98 | + app = KSVideo(title, video_file, tags, publish_date, account_file) | ||
| 99 | + else: | ||
| 100 | + print("Wrong platform, please check your input") | ||
| 101 | + exit() | ||
| 102 | + | ||
| 103 | + await app.main() | ||
| 104 | + | ||
| 105 | + | ||
| 106 | +if __name__ == "__main__": | ||
| 107 | + asyncio.run(main()) |
conf.py
0 → 100644
media/20231009111131.png
0 → 100644
86.7 KB
media/20231009111214.png
0 → 100644
35.2 KB
media/QR.png
0 → 100644
116 KB
media/get_bili_cookie.png
0 → 100644
41.3 KB
media/group-qr.png
0 → 100644
275 KB
media/mp.jpg
0 → 100644
9.44 KB
media/show/pdf3.gif
0 → 100644
396 KB
media/show/tkupload.gif
0 → 100644
901 KB
media/tk_login.png
0 → 100644
98 KB
media/xhs_error_cookie.png
0 → 100644
29.8 KB
uploader/__init__.py
0 → 100644
uploader/bilibili_uploader/__init__.py
0 → 100644
uploader/bilibili_uploader/biliup.exe
0 → 100644
This file is too large to display.
uploader/bilibili_uploader/main.py
0 → 100644
| 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 |
uploader/douyin_uploader/__init__.py
0 → 100644
uploader/douyin_uploader/main.py
0 → 100644
This diff is collapsed. Click to expand it.
uploader/ks_uploader/__init__.py
0 → 100644
uploader/ks_uploader/main.py
0 → 100644
| 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 | + context.on("close", lambda: context.storage_state(path=self.account_file)) | ||
| 93 | + | ||
| 94 | + # 创建一个新的页面 | ||
| 95 | + page = await context.new_page() | ||
| 96 | + # 访问指定的 URL | ||
| 97 | + await page.goto("https://cp.kuaishou.com/article/publish/video") | ||
| 98 | + kuaishou_logger.info('正在上传-------{}.mp4'.format(self.title)) | ||
| 99 | + # 等待页面跳转到指定的 URL,没进入,则自动等待到超时 | ||
| 100 | + kuaishou_logger.info('正在打开主页...') | ||
| 101 | + await page.wait_for_url("https://cp.kuaishou.com/article/publish/video") | ||
| 102 | + # 点击 "上传视频" 按钮 | ||
| 103 | + upload_button = page.locator("button[class^='_upload-btn']") | ||
| 104 | + await upload_button.wait_for(state='visible') # 确保按钮可见 | ||
| 105 | + | ||
| 106 | + async with page.expect_file_chooser() as fc_info: | ||
| 107 | + await upload_button.click() | ||
| 108 | + file_chooser = await fc_info.value | ||
| 109 | + await file_chooser.set_files(self.file_path) | ||
| 110 | + | ||
| 111 | + await asyncio.sleep(2) | ||
| 112 | + | ||
| 113 | + # if not await page.get_by_text("封面编辑").count(): | ||
| 114 | + # raise Exception("似乎没有跳转到到编辑页面") | ||
| 115 | + | ||
| 116 | + await asyncio.sleep(1) | ||
| 117 | + | ||
| 118 | + # 等待按钮可交互 | ||
| 119 | + new_feature_button = page.locator('button[type="button"] span:text("我知道了")') | ||
| 120 | + if await new_feature_button.count() > 0: | ||
| 121 | + await new_feature_button.click() | ||
| 122 | + | ||
| 123 | + kuaishou_logger.info("正在填充标题和话题...") | ||
| 124 | + await page.get_by_text("描述").locator("xpath=following-sibling::div").click() | ||
| 125 | + kuaishou_logger.info("clear existing title") | ||
| 126 | + await page.keyboard.press("Backspace") | ||
| 127 | + await page.keyboard.press("Control+KeyA") | ||
| 128 | + await page.keyboard.press("Delete") | ||
| 129 | + kuaishou_logger.info("filling new title") | ||
| 130 | + await page.keyboard.type(self.title) | ||
| 131 | + await page.keyboard.press("Enter") | ||
| 132 | + | ||
| 133 | + # 快手只能添加3个话题 | ||
| 134 | + for index, tag in enumerate(self.tags[:3], start=1): | ||
| 135 | + kuaishou_logger.info("正在添加第%s个话题" % index) | ||
| 136 | + await page.keyboard.type(f"#{tag} ") | ||
| 137 | + await asyncio.sleep(2) | ||
| 138 | + | ||
| 139 | + max_retries = 60 # 设置最大重试次数,最大等待时间为 2 分钟 | ||
| 140 | + retry_count = 0 | ||
| 141 | + | ||
| 142 | + while retry_count < max_retries: | ||
| 143 | + try: | ||
| 144 | + # 获取包含 '上传中' 文本的元素数量 | ||
| 145 | + number = await page.locator("text=上传中").count() | ||
| 146 | + | ||
| 147 | + if number == 0: | ||
| 148 | + kuaishou_logger.success("视频上传完毕") | ||
| 149 | + break | ||
| 150 | + else: | ||
| 151 | + if retry_count % 5 == 0: | ||
| 152 | + kuaishou_logger.info("正在上传视频中...") | ||
| 153 | + await asyncio.sleep(2) | ||
| 154 | + except Exception as e: | ||
| 155 | + kuaishou_logger.error(f"检查上传状态时发生错误: {e}") | ||
| 156 | + await asyncio.sleep(2) # 等待 2 秒后重试 | ||
| 157 | + retry_count += 1 | ||
| 158 | + | ||
| 159 | + if retry_count == max_retries: | ||
| 160 | + kuaishou_logger.warning("超过最大重试次数,视频上传可能未完成。") | ||
| 161 | + | ||
| 162 | + # 定时任务 | ||
| 163 | + if self.publish_date != 0: | ||
| 164 | + await self.set_schedule_time(page, self.publish_date) | ||
| 165 | + | ||
| 166 | + # 判断视频是否发布成功 | ||
| 167 | + while True: | ||
| 168 | + try: | ||
| 169 | + publish_button = page.get_by_text("发布", exact=True) | ||
| 170 | + if await publish_button.count() > 0: | ||
| 171 | + await publish_button.click() | ||
| 172 | + | ||
| 173 | + await asyncio.sleep(1) | ||
| 174 | + confirm_button = page.get_by_text("确认发布") | ||
| 175 | + if await confirm_button.count() > 0: | ||
| 176 | + await confirm_button.click() | ||
| 177 | + | ||
| 178 | + # 等待页面跳转,确认发布成功 | ||
| 179 | + await page.wait_for_url( | ||
| 180 | + "https://cp.kuaishou.com/article/manage/video?status=2&from=publish", | ||
| 181 | + timeout=5000, | ||
| 182 | + ) | ||
| 183 | + kuaishou_logger.success("视频发布成功") | ||
| 184 | + break | ||
| 185 | + except Exception as e: | ||
| 186 | + kuaishou_logger.info(f"视频正在发布中... 错误: {e}") | ||
| 187 | + await page.screenshot(full_page=True) | ||
| 188 | + await asyncio.sleep(1) | ||
| 189 | + | ||
| 190 | + await context.storage_state(path=self.account_file) # 保存cookie | ||
| 191 | + kuaishou_logger.info('cookie更新完毕!') | ||
| 192 | + await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看 | ||
| 193 | + # 关闭浏览器上下文和浏览器实例 | ||
| 194 | + await context.close() | ||
| 195 | + await browser.close() | ||
| 196 | + | ||
| 197 | + async def main(self): | ||
| 198 | + async with async_playwright() as playwright: | ||
| 199 | + await self.upload(playwright) | ||
| 200 | + | ||
| 201 | + async def set_schedule_time(self, page, publish_date): | ||
| 202 | + kuaishou_logger.info("click schedule") | ||
| 203 | + publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M:%S") | ||
| 204 | + await page.locator("label:text('发布时间')").locator('xpath=following-sibling::div').locator( | ||
| 205 | + '.ant-radio-input').nth(1).click() | ||
| 206 | + await asyncio.sleep(1) | ||
| 207 | + | ||
| 208 | + await page.locator('div.ant-picker-input input[placeholder="选择日期时间"]').click() | ||
| 209 | + await asyncio.sleep(1) | ||
| 210 | + | ||
| 211 | + await page.keyboard.press("Control+KeyA") | ||
| 212 | + await page.keyboard.type(str(publish_date_hour)) | ||
| 213 | + await page.keyboard.press("Enter") | ||
| 214 | + await asyncio.sleep(1) |
uploader/tencent_uploader/__init__.py
0 → 100644
uploader/tencent_uploader/main.py
0 → 100644
This diff is collapsed. Click to expand it.
uploader/tk_uploader/__init__.py
0 → 100644
uploader/tk_uploader/main.py
0 → 100644
This diff is collapsed. Click to expand it.
uploader/tk_uploader/main_chrome.py
0 → 100644
This diff is collapsed. Click to expand it.
uploader/tk_uploader/tk_config.py
0 → 100644
uploader/wyh_uploader/__init__.py
0 → 100644
File mode changed
uploader/wyh_uploader/main.py
0 → 100644
| 1 | +import os | ||
| 2 | + | ||
| 3 | +from utils.base_social_media import set_init_script | ||
| 4 | +from utils.files_times import get_absolute_path | ||
| 5 | +from utils.log import kuaishou_logger | ||
| 6 | +from playwright.async_api import Playwright, async_playwright | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +async def wyh_setup(account_file, handle=False): | ||
| 10 | + print(account_file) | ||
| 11 | + account_file = get_absolute_path(account_file, "wyh_uploader") | ||
| 12 | + if not os.path.exists(account_file): | ||
| 13 | + if not handle: | ||
| 14 | + return False | ||
| 15 | + kuaishou_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') | ||
| 16 | + await get_wyh_cookie(account_file) | ||
| 17 | + else: | ||
| 18 | + await open_wyh_main_page(account_file) | ||
| 19 | + return True | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +async def open_wyh_main_page(account_file): | ||
| 23 | + async with async_playwright() as playwright: | ||
| 24 | + options = { | ||
| 25 | + 'args': [ | ||
| 26 | + '--lang en-GB' | ||
| 27 | + ], | ||
| 28 | + 'headless': False, # Set headless option here | ||
| 29 | + } | ||
| 30 | + # Make sure to run headed. | ||
| 31 | + browser = await playwright.chromium.launch(**options) | ||
| 32 | + # Setup context however you like. | ||
| 33 | + context = await browser.new_context(storage_state=account_file) # Pass any options | ||
| 34 | + context = await set_init_script(context) | ||
| 35 | + # Pause the page, and start recording manually. | ||
| 36 | + page = await context.new_page() | ||
| 37 | + await page.goto("https://mp.163.com") | ||
| 38 | + | ||
| 39 | + board = page.locator("//*[@class='homeV4__board__card__data__value']") | ||
| 40 | + # html_content = await board.inner_html() | ||
| 41 | + # print(html_content) | ||
| 42 | + content = await board.nth(0).inner_text() #总粉丝数 | ||
| 43 | + print(content) | ||
| 44 | + content = await board.nth(1).inner_text() #总阅读数 | ||
| 45 | + print(content) | ||
| 46 | + content = await board.nth(2).inner_text() #总收益 | ||
| 47 | + print(content) | ||
| 48 | + | ||
| 49 | + await page.get_by_text("粉丝数据").click() | ||
| 50 | + await page.wait_for_timeout(5000) | ||
| 51 | + await page.get_by_text("内容数据").click() | ||
| 52 | + await page.wait_for_timeout(5000) | ||
| 53 | + await page.get_by_text("收益数据").click() | ||
| 54 | + await page.wait_for_timeout(5000) | ||
| 55 | + browser.close() | ||
| 56 | + | ||
| 57 | + | ||
| 58 | +async def get_wyh_cookie(account_file): | ||
| 59 | + print("get_wyh_cookie") | ||
| 60 | + async with async_playwright() as playwright: | ||
| 61 | + options = { | ||
| 62 | + 'args': [ | ||
| 63 | + '--lang en-GB' | ||
| 64 | + ], | ||
| 65 | + 'headless': False, # Set headless option here | ||
| 66 | + } | ||
| 67 | + # Make sure to run headed. | ||
| 68 | + browser = await playwright.chromium.launch(**options) | ||
| 69 | + # Setup context however you like. | ||
| 70 | + context = await browser.new_context() # Pass any options | ||
| 71 | + context = await set_init_script(context) | ||
| 72 | + # Pause the page, and start recording manually. | ||
| 73 | + page = await context.new_page() | ||
| 74 | + await page.goto("https://mp.163.com") | ||
| 75 | + # await page.pause() | ||
| 76 | + # 点击调试器的继续,保存cookie | ||
| 77 | + # await context.storage_state(path=account_file) | ||
| 78 | + | ||
| 79 | + # 自动登陆 | ||
| 80 | + await page.wait_for_timeout(20000) | ||
| 81 | + frame = page.frame_locator('//iframe[contains(@id, "x-URS-iframe")]') | ||
| 82 | + await frame.locator('[name="email"]').fill('liufuhua007@163.com') | ||
| 83 | + await frame.locator('[name="password"]').fill("Liuyihong1023@") | ||
| 84 | + await frame.locator('#dologin').click() | ||
| 85 | + | ||
| 86 | + # await page.pause() | ||
| 87 | + await page.wait_for_timeout(15000) | ||
| 88 | + # 点击调试器的继续,保存cookie | ||
| 89 | + await context.storage_state(path=account_file) |
uploader/xhs_uploader/__init__.py
0 → 100644
File mode changed
uploader/xhs_uploader/accounts.ini
0 → 100644
uploader/xhs_uploader/main.py
0 → 100644
| 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)) |
uploader/xhs_uploader/xhs_login_qrcode.py
0 → 100644
| 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)) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
utils/__init__.py
0 → 100644
File mode changed
utils/base_social_media.py
0 → 100644
| 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 | +SOCIAL_MEDIA_WANGYIHAO = 'wangyihao' | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +def get_supported_social_media() -> List[str]: | ||
| 15 | + return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU,SOCIAL_MEDIA_WANGYIHAO] | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +def get_cli_action() -> List[str]: | ||
| 19 | + return ["upload", "login", "watch"] | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +async def set_init_script(context): | ||
| 23 | + stealth_js_path = Path(BASE_DIR / "utils/stealth.min.js") | ||
| 24 | + await context.add_init_script(path=stealth_js_path) | ||
| 25 | + return context |
utils/constant.py
0 → 100644
| 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 |
utils/files_times.py
0 → 100644
| 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, 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 |
utils/log.py
0 → 100644
| 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"<fg #70acde>{{time:YYYY-MM-DD HH:mm:ss}}</fg #70acde> | <fg {color}>{{level}}</fg {color}>: <light-white>{{message}}</light-white>\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') |
utils/stealth.min.js
0 → 100644
This diff could not be displayed because it is too large.
videos/demo.mp4
0 → 100644
No preview for this file type
videos/demo.png
0 → 100644
103 KB
videos/demo.txt
0 → 100644
-
Please register or login to post a comment