liufuhua007

add code

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
......
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())
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

9.44 KB

1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies" / "bilibili_uploader").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
This file is too large to display.
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
1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies" / "douyin_uploader").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
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():
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
119 + while True:
120 + # 判断是是否进入视频发布页面,没进入,则自动等待到超时
121 + try:
122 + await page.wait_for_url(
123 + "https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page")
124 + break
125 + except:
126 + douyin_logger.info(f' [-] 正在等待进入视频发布页面...')
127 + await asyncio.sleep(0.1)
128 +
129 + # 填充标题和话题
130 + # 检查是否存在包含输入框的元素
131 + # 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素
132 + await asyncio.sleep(1)
133 + douyin_logger.info(f' [-] 正在填充标题和话题...')
134 + title_container = page.get_by_text('作品标题').locator("..").locator("xpath=following-sibling::div[1]").locator("input")
135 + if await title_container.count():
136 + await title_container.fill(self.title[:30])
137 + else:
138 + titlecontainer = page.locator(".notranslate")
139 + await titlecontainer.click()
140 + await page.keyboard.press("Backspace")
141 + await page.keyboard.press("Control+KeyA")
142 + await page.keyboard.press("Delete")
143 + await page.keyboard.type(self.title)
144 + await page.keyboard.press("Enter")
145 + css_selector = ".zone-container"
146 + for index, tag in enumerate(self.tags, start=1):
147 + await page.type(css_selector, "#" + tag)
148 + await page.press(css_selector, "Space")
149 + douyin_logger.info(f'总共添加{len(self.tags)}个话题')
150 +
151 + while True:
152 + # 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待
153 + try:
154 + # 新版:定位重新上传
155 + number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count()
156 + if number > 0:
157 + douyin_logger.success(" [-]视频上传完毕")
158 + break
159 + else:
160 + douyin_logger.info(" [-] 正在上传视频中...")
161 + await asyncio.sleep(2)
162 +
163 + if await page.locator('div.progress-div > div:has-text("上传失败")').count():
164 + douyin_logger.error(" [-] 发现上传出错了... 准备重试")
165 + await self.handle_upload_error(page)
166 + except:
167 + douyin_logger.info(" [-] 正在上传视频中...")
168 + await asyncio.sleep(2)
169 +
170 + #上传视频封面
171 + await self.set_thumbnail(page, self.thumbnail_path)
172 +
173 + # 更换可见元素
174 + await self.set_location(page, "杭州市")
175 +
176 + # 頭條/西瓜
177 + third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
178 + # 定位是否有第三方平台
179 + if await page.locator(third_part_element).count():
180 + # 检测是否是已选中状态
181 + if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'):
182 + await page.locator(third_part_element).locator('input.semi-switch-native-control').click()
183 +
184 + if self.publish_date != 0:
185 + await self.set_schedule_time_douyin(page, self.publish_date)
186 +
187 + # 判断视频是否发布成功
188 + while True:
189 + # 判断视频是否发布成功
190 + try:
191 + publish_button = page.get_by_role('button', name="发布", exact=True)
192 + if await publish_button.count():
193 + await publish_button.click()
194 + await page.wait_for_url("https://creator.douyin.com/creator-micro/content/manage**",
195 + timeout=3000) # 如果自动跳转到作品页面,则代表发布成功
196 + douyin_logger.success(" [-]视频发布成功")
197 + break
198 + except:
199 + douyin_logger.info(" [-] 视频正在发布中...")
200 + await page.screenshot(full_page=True)
201 + await asyncio.sleep(0.5)
202 +
203 + await context.storage_state(path=self.account_file) # 保存cookie
204 + douyin_logger.success(' [-]cookie更新完毕!')
205 + await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
206 + # 关闭浏览器上下文和浏览器实例
207 + await context.close()
208 + await browser.close()
209 +
210 + async def set_thumbnail(self, page: Page, thumbnail_path: str):
211 + if thumbnail_path:
212 + await page.click('text="选择封面"')
213 + await page.wait_for_selector("div.semi-modal-content:visible")
214 + await page.click('text="设置竖封面"')
215 + await page.wait_for_timeout(2000) # 等待2秒
216 + # 定位到上传区域并点击
217 + await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path)
218 + await page.wait_for_timeout(2000) # 等待2秒
219 + await page.locator("div[class^='extractFooter'] button:visible:has-text('完成')").click()
220 + # finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')")
221 + # if await finish_confirm_element.count():
222 + # await finish_confirm_element.click()
223 + # await page.locator("div[class^='footer'] button:has-text('完成')").click()
224 +
225 + async def set_location(self, page: Page, location: str = "杭州市"):
226 + # todo supoort location later
227 + # await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator(
228 + # "div.semi-select-single").nth(0).click()
229 + await page.locator('div.semi-select span:has-text("输入地理位置")').click()
230 + await page.keyboard.press("Backspace")
231 + await page.wait_for_timeout(2000)
232 + await page.keyboard.type(location)
233 + await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000)
234 + await page.locator('div[role="listbox"] [role="option"]').first.click()
235 +
236 + async def main(self):
237 + async with async_playwright() as playwright:
238 + await self.upload(playwright)
239 +
240 +
1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies" / "ks_uploader").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
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)
1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies" / "tencent_uploader").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
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=1500)
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)
1 +from pathlib import Path
2 +
3 +from conf import BASE_DIR
4 +
5 +Path(BASE_DIR / "cookies" / "tk_uploader").mkdir(exist_ok=True)
...\ No newline at end of file ...\ No newline at end of file
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 +
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()
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(500) # 等待500毫秒
131 + await self.locator_base.locator(hour_selector).click()
132 + # click time button again
133 + await page.wait_for_timeout(500) # 等待500毫秒
134 + await scheduled_picker.locator('div.TUXInputBox').nth(0).click()
135 + await page.wait_for_timeout(500) # 等待500毫秒
136 + # pick minutes after
137 + await scheduled_picker.locator('div.TUXInputBox').nth(0).click()
138 + await page.wait_for_timeout(500) # 等待500毫秒
139 + await self.locator_base.locator(minute_selector).click()
140 +
141 + # click title to remove the focus.
142 + # await self.locator_base.locator("h1:has-text('Upload video')").click()
143 +
144 + async def handle_upload_error(self, page):
145 + tiktok_logger.info("video upload error retrying.")
146 + select_file_button = self.locator_base.locator('button[aria-label="Select file"]')
147 + async with page.expect_file_chooser() as fc_info:
148 + await select_file_button.click()
149 + file_chooser = await fc_info.value
150 + await file_chooser.set_files(self.file_path)
151 +
152 + async def upload(self, playwright: Playwright) -> None:
153 + browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path)
154 + context = await browser.new_context(storage_state=f"{self.account_file}")
155 + context = await set_init_script(context)
156 + page = await context.new_page()
157 +
158 + # change language to eng first
159 + await self.change_language(page)
160 + await page.goto("https://www.tiktok.com/tiktokstudio/upload")
161 + tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4')
162 +
163 + await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000)
164 +
165 + try:
166 + await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000)
167 + tiktok_logger.info("Either iframe or div appeared.")
168 + except Exception as e:
169 + tiktok_logger.error("Neither iframe nor div appeared within the timeout.")
170 +
171 + await self.choose_base_locator(page)
172 +
173 + upload_button = self.locator_base.locator(
174 + 'button:has-text("Select video"):visible')
175 + await upload_button.wait_for(state='visible') # 确保按钮可见
176 +
177 + async with page.expect_file_chooser() as fc_info:
178 + await upload_button.click()
179 + file_chooser = await fc_info.value
180 + await file_chooser.set_files(self.file_path)
181 +
182 + await self.add_title_tags(page)
183 + # detect upload status
184 + await self.detect_upload_status(page)
185 + if self.thumbnail_path:
186 + tiktok_logger.info(f'[+] Uploading thumbnail file {self.title}.png')
187 + await self.upload_thumbnails(page)
188 +
189 + if self.publish_date != 0:
190 + await self.set_schedule_time(page, self.publish_date)
191 +
192 + await self.click_publish(page)
193 +
194 + await context.storage_state(path=f"{self.account_file}") # save cookie
195 + tiktok_logger.info(' [-] update cookie!')
196 + await asyncio.sleep(2) # close delay for look the video status
197 + # close all
198 + await context.close()
199 + await browser.close()
200 +
201 + async def add_title_tags(self, page):
202 +
203 + editor_locator = self.locator_base.locator('div.public-DraftEditor-content')
204 + await editor_locator.click()
205 +
206 + await page.keyboard.press("End")
207 +
208 + await page.keyboard.press("Control+A")
209 +
210 + await page.keyboard.press("Delete")
211 +
212 + await page.keyboard.press("End")
213 +
214 + await page.wait_for_timeout(1000) # 等待1秒
215 +
216 + await page.keyboard.insert_text(self.title)
217 + await page.wait_for_timeout(1000) # 等待1秒
218 + await page.keyboard.press("End")
219 +
220 + await page.keyboard.press("Enter")
221 +
222 + # tag part
223 + for index, tag in enumerate(self.tags, start=1):
224 + tiktok_logger.info("Setting the %s tag" % index)
225 + await page.keyboard.press("End")
226 + await page.wait_for_timeout(1000) # 等待1秒
227 + await page.keyboard.insert_text("#" + tag + " ")
228 + await page.keyboard.press("Space")
229 + await page.wait_for_timeout(1000) # 等待1秒
230 +
231 + await page.keyboard.press("Backspace")
232 + await page.keyboard.press("End")
233 +
234 + async def upload_thumbnails(self, page):
235 + await self.locator_base.locator(".cover-container").click()
236 + await self.locator_base.locator(".cover-edit-container >> text=Upload cover").click()
237 + async with page.expect_file_chooser() as fc_info:
238 + await self.locator_base.locator(".upload-image-upload-area").click()
239 + file_chooser = await fc_info.value
240 + await file_chooser.set_files(self.thumbnail_path)
241 + await self.locator_base.locator('div.cover-edit-panel:not(.hide-panel)').get_by_role(
242 + "button", name="Confirm").click()
243 + await page.wait_for_timeout(3000) # wait 3s, fix it later
244 +
245 + async def change_language(self, page):
246 + # set the language to english
247 + await page.goto("https://www.tiktok.com")
248 + await page.wait_for_url("https://www.tiktok.com/", timeout=100000)
249 + await page.wait_for_selector('#header-more-menu-icon')
250 +
251 + await page.locator('#header-more-menu-icon').hover()
252 + await page.locator('[data-e2e="language-select"]').click()
253 + await page.locator('#lang-setting-popup-list >> text=English').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 self.locator_base.locator(success_flag_div).wait_for(state="visible", timeout=3000)
264 + tiktok_logger.success(" [-] video published success")
265 + break
266 + except Exception as e:
267 + if await self.locator_base.locator(success_flag_div).count():
268 + tiktok_logger.success(" [-]video published success")
269 + break
270 + else:
271 + tiktok_logger.exception(f" [-] Exception: {e}")
272 + tiktok_logger.info(" [-] video publishing")
273 + await page.screenshot(full_page=True)
274 + await asyncio.sleep(0.5)
275 +
276 + async def detect_upload_status(self, page):
277 + while True:
278 + try:
279 + # if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None:
280 + if await self.locator_base.locator(
281 + 'div.button-group > button >> text=Post').get_attribute("disabled") is None:
282 + tiktok_logger.info(" [-]video uploaded.")
283 + break
284 + else:
285 + tiktok_logger.info(" [-] video uploading...")
286 + await asyncio.sleep(2)
287 + if await self.locator_base.locator(
288 + 'button[aria-label="Select file"]').count():
289 + tiktok_logger.info(" [-] found some error while uploading now retry...")
290 + await self.handle_upload_error(page)
291 + except:
292 + tiktok_logger.info(" [-] video uploading...")
293 + await asyncio.sleep(2)
294 +
295 + async def choose_base_locator(self, page):
296 + # await page.wait_for_selector('div.upload-container')
297 + if await page.locator('iframe[data-tt="Upload_index_iframe"]').count():
298 + self.locator_base = page.frame_locator(Tk_Locator.tk_iframe)
299 + else:
300 + self.locator_base = page.locator(Tk_Locator.default)
301 +
302 + async def main(self):
303 + async with async_playwright() as playwright:
304 + await self.upload(playwright)
1 +
2 +class Tk_Locator(object):
3 + tk_iframe = '[data-tt="Upload_index_iframe"]'
4 + default = 'body'
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)
1 +[account1]
2 +cookies = changeme
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))
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
File mode changed
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
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
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
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')
This diff could not be displayed because it is too large.
No preview for this file type
1 +这位勇敢的男子为了心爱之人每天坚守 🥺❤️‍🩹
2 +#坚持不懈 #爱情执着 #奋斗使者 #短视频
...\ No newline at end of file ...\ No newline at end of file