pookz@stme commited on
Commit
516e545
·
1 Parent(s): 396256f

1. add tencent video uploader

Browse files
examples/upload_video_to_tencent_video.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ from conf import BASE_DIR
5
+ from tencent_uploader.main import weixin_setup, TencentVideo
6
+ from utils.constant import TencentZoneTypes
7
+ from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
8
+
9
+
10
+ if __name__ == '__main__':
11
+ filepath = Path(BASE_DIR) / "videos"
12
+ account_file = Path(BASE_DIR / "tencent_uploader" / "account.json")
13
+ # 获取视频目录
14
+ folder_path = Path(filepath)
15
+ # 获取文件夹中的所有文件
16
+ files = list(folder_path.glob("*.mp4"))
17
+ file_num = len(files)
18
+ publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
19
+ cookie_setup = asyncio.run(weixin_setup(account_file, handle=True))
20
+ category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传
21
+ for index, file in enumerate(files):
22
+ title, tags = get_title_and_hashtags(str(file))
23
+ # 打印视频文件名、标题和 hashtag
24
+ print(f"视频文件名:{file}")
25
+ print(f"标题:{title}")
26
+ print(f"Hashtag:{tags}")
27
+ app = TencentVideo(title, file, tags, publish_datetimes[index], account_file, category)
28
+ asyncio.run(app.main(), debug=False)
tencent_uploader/__init__.py ADDED
File without changes
tencent_uploader/main.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from datetime import datetime
4
+
5
+ from playwright.async_api import Playwright, async_playwright
6
+ import os
7
+ import asyncio
8
+
9
+ from utils.files_times import get_absolute_path
10
+
11
+
12
+ def format_str_for_short_title(origin_title: str) -> str:
13
+ # 定义允许的特殊字符
14
+ allowed_special_chars = "《》“”:+?%°"
15
+
16
+ # 移除不允许的特殊字符
17
+ filtered_chars = [char if char.isalnum() or char in allowed_special_chars else ' ' if char == ',' else '' for
18
+ char in origin_title]
19
+ formatted_string = ''.join(filtered_chars)
20
+
21
+ # 调整字符串长度
22
+ if len(formatted_string) > 16:
23
+ # 截断字符串
24
+ formatted_string = formatted_string[:16]
25
+ elif len(formatted_string) < 6:
26
+ # 使用空格来填充字符串
27
+ formatted_string += ' ' * (6 - len(formatted_string))
28
+
29
+ return formatted_string
30
+
31
+
32
+ async def cookie_auth(account_file):
33
+ async with async_playwright() as playwright:
34
+ browser = await playwright.chromium.launch(headless=True)
35
+ context = await browser.new_context(storage_state=account_file)
36
+ # 创建一个新的页面
37
+ page = await context.new_page()
38
+ # 访问指定的 URL
39
+ await page.goto("https://channels.weixin.qq.com/platform/post/create")
40
+ try:
41
+ await page.wait_for_selector('div.title-name:has-text("视频号小店")', timeout=5000) # 等待5秒
42
+ print("[+] 等待5秒 cookie 失效")
43
+ return False
44
+ except:
45
+ print("[+] cookie 有效")
46
+ return True
47
+
48
+
49
+ async def save_storage_state(account_file: str):
50
+ async with async_playwright() as p:
51
+ browser = await p.chromium.launch(headless=False)
52
+ context = await browser.new_context()
53
+ page = await context.new_page()
54
+ await page.goto("https://channels.weixin.qq.com")
55
+ print("请在浏览器中扫码登录...")
56
+ await asyncio.sleep(20) # 给用户60秒时间进行扫码登录
57
+
58
+ # 保存存储状态到文件
59
+ storage_state = await context.storage_state()
60
+ with open(account_file, 'w') as f:
61
+ f.write(json.dumps(storage_state))
62
+ await browser.close()
63
+
64
+
65
+ async def weixin_setup(account_file, handle=False):
66
+ account_file = get_absolute_path(account_file, "tencent_uploader")
67
+ if not os.path.exists(account_file) or not await cookie_auth(account_file):
68
+ if not handle:
69
+ # Todo alert message
70
+ return False
71
+ print('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件')
72
+ # await save_storage_state(account_file)
73
+ os.system('python3 -m playwright install')
74
+ os.system(f'playwright codegen channels.weixin.qq.com --save-storage={account_file}') # 生成cookie文件
75
+ return True
76
+
77
+
78
+ class TencentVideo(object):
79
+ def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None):
80
+ self.title = title # 视频标题
81
+ self.file_path = file_path
82
+ self.tags = tags
83
+ self.publish_date = publish_date
84
+ self.account_file = account_file
85
+ self.category = category
86
+ self.local_executable_path = "C:/Program Files/Google/Chrome/Application/chrome.exe"
87
+ self.date_format = '%Y年%m月%d日 %H:%M'
88
+
89
+ async def set_schedule_time_tencent(self, page, publish_date):
90
+ print("click schedule")
91
+
92
+ label_element = page.locator("label").filter(has_text="定时").nth(1)
93
+ await label_element.click()
94
+
95
+ await page.click('input[placeholder="请选择发表时间"]')
96
+
97
+ current_month = str(publish_date.month) + "月"
98
+ # 获取当前的月份
99
+ page_month = await page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")')
100
+
101
+ # 检查当前月份是否与目标月份相同
102
+ if page_month != current_month:
103
+ await page.click('button.weui-desktop-btn__icon__right')
104
+
105
+ # 获取页面元素
106
+ elements = await page.query_selector_all('table.weui-desktop-picker__table a')
107
+
108
+ # 遍历元素并点击匹配的元素
109
+ for element in elements:
110
+ if 'weui-desktop-picker__disabled' in await element.evaluate('el => el.className'):
111
+ continue
112
+ text = await element.inner_text()
113
+ if text.strip() == str(publish_date.day):
114
+ await element.click()
115
+ break
116
+
117
+ # 输入小时部分(假设选择11小时)
118
+ await page.click('input[placeholder="请选择时间"]')
119
+ await page.keyboard.press("Control+KeyA")
120
+ await page.keyboard.type(str(publish_date.hour))
121
+
122
+ # 选择标题栏(令定时时间生效)
123
+ await page.locator("div.input-editor").click()
124
+
125
+ async def handle_upload_error(self, page):
126
+ print("视频出错了,重新上传中")
127
+ await page.locator('div.media-status-content div.tag-inner:has-text("删除")').click()
128
+ await page.get_by_role('button', name="删除", exact=True).click()
129
+ file_input = page.locator('input[type="file"]')
130
+ await file_input.set_input_files(self.file_path)
131
+
132
+ async def upload(self, playwright: Playwright) -> None:
133
+ # 使用 Chromium (这里使用系统内浏览器,用chromium 会造成h264错误
134
+ browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path)
135
+ # 创建一个浏览器上下文,使用指定的 cookie 文件
136
+ context = await browser.new_context(storage_state=f"{self.account_file}")
137
+
138
+ # 创建一个新的页面
139
+ page = await context.new_page()
140
+ # 访问指定的 URL
141
+ await page.goto("https://channels.weixin.qq.com/platform/post/create")
142
+ print('[+]正在上传-------{}.mp4'.format(self.title))
143
+ # 等待页面跳转到指定的 URL,没进入,则自动等待到超时
144
+ await page.wait_for_url("https://channels.weixin.qq.com/platform/post/create")
145
+ # await page.wait_for_selector('input[type="file"]', timeout=10000)
146
+ file_input = page.locator('input[type="file"]')
147
+ await file_input.set_input_files(self.file_path)
148
+ # 填充标题和话题
149
+ await self.add_title_tags(page)
150
+ # 添加商品
151
+ # await self.add_product(page)
152
+ # 合集功能
153
+ await self.add_collection(page)
154
+ # 原创选择
155
+ await self.add_original(page)
156
+ # 检测上传状态
157
+ await self.detact_upload_status(page)
158
+ if self.publish_date != 0:
159
+ await self.set_schedule_time_tencent(page, self.publish_date)
160
+ # 添加短标题
161
+ await self.add_short_title(page)
162
+
163
+ await self.click_publish(page)
164
+
165
+ await context.storage_state(path=f"{self.account_file}") # 保存cookie
166
+ print(' [-]cookie更新完毕!')
167
+ await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
168
+ # 关闭浏览器上下文和浏览器实例
169
+ await context.close()
170
+ await browser.close()
171
+ print('[+]正在监控执行计划中.......')
172
+
173
+ async def add_short_title(self, page):
174
+ short_title_element = page.get_by_text("短标题", exact=True).locator("..").locator(
175
+ "xpath=following-sibling::div").locator(
176
+ 'span input[type="text"]')
177
+ if await short_title_element.count():
178
+ short_title = format_str_for_short_title(self.title)
179
+ await short_title_element.fill(short_title)
180
+
181
+ async def click_publish(self, page):
182
+ while True:
183
+ try:
184
+ publish_buttion = page.locator('div.form-btns button:has-text("发表")')
185
+ if await publish_buttion.count():
186
+ await publish_buttion.click()
187
+ await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=1500)
188
+ print(" [-]视频发布成功")
189
+ break
190
+ except Exception as e:
191
+ current_url = page.url
192
+ if "https://channels.weixin.qq.com/platform/post/list" in current_url:
193
+ print(" [-]视频发布成功")
194
+ break
195
+ else:
196
+ print(f" [-] Exception: {e}")
197
+ print(" [-] 视频正在发布中...")
198
+ await page.screenshot(full_page=True)
199
+ await asyncio.sleep(0.5)
200
+
201
+ async def detact_upload_status(self, page):
202
+ while True:
203
+ # 匹配删除按钮,代表视频上传完毕,如果不存在,代表视频正在上传,则等待
204
+ try:
205
+ # 匹配删除按钮,代表视频上传完毕
206
+ if "weui-desktop-btn_disabled" not in await page.get_by_role("button", name="发表").get_attribute(
207
+ 'class'):
208
+ print(" [-]视频上传完毕")
209
+ break
210
+ else:
211
+ print(" [-] 正在上传视频中...")
212
+ await asyncio.sleep(2)
213
+ # 出错了视频出错
214
+ if await page.locator('div.status-msg.error').count() and await page.locator(
215
+ 'div.media-status-content div.tag-inner:has-text("删除")').count():
216
+ print(" [-] 发现上传出错了...")
217
+ await self.handle_upload_error(page)
218
+ except:
219
+ print(" [-] 正在上传视频中...")
220
+ await asyncio.sleep(2)
221
+
222
+ async def add_title_tags(self, page):
223
+ await page.locator("div.input-editor").click()
224
+ await page.keyboard.type(self.title)
225
+ await page.keyboard.press("Enter")
226
+ for index, tag in enumerate(self.tags, start=1):
227
+ await page.keyboard.type("#" + tag)
228
+ await page.keyboard.press("Space")
229
+ print(f"成功添加hashtag: {len(self.tags)}")
230
+
231
+ async def add_collection(self, page):
232
+ collection_elements = page.get_by_text("添加到合集").locator("xpath=following-sibling::div").locator(
233
+ '.option-list-wrap > div')
234
+ if await collection_elements.count() > 1:
235
+ await page.get_by_text("添加到合集").locator("xpath=following-sibling::div").click()
236
+ await collection_elements.first.click()
237
+
238
+ async def add_original(self, page):
239
+ if await page.get_by_label("视频为原创").count():
240
+ await page.get_by_label("视频为原创").check()
241
+ # 检查 "我已阅读并同意 《视频号原创声明使用条款》" 元素是否存在
242
+ label_locator = await page.locator('label:has-text("我已阅读并同意 《视频号原创声明使用条款》")').is_visible()
243
+ if label_locator:
244
+ await page.get_by_label("我已阅读并同意 《视频号原创声明使用条款》").check()
245
+ await page.get_by_role("button", name="声明原创").click()
246
+ # 2023年11月20日 wechat更新: 可能新账号或者改版账号,出现新的选择页面
247
+ if await page.locator('div.label span:has-text("声明原创")').count() and self.category:
248
+ # 因处罚无法勾选原创,故先判断是否可用
249
+ if not await page.locator('div.declare-original-checkbox input.ant-checkbox-input').is_disabled():
250
+ await page.locator('div.declare-original-checkbox input.ant-checkbox-input').click()
251
+ if not await page.locator(
252
+ 'div.declare-original-dialog label.ant-checkbox-wrapper.ant-checkbox-wrapper-checked:visible').count():
253
+ await page.locator('div.declare-original-dialog input.ant-checkbox-input:visible').click()
254
+ if await page.locator('div.original-type-form > div.form-label:has-text("原创类型"):visible').count():
255
+ await page.locator('div.form-content:visible').click() # 下拉菜单
256
+ await page.locator(
257
+ f'div.form-content:visible ul.weui-desktop-dropdown__list li.weui-desktop-dropdown__list-ele:has-text("{self.category}")').first.click()
258
+ await asyncio.sleep(1)
259
+ if await page.locator('button:has-text("声明原创"):visible').count():
260
+ await page.locator('button:has-text("声明原创"):visible').click()
261
+
262
+ async def main(self):
263
+ async with async_playwright() as playwright:
264
+ await self.upload(playwright)
utils/constant.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = '科技'