diff --git a/wxsph/main.py b/wxsph/main.py new file mode 100644 index 0000000..ff40707 --- /dev/null +++ b/wxsph/main.py @@ -0,0 +1,628 @@ +import hashlib +import os +import random +import time + +import cv2 +from PIL import Image +from io import BytesIO + +import requests +import json +import uuid + +from moviepy.video.io.VideoFileClip import VideoFileClip + + +class Auth: + def __init__(self, cookies): + self.cookies = cookies + self.headers = { + "Host": "channels.weixin.qq.com", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "sec-ch-ua-platform": "\"Windows\"", + "X-WECHAT-UIN": "0000000000", + "sec-ch-ua": "\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"", + "sec-ch-ua-mobile": "?0", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Origin": "https://channels.weixin.qq.com", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Referer": "https://channels.weixin.qq.com/platform/", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8" + } + + def get_rid(self): + timestamp_hex = format(int(time.time()), "x") + random_hex = "".join(format(random.randint(0, 15), "x") for _ in range(8)) + + result = f"{timestamp_hex}-{random_hex}" + # print(result) + return result + + def get_aid(self): + return str(uuid.uuid4()) + + def get_v2(self): + url = "https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/auth/auth_data" + params = { + "_aid": self.get_aid(), + "_rid": self.get_rid(), + "_pageUrl": "https://channels.weixin.qq.com/platform/login-for-iframe" + } + data = { + "timestamp": str(int(time.time() * 1000)), + "_log_finder_uin": "", + "_log_finder_id": "", + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + data = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data).json() + print(response) + return response['data']['finderUser']['finderUsername'] + + def get_auth(self, ): + self.v2 = self.get_v2() + + url = "https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_upload_params" + params = { + "_aid": self.get_aid(), + "_rid": self.get_rid(), + "_pageUrl": "https://channels.weixin.qq.com/platform/" + } + data = { + "timestamp": str(int(time.time() * 1000)), + "_log_finder_uin": "", + "_log_finder_id": self.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + data = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data).json() + authKey = response['data']['authKey'] + print('authkey--', authKey) + return authKey + + +class WxSphImage(object): + def __init__(self, cookies): + self.auto = Auth(cookies) + self.authorization = self.auto.get_auth() # 获取鉴权 + self.cookies = cookies + self.image_file = '' # 图片地址 + self.image_data = b'' # 图片内容 + self.image_size = 0 # 图片长度 + self.headers = {} + self.width, self.height = 0, 0 + self.image_file = '' + self.width, self.height, self.duration = 0, 0, 0 + + def md5(self, data): + if type(data) == str: + data = data.encode('utf-8') + return hashlib.md5(data).hexdigest() + + def get_image_data(self): + with open(self.image_file, 'rb') as f: + data = f.read() + return data + + def get_ts(self): + return str(int(time.time() * 1000)) + + def get_headers(self): + headers = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Authorization": self.authorization, + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-MD5": "null", + "Content-Type": "application/json", + "Origin": "https://channels.weixin.qq.com", + "Pragma": "no-cache", + "Referer": "https://channels.weixin.qq.com/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + "X-Arguments": f"apptype=251&filetype=20304&weixinnum=2841261318&filekey=finder_video_img.jpeg&filesize={self.image_size}&taskid={self.auto.get_aid()}&scene=2", + "sec-ch-ua": "\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"" + } + return headers + + def get_uid(self, BlockPartLength): + """ + 获取要上传的图片地址 + :return: + """ + url = "https://finderassistancea.video.qq.com/applyuploaddfs" + data = { + "BlockSum": len(BlockPartLength), + "BlockPartLength": BlockPartLength + } + data = json.dumps(data, separators=(',', ':')) + response = requests.put(url, headers=self.headers, data=data).json() + print(response) + return response['UploadID'] + + def put_image(self, uid, image_data=None, PartNumber=1): + """ + 上传图片/视频 + PartNumber 这个是上传视频用的 1是第一段 2是第二 + image_data 为了适配视频上传 + :return: + """ + # url = "https://finderassistancec.video.qq.com/uploadpartdfs?" + url = 'https://finderassistancee.video.qq.com/uploadpartdfs?' + params = {'PartNumber': PartNumber, + 'UploadID': uid, + 'QuickUpload': 2 + } + + if image_data is None: + image_data = self.image_data + headers = self.headers + else: + + headers = self.get_headers() + headers[ + "X-Arguments"] = f"apptype=251&filetype=20302&weixinnum=2841261318&filekey=aa.mp4&filesize={self.image_size}&taskid={self.auto.get_aid()}&scene=2" + headers['content-md5'] = self.md5(image_data) + response = requests.put(url, data=image_data, headers=headers, params=params).json() + print(response) + + return response['ETag'], response['TransFlag'] + + def get_image_url(self, uid, etag='', trans_flag='0_0', part_info=[]): + """ + 获取图片地址 + :param etag: + :param trans_flag: + :param uid: + :return: + """ + url = "https://finderassistancea.video.qq.com/completepartuploaddfs" + # https://finderassistancea.video.qq.com/completepartuploaddfs? + params = { + "UploadID": uid + } + data = { + "TransFlag": trans_flag, + "PartInfo": [] + } + if not part_info: + data['PartInfo'] = [ + { + "PartNumber": 1, + "ETag": etag + } + ] + + else: + data['PartInfo'] = part_info + + data = json.dumps(data, separators=(',', ':')) + print(data) + response = requests.post(url, headers=self.headers, params=params, data=data).json() + print(response) + return response['DownloadURL'] + + def get_traceKey(self): + """ + 这个是为了获取发布视频的 traceKey + :return: + """ + url = "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/get-finder-post-trace-key" + params = { + '_aid': self.auto.get_aid(), + '_rid': self.auto.get_rid(), + "_pageUrl": "https://channels.weixin.qq.com/micro/content/post/finderNewLifeCreate" + } + data = { + "objectId": "", + "timestamp": self.get_ts(), + "_log_finder_uin": "", + "_log_finder_id": self.auto.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + data = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=cookies, params=params, data=data).json() + print(response) + traceKey = response['data']['traceKey'] + + return traceKey + + def get_clip_key(self, video_url, traceKey, width, height, duration): + """ + 这个用来获取发布视频的参数 + :return: + """ + + url = "https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_clip_video" + params = { + "_aid": self.auto.get_aid(), + "_rid": self.auto.get_rid(), + "_pageUrl": "https://channels.weixin.qq.com/micro/content/post/create" + } + data = { + "url": video_url, + "timeStart": 0, + "cropDuration": 0, + "height": 1280, + "width": width, + "x": 0, + "y": 0, + "clipOriginVideoInfo": { + "width": width, + "height": height, + "duration": duration, + "fileSize": self.image_size + }, + "traceInfo": { + "traceKey": traceKey, + "uploadCdnStart": int(time.time()), + "uploadCdnEnd": int(time.time()) + }, + "targetWidth": width, + "targetHeight": height, + "type": 4, + "useAstraThumbCover": 1, + "timestamp": self.get_ts(), + "_log_finder_uin": "", + "_log_finder_id": self.auto.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + data = json.dumps(data, separators=(',', ':')) + response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data).json() + + print(response) + return response['data']['clipKey'] + + def release(self, title, connect, media): + """ + 发布 + :param title: 内容头 + :param connect: 内容 + :param media: 图片的列表 + :return: + """ + url = 'https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_create' + params = { + '_aid': self.auto.get_aid(), + '_rid': self.auto.get_rid(), + '_pageUrl': 'https://channels.weixin.qq.com/micro/content/post/finderNewLifeCreate' + } + traceKey = self.get_traceKey() + data = { + "objectType": 0, + "longitude": 0, + "latitude": 0, + "feedLongitude": 0, + "feedLatitude": 0, + "originalFlag": 0, + "topics": [], + "isFullPost": 1, + "handleFlag": 2, + "videoClipTaskId": "", + "traceInfo": { + "traceKey": traceKey, + "uploadCdnStart": int(time.time()), + "uploadCdnEnd": int(time.time()) + }, + "objectDesc": { + "mpTitle": "", + "description": connect, + "extReading": {}, + "mediaType": 2, + "location": { + "latitude": 31.992259979248047, + "longitude": 118.77870178222656, + "city": "南京市", + "poiClassifyId": "" + }, + "topic": { + "finderTopicInfo": "11" + }, + "event": {}, + "mentionedUser": [], + "media": media, + "finderNewlifeDesc": { + "richTextTitle": title, + "richTextJson": '[{"insert":"' + title + '"},{"attributes":{"header":1},"insert":"\\n"},{"insert":"' + connect + '"},{"insert":"\\n\\n"}]', + "fromRichPublisher": 1 + }, + "member": {} + }, + "postFlag": 0, + "mode": 1, + "clientid": self.auto.get_aid(), + "timestamp": self.get_ts(), + "_log_finder_uin": "", + "_log_finder_id": self.auto.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + + # 5. 发送POST请求 + response = requests.post( + url=url, + params=params, + headers=self.headers, + cookies=cookies, + json=data, # 确保中文正常传输 + timeout=30 # 设置超时时间 + ) + print(response.json()) + + def release_video(self, title, connect, video_url, im_url): + """ + 发布 + :param title: 内容头 + :param connect: 内容 + :param media: 图片的列表 + :return: + """ + url = 'https://channels.weixin.qq.com/micro/content/cgi-bin/mmfinderassistant-bin/post/post_create' + params = { + '_aid': self.auto.get_aid(), + '_rid': self.auto.get_rid(), + '_pageUrl': 'https://channels.weixin.qq.com/micro/content/post/finderNewLifeCreate' + } + traceKey = self.get_traceKey() + + width = self.width + height = self.height + duration = self.duration + clip = self.get_clip_key(video_url=video_url, traceKey=traceKey, width=width, height=height, duration=duration) + print(clip) + data = { + "objectType": 0, + "longitude": 0, + "latitude": 0, + "feedLongitude": 0, + "feedLatitude": 0, + "originalFlag": 0, + "topics": [], + "isFullPost": 1, + "handleFlag": 2, + "videoClipTaskId": clip, + "traceInfo": { + "traceKey": traceKey, + "uploadCdnStart": int(time.time()), + "uploadCdnEnd": int(time.time()), + }, + "objectDesc": { + "mpTitle": "", + "description": title, + "extReading": {}, + "mediaType": 4, + "location": { + "latitude": 31.992259979248047, + "longitude": 118.77870178222656, + "city": "南京市", + "poiClassifyId": "" + }, + "topic": { + "finderTopicInfo": "11" + }, + "event": {}, + "mentionedUser": [], + "media": [ + { + "url": video_url, + "fileSize": self.image_size, + "thumbUrl": im_url, + "fullThumbUrl": im_url, + "mediaType": 4, + "videoPlayLen": int(duration), + "width": width, + "height": height, + "md5sum": self.auto.get_aid(), + "coverUrl": im_url, + "fullCoverUrl": im_url, + "urlCdnTaskId": clip + } + ], + "shortTitle": [{"shortTitle": connect}], + "member": {} + }, + "report": { + "clipKey": clip, + "draftId": clip, + "timestamp": self.get_ts(), + "_log_finder_uin": "", + "_log_finder_id": self.auto.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7, + "height": height, + "width": width, + "duration": duration, + "fileSize": self.image_size, + "uploadCost": 268 + }, + "postFlag": 0, + "mode": 1, + "clientid": self.auto.get_aid(), + "timestamp": self.get_ts(), + "_log_finder_uin": "", + "_log_finder_id": self.auto.v2, + "rawKeyBuff": None, + "pluginSessionId": None, + "scene": 7, + "reqScene": 7 + } + + # 5. 发送POST请求 + response = requests.post( + url=url, + params=params, + headers=self.headers, + cookies=cookies, + json=data, # 确保中文正常传输 + timeout=30 # 设置超时时间 + ).json() + print(response) + return response + + def run_images(self, image_file): + self.image_file = image_file # 图片地址 + self.image_data = self.get_image_data() # 图片内容 + self.image_size = len(self.image_data) # 图片长度 + self.headers = self.get_headers() + img = Image.open(BytesIO(self.image_data)) + self.width, self.height = img.size + uid = self.get_uid([ + self.image_size + ]) + etag, trans_flag = self.put_image(uid) + image_url = self.get_image_url(uid=uid, etag=etag, trans_flag=trans_flag) # 这里官方是上传了3个不同尺寸的 测试一样的也可以 + # thumbUrl = self.get_image_url(uid=uid, etag=etag, trans_flag=trans_flag) + # fullThumbUrl = self.get_image_url(uid=uid, etag=etag, trans_flag=trans_flag) + media = { + "url": image_url, + "fileSize": self.image_size, + "thumbUrl": image_url, + "fullThumbUrl": image_url, + "mediaType": 2, + "videoPlayLen": 0, + "width": self.width, + "height": self.height, + "md5sum": self.auto.get_aid(), + "urlCdnTaskId": "" + } + return media + # self.release(title='下午好', connect='我是阿巴阿巴阿巴', media=media) + + def video_cover(self, video_path): + cap = cv2.VideoCapture(video_path) + # 获取视频信息 + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = cap.get(cv2.CAP_PROP_FPS) + frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) + + duration = frame_count / fps + + print("宽高:", width, height) + print("时长:", duration) + self.width, self.height, self.duration = width, height, duration + # 读取第1秒帧 + cap.set(cv2.CAP_PROP_POS_MSEC, 1000) + + ret, frame = cap.read() + + cap.release() + + if ret: + # OpenCV是BGR,需要转RGB + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + img = Image.fromarray(frame) + + img.save("cover.png") + + def run_video(self, video_path): + """ + 这里发现视频和图片上传一致就共用了 + :param video_path: + :return: + """ + + self.image_file = video_path # 视频地址 + self.image_data = self.get_image_data() # 视频内容 + self.image_size = len(self.image_data) # 视频长度 + self.headers = self.get_headers() + self.headers[ + "X-Arguments"] = f"apptype=251&filetype=20302&weixinnum=2841261318&filekey=aa.mp4&filesize={self.image_size}&taskid={self.auto.get_aid()}&scene=2" + # apptype=251&filetype=20302&weixinnum=2841261318&filekey=_1.mp4&filesize=122544202&taskid=436451aa-f4fe-4a50-81be-4616bdb08c6c&scene=2 + chunk_size = 1024 * 1024 * 8 # 这个是上传视频的最大值 + if self.width == 0: + self.video_cover(video_path) + else: + print('已有视频数据,不再次获取') + BlockPartLength = [] # 每个视频大小存放 + if self.image_size > chunk_size: + for start in range(0, self.image_size, chunk_size): + end = min(start + chunk_size, self.image_size) + BlockPartLength.append(end) + else: + BlockPartLength.append(len(self.image_data)) + # print(BlockPartLength) + uid = self.get_uid(BlockPartLength=BlockPartLength) + start = 0 + part_info = [] + trans_flag = '0_0' + for index, end in enumerate(BlockPartLength): + chunk = self.image_data[start:end] + print(index, start, end, len(chunk)) + etag, trans_flag = self.put_image(uid, image_data=chunk, PartNumber=index + 1) + start = end + part_info.append({ + "PartNumber": index + 1, + "ETag": etag + }) + video_url = self.get_image_url(uid=uid, part_info=part_info, trans_flag=trans_flag) + return video_url + + +def start(cookie): + wxsph_image = WxSphImage(cookie) + media = [] + for i in os.listdir('data'): + r = wxsph_image.run_images(f'data\{i}') + media.append(r) + # wxsph_image.release(title='下午好', connect='我是大鹅,大白鹅', media=media) + + +def start2(cookie, path='bb.mp4'): + wxsph_image = WxSphImage(cookie) + wxsph_image.video_cover(path) + media = wxsph_image.run_images("cover.png") + im_url = media['url'] + video_url = wxsph_image.run_video(path) + wxsph_image.release_video(title='护肤小妙招', connect='零零零零', video_url=video_url, im_url=im_url) + + +if __name__ == '__main__': + cookies = { + "sessionid": 'BgAAqTVttQ7sIhon30QFGwPOpNPlQIpVNuqwUrd3lCCvLzrBkAEkEyuk1nI3zfGoMe0Qd38FvByNaq1%2FTWIaXwoiH7apYb6usnTCPewc2ucN', + "wxuin": "1256030655" + } + # start(cookies) + start2(cookies) + +# https://channels.weixin.qq.com/platform/post/finderNewLifeCreate +""" +原图 1440*1920 + 810*1080 + 1440*1920 + + + 1438 * + +http://wxapp.tc.qq.com/251/20302/stodownload?bizid=1023&dotrans=0&encfilekey=Cvvj5Ix3eewK0tHtibORqcsqchXNh0Gf3sJcaYqC2rQAj7FSjPM3xflXxKHA63ZOrTibicJ9v7u7J4fYdgdOOMAySBURcTZ7sSROXeYhcJkA0NMVcbicDq8iaPQnqVGj2zIxb&findertoken=0886eae8ca0a10a4dea4cd061800223c66696e64657275706c6f616475726c5f323834313236313331385f313737323639353333323636305f333531383737313532323839353535323834312a2039393936353864633535653261366137643662393866303232316264363533613801400348005000580260ce9e01&hy=SH&idx=1&m=&scene=2&token=AxricY7RBHdW6dqda02zm3HiaicQibGV1ib8zfD9icC3T9A8c6t4zN9VWAM7nZtvAtt4oAqMqEvxj9KoFTiaYFBtQP5UC5BmmDWK37LTFiabH3m6rhq8sp8XWWaibicg&uzid=7a15c +http://wxapp.tc.qq.com/251/20304/stodownload?bizid=1023&dotrans=0&encfilekey=Cvvj5Ix3eewK0tHtibORqcsqchXNh0Gf3sJcaYqC2rQAgkKWLMfzK1J50xmbjOvs8wYzYKbvibicABqEW4zyA0bsuHYIdDdRYMuGz1dU2OyuO6WXBrYKveY2gDRZkv84t2A&hy=SH&idx=1&m=&scene=2&token=x5Y29zUxcibBpSHJLicjXJ6R7YDibFkMzwCNXu1DAa7W7jvkodt2kjXmsYYrnWNuBc35NgDetCXmeTxBexibwoDiaITKay4FZwicy6ich6DvGhZQMHTGCo1RANhxQ&uzid=1 +http://wxapp.tc.qq.com/251/20304/stodownload?bizid=1023&dotrans=0&encfilekey=Cvvj5Ix3eewK0tHtibORqcsqchXNh0Gf3sJcaYqC2rQAgkKWLMfzK1J50xmbjOvs8wYzYKbvibicADoD7ptve2Vn5jFhCMOJRfXkekcy1lTcR3oOYKHZGibZOf2jicTMCEWM5&hy=SH&idx=1&m=&scene=2&token=x5Y29zUxcibBpSHJLicjXJ6QtAnlI3LxaeKdyJpZocIvkRYdXaYYp1iaNnm3H3rMb4cTeAaCc7EUmmx9Mqic7rMRYicuza3e6yibBjyQYRgGtE9FnMicSibFrNZScA&uzid=1 +"""