496 lines
21 KiB
Python
496 lines
21 KiB
Python
# coding = utf-8
|
||
#!/usr/bin/python
|
||
import re
|
||
import sys
|
||
import json
|
||
import time
|
||
import base64
|
||
import hashlib
|
||
import random
|
||
import string
|
||
import urllib.parse
|
||
from Crypto.Cipher import AES
|
||
from Crypto.Util.Padding import pad, unpad
|
||
from Crypto.PublicKey import RSA
|
||
from Crypto.Cipher import PKCS1_v1_5
|
||
from base.spider import Spider
|
||
|
||
sys.path.append('..')
|
||
|
||
class Spider(Spider):
|
||
def __init__(self):
|
||
self.name = "瓜子"
|
||
self.hosts = [
|
||
'https://apinew.uozvr.com',
|
||
'https://api.w32z7vtd.com',
|
||
'https://api.6a7nnf7.com',
|
||
'https://api.umygrx3.com',
|
||
'https://api.rmedphk.com'
|
||
]
|
||
self.host_index = 0
|
||
self.host = self.hosts[self.host_index]
|
||
|
||
# AES 固定密钥(与Java版一致)
|
||
self.AES_KEY = 'OITxa5OqAYjhswxx'
|
||
self.AES_IV = 'rCMNwZASNBKZ8mXV'
|
||
|
||
# RSA 公钥/私钥
|
||
self.RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUM5+/y8sPsWkd1/RQS64X259EUwxFXFE5HlA65MqrxnPs0JqoSRojSDy5QhwvROlaD6TwRQHKMY2OAZ6SnQeUJsChTEFIR9qUkwrs3/MVUMxjsv6JS6Oe/juclyJGTgVmDhB55EafXsD0SQYVj/QXXsxR6ewR5E2kL52yAAD4yQIDAQAB"
|
||
self.RSA_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGAe6hKrWLi1zQmjTT1
|
||
ozbE4QdFeJGNxubxld6GrFGximxfMsMB6BpJhpcTouAqywAFppiKetUBBbXwYsYU
|
||
1wNr648XVmPmCMCy4rY8vdliFnbMUj086DU6Z+/oXBdWU3/b1G0DN3E9wULRSwcK
|
||
ZT3wj/cCI1vsCm3gj2R5SqkA9Y0CAwEAAQKBgAJH+4CxV0/zBVcLiBCHvSANm0l7
|
||
HetybTh/j2p0Y1sTXro4ALwAaCTUeqdBjWiLSo9lNwDHFyq8zX90+gNxa7c5EqcW
|
||
V9FmlVXr8VhfBzcZo1nXeNdXFT7tQ2yah/odtdcx+vRMSGJd1t/5k5bDd9wAvYdI
|
||
DblMAg+wiKKZ5KcdAkEA1cCakEN4NexkF5tHPRrR6XOY/XHfkqXxEhMqmNbB9U34
|
||
saTJnLWIHC8IXys6Qmzz30TtzCjuOqKRRy+FMM4TdwJBAJQZFPjsGC+RqcG5UvVM
|
||
iMPhnwe/bXEehShK86yJK/g/UiKrO87h3aEu5gcJqBygTq3BBBoH2md3pr/W+hUM
|
||
WBsCQQChfhTIrdDinKi6lRxrdBnn0Ohjg2cwuqK5zzU9p/N+S9x7Ck8wUI53DKm8
|
||
jUJE8WAG7WLj/oCOWEh+ic6NIwTdAkEAj0X8nhx6AXsgCYRql1klbqtVmL8+95KZ
|
||
K7PnLWG/IfjQUy3pPGoSaZ7fdquG8bq8oyf5+dzjE/oTXcByS+6XRQJAP/5ciy1b
|
||
L3NhUhsaOVy55MHXnPjdcTX0FaLi+ybXZIfIQ2P4rb19mVq1feMbCXhz+L1rG8oa
|
||
t5lYKfpe8k83ZA==
|
||
-----END RSA PRIVATE KEY-----"""
|
||
|
||
self.DEVICE_OLD_KEY = "aLFBMWpxBrIDAD1Si/KVvm41"
|
||
|
||
# 设备信息(随机生成)
|
||
self.deviceId = str(864150060000000 + random.randint(0, 9999))
|
||
self.deviceKey = ''.join(random.choices('0123456789ABCDEF', k=40)) # 20字节hex大写
|
||
self.token = ""
|
||
self.token_id = ""
|
||
self.registered = False
|
||
|
||
self.header = {
|
||
'User-Agent': 'Lavf/57.83.100',
|
||
'code': 'GZ0369',
|
||
'deviceId': self.deviceId,
|
||
'lang': 'zh_cn',
|
||
'Cache-Control': 'no-cache',
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'Version': '2604028',
|
||
'PackageName': 'com.ae06aebdbb.y286327f5a.ofe849883320260517',
|
||
'Ver': '3.0.3.2',
|
||
'api-ver': '3.0.3.2',
|
||
'Referer': self.host
|
||
}
|
||
|
||
self.cache = {}
|
||
self.cache_timeout = 300
|
||
|
||
# 初始化token
|
||
self.init_token()
|
||
|
||
def getName(self):
|
||
return self.name
|
||
|
||
def init(self, extend=''):
|
||
pass
|
||
|
||
# ---------- 设备注册与认证 ----------
|
||
def init_token(self):
|
||
"""初始化token:注册设备 -> 刷新"""
|
||
print("===== 初始化设备认证 =====")
|
||
try:
|
||
if not self.registered:
|
||
self.sign_up()
|
||
# 刷新获取最终token
|
||
self.refresh_token()
|
||
except Exception as e:
|
||
print(f"初始化token失败: {e}")
|
||
# 兜底使用原有硬编码(几乎没用)
|
||
self.token = '024212ef0975c5306a1434e113a46463.bc77313e11a248558a6ca244ca980944ec3421fa480c50e0229ad91f1cb15aea582603202cd71796885c9e5163e500f1b72f737059aff1ddb8beea47c5a331d6760540345b7f88b2302a0e6e09589f9dcf3ff9175d8c905f990203f5fc04748008ea7a366571cbf5b09509a873dcfba3cf1d5590385f5f7ef6e01d1850974aa220eb5178c89e61c24411af9b9a19435e.06fde789ece48d9b33c5dc857e04e9b5838f08264d928b87237d3476c4484b46'
|
||
|
||
def sign_up(self):
|
||
"""注册设备"""
|
||
print("注册新设备...")
|
||
params = {
|
||
"new_key": self.deviceKey,
|
||
"old_key": self.DEVICE_OLD_KEY,
|
||
"phone_type": 1,
|
||
"code": ""
|
||
}
|
||
result = self._auth_request('/App/Authentication/Device/signUp', params)
|
||
self._apply_auth(result)
|
||
self.registered = True
|
||
|
||
def sign_in(self):
|
||
"""登录设备"""
|
||
print("设备登录...")
|
||
params = {
|
||
"new_key": self.deviceKey,
|
||
"old_key": self.DEVICE_OLD_KEY
|
||
}
|
||
result = self._auth_request('/App/Authentication/Device/signIn', params)
|
||
self._apply_auth(result)
|
||
|
||
def _apply_auth(self, result):
|
||
"""从认证响应中提取token"""
|
||
new_token = result.get('token', '')
|
||
if not new_token:
|
||
raise Exception("认证失败,无token返回: {}".format(result))
|
||
self.token = new_token
|
||
new_token_id = result.get('app_user_id', '')
|
||
if new_token_id:
|
||
self.token_id = new_token_id
|
||
print(f"获取token成功, token前缀: {self.token[:30]}...")
|
||
|
||
def refresh_token(self):
|
||
"""刷新token"""
|
||
print("刷新token...")
|
||
result = self._auth_request('/App/Authentication/Authenticator/refresh', {})
|
||
self._apply_auth(result)
|
||
|
||
def _auth_request(self, path, params):
|
||
"""认证类请求(不需要ensure_token)"""
|
||
return self._send_encrypted_request(params, path, is_auth=True)
|
||
|
||
# ---------- 业务请求核心(修复加密与签名) ----------
|
||
def ensure_token(self):
|
||
"""确保token有效,如未就绪则重新获取"""
|
||
if not self.token or not self.token_id:
|
||
if self.registered:
|
||
self.sign_in()
|
||
else:
|
||
self.sign_up()
|
||
self.refresh_token()
|
||
|
||
def _send_encrypted_request(self, data, path, is_auth=False):
|
||
"""
|
||
发送加密请求,返回解密后的字典
|
||
:param data: 业务参数字典
|
||
:param path: 请求路径
|
||
:param is_auth: 是否为认证类请求(signUp/signIn/refresh),此时不使用ensure_token
|
||
"""
|
||
try:
|
||
if not is_auth:
|
||
self.ensure_token()
|
||
|
||
# 1. 将参数转为JSON并AES加密
|
||
json_params = json.dumps(data)
|
||
encrypted = self.aes_encrypt(json_params, self.AES_KEY, self.AES_IV)
|
||
request_key = encrypted.upper() # Java中是bytesToHex(encrypted).toUpperCase()
|
||
|
||
# 2. 生成keys (RSA加密 iv/key JSON)
|
||
key_json = json.dumps({"iv": self.AES_IV, "key": self.AES_KEY})
|
||
keys = self.rsa_encrypt(key_json, self.RSA_PUBLIC_KEY)
|
||
|
||
# 3. 生成签名
|
||
t = str(int(time.time()))
|
||
sign_str = f"token_id=,token={self.token},phone_type=1,request_key={request_key},app_id=1,time={t},keys={keys}*&zvdvdvddbfikkkumtmdwqppp?|4Y!s!2br"
|
||
signature = self.get_md5(sign_str) # 已改为大写
|
||
|
||
# 4. 构建请求体
|
||
body = {
|
||
'token': self.token,
|
||
'token_id': '',
|
||
'phone_type': '1',
|
||
'time': t,
|
||
'phone_model': 'xiaomi-25031', # 与Java版保持一致
|
||
'keys': keys,
|
||
'request_key': request_key,
|
||
'signature': signature,
|
||
'app_id': '1',
|
||
'ad_version': '1'
|
||
}
|
||
|
||
# 5. 发送请求
|
||
url = f"{self.host}{path}"
|
||
response = self.post(url, headers=self.header, data=body, timeout=10)
|
||
|
||
if response.status_code != 200:
|
||
raise Exception(f"HTTP {response.status_code}")
|
||
|
||
resp_json = response.json()
|
||
# 检查业务code(若不为200可能token过期)
|
||
if 'code' in resp_json and resp_json['code'] != 200:
|
||
print(f"业务错误码: {resp_json['code']}, 信息: {resp_json}")
|
||
# 如果不是认证请求,尝试重新获取token后重试一次(这里简单处理,外层get_data已有重试)
|
||
raise Exception("业务错误")
|
||
|
||
data_section = resp_json.get('data')
|
||
if not data_section:
|
||
raise Exception("响应缺少data字段")
|
||
|
||
encrypted_response = data_section.get('response_key', '')
|
||
encrypted_keys = data_section.get('keys', '')
|
||
|
||
# 6. 解密响应
|
||
decrypted_keys_json = self.rsa_decrypt(encrypted_keys, self.RSA_PRIVATE_KEY)
|
||
key_info = json.loads(decrypted_keys_json)
|
||
resp_key = key_info['key']
|
||
resp_iv = key_info['iv']
|
||
decrypted_data = self.aes_decrypt(encrypted_response, resp_key, resp_iv)
|
||
return json.loads(decrypted_data)
|
||
|
||
except Exception as e:
|
||
print(f"请求失败 [{path}]: {e}")
|
||
return None
|
||
|
||
def get_data(self, data, path, use_cache=True):
|
||
"""带重试和域名轮询的数据获取(保持原框架)"""
|
||
try:
|
||
cache_key = f"{path}_{hash(str(data))}" if use_cache else None
|
||
if use_cache and cache_key in self.cache:
|
||
cached_data, timestamp = self.cache[cache_key]
|
||
if time.time() - timestamp < self.cache_timeout:
|
||
return cached_data
|
||
|
||
for attempt in range(3):
|
||
tried = 0
|
||
while tried < len(self.hosts):
|
||
self.host = self.hosts[self.host_index]
|
||
self.header['Referer'] = self.host
|
||
result = self._send_encrypted_request(data, path)
|
||
if result is not None:
|
||
print(f"请求成功: {path}, 域名: {self.host}")
|
||
if use_cache and cache_key:
|
||
self.cache[cache_key] = (result, time.time())
|
||
return result
|
||
|
||
# 切换到下一个域名
|
||
self.host_index = (self.host_index + 1) % len(self.hosts)
|
||
tried += 1
|
||
|
||
# 所有域名失败,尝试重新认证并重试
|
||
if attempt < 2:
|
||
print("所有域名失败,尝试重新认证...")
|
||
try:
|
||
self.ensure_token()
|
||
except:
|
||
pass
|
||
self.host_index = 0
|
||
else:
|
||
break
|
||
return None
|
||
except Exception as e:
|
||
print(f"get_data异常: {e}")
|
||
return None
|
||
|
||
# ---------- 加解密工具 ----------
|
||
def aes_encrypt(self, text, key, iv):
|
||
try:
|
||
key_bytes = key.encode('utf-8')
|
||
iv_bytes = iv.encode('utf-8')
|
||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||
encrypted = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size))
|
||
return encrypted.hex().upper()
|
||
except Exception as e:
|
||
print(f"AES加密失败: {e}")
|
||
return ""
|
||
|
||
def aes_decrypt(self, text, key, iv):
|
||
try:
|
||
key_bytes = key.encode('utf-8')
|
||
iv_bytes = iv.encode('utf-8')
|
||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||
encrypted_bytes = bytes.fromhex(text)
|
||
decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
|
||
return decrypted.decode('utf-8')
|
||
except Exception as e:
|
||
print(f"AES解密失败: {e}")
|
||
return ""
|
||
|
||
def rsa_encrypt(self, text, public_key_str):
|
||
"""RSA公钥加密(PKCS1v1.5)"""
|
||
try:
|
||
key = RSA.import_key("-----BEGIN PUBLIC KEY-----\n" + public_key_str + "\n-----END PUBLIC KEY-----")
|
||
cipher = PKCS1_v1_5.new(key)
|
||
encrypted = cipher.encrypt(text.encode('utf-8'))
|
||
return base64.b64encode(encrypted).decode('utf-8')
|
||
except Exception as e:
|
||
print(f"RSA加密失败: {e}")
|
||
return ""
|
||
|
||
def rsa_decrypt(self, encrypted_data, private_key_str):
|
||
"""RSA私钥解密"""
|
||
try:
|
||
encrypted_bytes = base64.b64decode(encrypted_data)
|
||
rsa_key = RSA.import_key(private_key_str)
|
||
cipher = PKCS1_v1_5.new(rsa_key)
|
||
decrypted = cipher.decrypt(encrypted_bytes, None)
|
||
return decrypted.decode('utf-8') if decrypted else ""
|
||
except Exception as e:
|
||
print(f"RSA解密失败: {e}")
|
||
return ""
|
||
|
||
def get_md5(self, text):
|
||
return hashlib.md5(text.encode()).hexdigest().upper() # 与Java一致大写
|
||
|
||
# ---------- 业务方法(不变) ----------
|
||
def homeContent(self, filter):
|
||
result = {}
|
||
classes = [
|
||
{"type_name": "电影", "type_id": "1"},
|
||
{"type_name": "电视剧", "type_id": "2"},
|
||
{"type_name": "动漫", "type_id": "4"},
|
||
{"type_name": "综艺", "type_id": "3"},
|
||
{"type_name": "短剧", "type_id": "64"}
|
||
]
|
||
result['class'] = classes
|
||
filters = {}
|
||
for cate in classes:
|
||
tid = cate['type_id']
|
||
filters[tid] = [
|
||
{"key": "area", "name": "地区", "value": [
|
||
{"n": "全部", "v": "0"}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "法国", "v": "法国"},
|
||
{"n": "泰国", "v": "泰国"}, {"n": "印度", "v": "印度"}, {"n": "其他", "v": "其他"}
|
||
]},
|
||
{"key": "year", "name": "年份", "value": [
|
||
{"n": "全部", "v": "0"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
|
||
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "2021", "v": "2021"},
|
||
{"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"},
|
||
{"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"}, {"n": "2015", "v": "2015"},
|
||
{"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"},
|
||
{"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"},
|
||
{"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"},
|
||
{"n": "2005", "v": "2005"}, {"n": "更早", "v": "2004"}
|
||
]},
|
||
{"key": "sort", "name": "排序", "value": [
|
||
{"n": "最新", "v": "d_id"}, {"n": "最热", "v": "d_hits"}, {"n": "推荐", "v": "d_score"}
|
||
]}
|
||
]
|
||
result['filters'] = filters
|
||
return result
|
||
|
||
def homeVideoContent(self):
|
||
return {'list': []}
|
||
|
||
def categoryContent(self, tid, pg, filter, extend):
|
||
videos = []
|
||
try:
|
||
body = {
|
||
"area": extend.get('area', '0'),
|
||
"year": extend.get('year', '0'),
|
||
"pageSize": "30",
|
||
"sort": extend.get('sort', 'd_id'),
|
||
"page": str(pg),
|
||
"tid": tid
|
||
}
|
||
cache_key = f"category_{tid}_{pg}_{hash(str(body))}"
|
||
data = self.get_cached_data(cache_key, body, '/App/IndexList/indexList')
|
||
if data and 'list' in data:
|
||
for item in data['list']:
|
||
vod_continu = item.get('vod_continu', 0)
|
||
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集'
|
||
video = {
|
||
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
|
||
"vod_name": item.get('vod_name', ''),
|
||
"vod_pic": item.get('vod_pic', ''),
|
||
"vod_remarks": remarks
|
||
}
|
||
videos.append(video)
|
||
except Exception as e:
|
||
print(f"获取分类内容失败: {e}")
|
||
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
|
||
|
||
def detailContent(self, ids):
|
||
try:
|
||
vod_id = ids[0].split('/')[0]
|
||
t = str(int(time.time()))
|
||
body1 = {"token_id": self.token_id, "vod_id": vod_id, "mobile_time": t, "token": self.token}
|
||
qdata = self.get_data(body1, '/App/IndexPlay/playInfo')
|
||
body2 = {"vurl_cloud_id": "2", "vod_d_id": vod_id}
|
||
jdata = self.get_data(body2, '/App/Resource/Vurl/show')
|
||
if not qdata or 'vodInfo' not in qdata:
|
||
return {'list': []}
|
||
vod = qdata['vodInfo']
|
||
video_detail = {
|
||
"vod_id": vod_id,
|
||
"vod_name": vod.get('vod_name', ''),
|
||
"vod_pic": vod.get('vod_pic', ''),
|
||
"vod_year": vod.get('vod_year', ''),
|
||
"vod_area": vod.get('vod_area', ''),
|
||
"vod_actor": vod.get('vod_actor', ''),
|
||
"vod_director": vod.get('vod_director', ''),
|
||
"vod_content": vod.get('vod_use_content', '').strip(),
|
||
"vod_play_from": "瓜子影视"
|
||
}
|
||
play_list = []
|
||
if jdata and 'list' in jdata:
|
||
for index, item in enumerate(jdata['list']):
|
||
if 'play' in item:
|
||
n, p = [], []
|
||
for key, value in item['play'].items():
|
||
if 'param' in value and value['param']:
|
||
n.append(key)
|
||
p.append(value['param'])
|
||
if p:
|
||
play_name = str(index + 1) if len(jdata['list']) != 1 else vod.get('vod_name', '')
|
||
play_url = f"{p[-1]}||{'@'.join(n)}"
|
||
play_list.append(f"{play_name}${play_url}")
|
||
video_detail["vod_play_url"] = "#".join(play_list)
|
||
return {'list': [video_detail]}
|
||
except Exception as e:
|
||
print(f"获取详情失败: {e}")
|
||
return {'list': []}
|
||
|
||
def searchContent(self, key, quick, pg=1):
|
||
videos = []
|
||
try:
|
||
body = {"keywords": key, "order_val": "1", "page": str(pg)}
|
||
data = self.get_data(body, '/App/Index/findMoreVod', use_cache=False)
|
||
if data and 'list' in data:
|
||
for item in data['list']:
|
||
vod_continu = item.get('vod_continu', 0)
|
||
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集'
|
||
videos.append({
|
||
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
|
||
"vod_name": item.get('vod_name', ''),
|
||
"vod_pic": item.get('vod_pic', ''),
|
||
"vod_remarks": remarks
|
||
})
|
||
except Exception as e:
|
||
print(f"搜索失败: {e}")
|
||
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
|
||
|
||
def playerContent(self, flag, id, vipFlags):
|
||
try:
|
||
parts = id.split('||')
|
||
if len(parts) < 2:
|
||
return {"parse": 0, "playUrl": "", "url": ""}
|
||
param_str = parts[0]
|
||
resolutions = parts[1].split('@') if len(parts) > 1 else []
|
||
params = {}
|
||
for pair in param_str.split('&'):
|
||
if '=' in pair:
|
||
key, value = pair.split('=', 1)
|
||
params[key] = value
|
||
if resolutions:
|
||
resolutions.sort(key=lambda x: int(x) if x.isdigit() else 0, reverse=True)
|
||
params['resolution'] = resolutions[0]
|
||
data = self.get_data(params, '/App/Resource/VurlDetail/showOne', use_cache=False)
|
||
if data and 'url' in data:
|
||
return {"parse": 0, "playUrl": "", "url": data['url'],
|
||
"header": json.dumps({"User-Agent": "Lavf/57.83.100", "Referer": "http://WJiZxLXA2.com/"}), 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'}
|
||
return {"parse": 0, "playUrl": "", "url": ""}
|
||
except Exception as e:
|
||
print(f"播放解析失败: {e}")
|
||
return {"parse": 0, "playUrl": "", "url": ""}
|
||
|
||
def isVideoFormat(self, url):
|
||
video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts']
|
||
return any(url.lower().endswith(fmt) for fmt in video_formats)
|
||
|
||
def manualVideoCheck(self):
|
||
pass
|
||
|
||
def localProxy(self, params):
|
||
return None
|
||
|
||
def get_cached_data(self, cache_key, data, path):
|
||
current_time = time.time()
|
||
if cache_key in self.cache:
|
||
cached_data, timestamp = self.cache[cache_key]
|
||
if current_time - timestamp < self.cache_timeout:
|
||
return cached_data
|
||
result = self.get_data(data, path)
|
||
if result:
|
||
self.cache[cache_key] = (result, current_time)
|
||
return result
|
||
|
||
if __name__ == '__main__':
|
||
pass |