Sync all projects
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from urllib.parse import quote, unquote, urljoin, urlparse
|
||||
import re, sys, time, json, random, base64, hashlib, urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
headers,play_header,host,play_domain,encrypt_domain,proxyurl = {
|
||||
'User-Agent': "okhttp/4.9.0",
|
||||
'Connection': "Keep-Alive",
|
||||
'Accept-Encoding': "gzip",
|
||||
'log-header': "I am the log request header.",
|
||||
'app_id': "",
|
||||
'channel_code': "",
|
||||
'cur_time': "",
|
||||
'device_id': "",
|
||||
'mob_mfr': "huawei",
|
||||
'mobmodel': "DCO-AL00",
|
||||
'package_name': "",
|
||||
'sign': "",
|
||||
'sys_platform': "2",
|
||||
'sysrelease': "12",
|
||||
'token': "",
|
||||
'version': "40000"
|
||||
},{'User-Agent': 'Mozi'},'','','',''
|
||||
|
||||
def init(self, extend=''):
|
||||
try:
|
||||
ext = json.loads(extend.strip())
|
||||
host = ext.get('host').rstrip('/')
|
||||
if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host): return None
|
||||
appid = ext.get('app_id').strip()
|
||||
deviceid = ext.get('deviceid').strip()
|
||||
version_code = ext.get('versionCode').strip()
|
||||
channel = ext.get('UMENG_CHANNEL').strip()
|
||||
package_name = ext.get('package_name').strip()
|
||||
self.encrypt_domain = ext.get('EncryptDomain').strip()
|
||||
if not(appid and deviceid and version_code and channel and package_name and self.encrypt_domain): return None
|
||||
if not self.is_valid_android_id(deviceid): return None
|
||||
self.headers['app_id'] = appid
|
||||
self.headers['channel_code'] = channel
|
||||
self.headers['device_id'] = deviceid
|
||||
self.headers['version'] = version_code
|
||||
self.headers['package_name'] = package_name
|
||||
try:
|
||||
self.proxyurl = f'{self.getProxyUrl(True)}&type=xiaocao'
|
||||
except Exception:
|
||||
self.proxyurl = 'http://127.0.0.1:9978/proxy?do=py&type=xiaocao'
|
||||
self.login(host)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def homeContent(self, filter):
|
||||
if not self.host: return None
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
response = self.post(f'{self.host}/api/type/get_list', headers=headers, verify=False).text
|
||||
data = json.loads(self.decrypt(response))
|
||||
classes = [{'type_id': i['id'], 'type_name': i['name']} for i in data.get('result', [])]
|
||||
if not any(i['type_id'] == 31 for i in classes):
|
||||
classes.append({'type_id': 31, 'type_name': '短剧'})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
if not self.host: return None
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
try:
|
||||
response = self.post(f'{self.host}/api/channel/get_list', headers=headers,verify=False).text
|
||||
data = self.decrypt(response)
|
||||
result = json.loads(data)['result']
|
||||
classes, videos, recommend_id = [], [], ''
|
||||
for i in result:
|
||||
if isinstance(i, dict):
|
||||
if i.get('channel_name') == '推荐':
|
||||
recommend_id = i.get('id')
|
||||
break
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
except Exception:
|
||||
recommend_id = '324'
|
||||
payload2 = {
|
||||
'psize': "100",
|
||||
'channel_id': recommend_id,
|
||||
'pn': "1"
|
||||
}
|
||||
response2 = self.post(f'{self.host}/api/channel/get_info',data=payload2, headers=headers, verify=False).text
|
||||
data2 = self.decrypt(response2)
|
||||
result2 = json.loads(data2)['result']
|
||||
for item in result2:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for block in item.get('block_list', []):
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
for vod in block.get('vod_list', []):
|
||||
if vod['type_pid'] == 1:
|
||||
remark = f"评分:{vod['vod_douban_score']}"
|
||||
elif vod['type_pid'] != 2 and vod['type_pid'] != 4 and vod['type_pid'] != 31:
|
||||
remark = vod['collection_new_title']
|
||||
if vod['type_pid'] == 3:
|
||||
remark = self.remove_invalid_episode(remark)
|
||||
else:
|
||||
remark = f"{vod['vod_serial']}集全" if vod.get('vod_isend') == 1 else f"更新至{vod['vod_serial']}集"
|
||||
videos.append({
|
||||
'vod_id': vod['id'],
|
||||
'vod_name': vod['vod_name'],
|
||||
'vod_pic': vod['vod_pic'],
|
||||
'vod_remarks': remark,
|
||||
'vod_year': vod['vod_year']
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if not self.host: return None
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
payload = {
|
||||
'area': "",
|
||||
'year': "",
|
||||
'type_id': tid,
|
||||
'sort': "",
|
||||
'type': "",
|
||||
'pn': pg
|
||||
}
|
||||
response = self.post(f'{self.host}/api/search/screen', data=payload, headers=headers, verify=False).text
|
||||
data = json.loads(self.decrypt(response))
|
||||
videos = []
|
||||
for i in data.get('result',[]):
|
||||
if i['type_pid'] == 1:
|
||||
remark = f"评分:{i['vod_douban_score']}"
|
||||
elif i['type_pid'] != 2 and i['type_pid'] != 4 and i['type_pid'] != 31:
|
||||
remark = i['collection_new_title']
|
||||
if i['type_pid'] == 3:
|
||||
remark = self.remove_invalid_episode(remark)
|
||||
else:
|
||||
remark = f"{i['vod_serial']}集全" if i.get('vod_isend') == 1 else f"更新至{i['vod_serial']}集"
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['vod_name'],
|
||||
'vod_pic': i['vod_pic'],
|
||||
'vod_remarks': remark,
|
||||
'vod_year': i['vod_year']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def remove_invalid_episode(self, content):
|
||||
pattern = r'第(.*?)集'
|
||||
def replace_match(match):
|
||||
middle_content = match.group(1)
|
||||
return middle_content
|
||||
processed_content = re.sub(pattern, replace_match, content)
|
||||
return processed_content
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
if not self.host: return None
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
payload = {'kw': key,'pn': pg}
|
||||
response = self.post(f'{self.host}/api/search/result', data=payload, headers=headers, verify=False).text
|
||||
data = json.loads(self.decrypt(response))
|
||||
videos = []
|
||||
for i in data.get('result',[]):
|
||||
if i['type_pid'] == 1:
|
||||
remark = f"评分:{i['vod_douban_score']}"
|
||||
elif i['type_pid'] != 2 and i['type_pid'] != 4 and i['type_pid'] != 31:
|
||||
remark = i['collection_new_title']
|
||||
if i['type_pid'] == 3:
|
||||
remark = self.remove_invalid_episode(remark)
|
||||
else:
|
||||
vod_serial = i['vod_serial']
|
||||
remark = f"{i['vod_tag']},短剧" if i['type_pid'] == 31 else f"{vod_serial}集全" if i.get('vod_isend') == 1 else f"更新至{vod_serial}集"
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['vod_name'],
|
||||
'vod_pic': i['vod_pic'],
|
||||
'vod_remarks': remark,
|
||||
'vod_year': i['vod_year']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not self.host: return None
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
payload = {
|
||||
'sig': '',
|
||||
'nc_token': '',
|
||||
'code': '',
|
||||
'phone': '',
|
||||
'vod_id': ids[0],
|
||||
'session_id': ''
|
||||
}
|
||||
response = self.post(f'{self.host}/api/video/result', data=payload, headers=headers, verify=False).text
|
||||
data = self.decrypt(response)
|
||||
result = json.loads(data)['result']
|
||||
play_urls = []
|
||||
for i in result['vod_collection']:
|
||||
if isinstance(i, dict):
|
||||
play_urls.append(f"{i['title']}${i['vod_id']}@{i['id']}@{i['cur_time']}@{i['vod_token']}")
|
||||
video = {
|
||||
'vod_id': result['id'],
|
||||
'vod_name': result['vod_name'],
|
||||
'vod_pic': result['vod_pic'],
|
||||
'vod_remarks': result['remarks'],
|
||||
'vod_year': result['vod_year'],
|
||||
'vod_area': result['vod_area'],
|
||||
'vod_actor': result['vod_actor'],
|
||||
'vod_director': result['vod_director'],
|
||||
'vod_content': result['vod_blurb'],
|
||||
'vod_play_from': '小草',
|
||||
'vod_play_url': '#'.join(play_urls),
|
||||
'type_name': result['vod_tag']
|
||||
}
|
||||
return {'list': [video]}
|
||||
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
vod_id, collection_id, cur_time, vod_token = id.split('@', 3)
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
payload = {
|
||||
'collection_id': collection_id,
|
||||
'sig': "",
|
||||
'nc_token': "",
|
||||
'code': "",
|
||||
'phone': "",
|
||||
'vod_id': vod_id,
|
||||
'session_id': "",
|
||||
'vod_token': vod_token,
|
||||
'cur_time': cur_time
|
||||
}
|
||||
response = self.post(f'{self.host}/api/video/collection', data=payload, headers=headers, verify=False).text
|
||||
data = self.decrypt(response)
|
||||
result = json.loads(data)['result']
|
||||
try:
|
||||
ck = base64.b64decode(result['ck']).decode('utf-8')
|
||||
except Exception:
|
||||
ck = result['ck']
|
||||
vod_url = result['vod_url']
|
||||
check_url = result.get('check_page_url')
|
||||
url = check_url or self.proxyurl + '&url=' + quote(f"{vod_url}?{ck}", safe='')
|
||||
return {'jx': '0', 'parse': '0', 'url': url, 'header': self.play_header}
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "xiaocao":
|
||||
return self.xiaocao_m3u8_proxy(params)
|
||||
return None
|
||||
|
||||
def login(self, host):
|
||||
if self.headers['token'] and self.host: return
|
||||
headers = self.headers.copy()
|
||||
headers['cur_time'] = self.timestamp()
|
||||
headers['sign'] = self.sign(headers['cur_time'])
|
||||
payload = {
|
||||
'invited_by': '',
|
||||
'ua': 'Mozilla/5.0 (Linux; Android 12; 23116PN5BC Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36',
|
||||
'is_install': '0'
|
||||
}
|
||||
response = self.post(f'{host}/api/public/init', data=payload, headers=headers, verify=False).text
|
||||
data = self.decrypt(response)
|
||||
result = json.loads(data)['result']
|
||||
self.headers['token'] = result['user_info']['token']
|
||||
self.host = result['sys_conf']['api_url'].rstrip('/')
|
||||
if not (self.headers['token'] or self.play_domain or self.play_domain):
|
||||
self.host = ''
|
||||
|
||||
def sign(self, cur_time):
|
||||
return self.md5(f"zD9[bM4~sF4~uY2){self.headers['device_id']}{cur_time}").upper()
|
||||
|
||||
def md5(self, str):
|
||||
md5_hash = hashlib.md5()
|
||||
md5_hash.update(str.encode('utf-8'))
|
||||
return md5_hash.hexdigest()
|
||||
|
||||
def xiaocao_m3u8_proxy(self, params):
|
||||
url = unquote(params['url'])
|
||||
if params.get('format') == "ts":
|
||||
data = {"Location": url + self.hls_sign(url), "Content-Length": "0"}
|
||||
return [302, "text/html; charset=utf-8", None, data]
|
||||
else:
|
||||
data = self.xiaocao_modify_m3u8(url)
|
||||
return [200, "application/vnd.apple.mpegurl", data]
|
||||
|
||||
def xiaocao_modify_m3u8(self, raw_url):
|
||||
ck = raw_url.split('?')[1]
|
||||
m3u8_url = raw_url + self.hls_sign(raw_url)
|
||||
m3u8_content = self.fetch(m3u8_url, headers=self.play_header, verify=False).text
|
||||
content = m3u8_content if m3u8_content is not None else self.m3u8_content
|
||||
if content is None: raise ValueError("M3U8为空")
|
||||
parsed = urlparse(raw_url)
|
||||
base = f"{parsed.scheme}://{parsed.netloc}{parsed.path.rsplit('/', 1)[0]}/"
|
||||
output_lines = []
|
||||
for line in content.splitlines():
|
||||
stripped_line = line.strip()
|
||||
if stripped_line and not stripped_line.startswith('#'):
|
||||
if not stripped_line.startswith(('http://', 'https://')):
|
||||
full_url = urljoin(base, stripped_line)
|
||||
else:
|
||||
full_url = stripped_line
|
||||
signed_url = self.proxyurl + f'&format=ts&url=' + quote(full_url + '&' + ck,safe='')
|
||||
output_lines.append(signed_url)
|
||||
else:
|
||||
output_lines.append(line)
|
||||
return '\n'.join(output_lines)
|
||||
|
||||
def decrypt(self, base64_ciphertext):
|
||||
try:
|
||||
ciphertext = base64.b64decode(base64_ciphertext)
|
||||
cipher = AES.new('aZ9$kU5%qI7=yC2=zH2#gM0@pX7^wF3a'.encode('utf-8'), AES.MODE_CBC, 'hY2&tN3]kF7,dL7='.encode('utf-8'))
|
||||
plaintext_bytes = cipher.decrypt(ciphertext)
|
||||
padding_len = plaintext_bytes[-1]
|
||||
plaintext_bytes = plaintext_bytes[:-padding_len]
|
||||
return plaintext_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
raise Exception(f"解密失败: {str(e)}")
|
||||
|
||||
def get_url_path(self, url):
|
||||
parsed_url = urlparse(url)
|
||||
return parsed_url.path
|
||||
|
||||
def hls_sign(self, url):
|
||||
hex_time = self.hex_time()
|
||||
if '?' in url: url = url.split('?')[0]
|
||||
data = self.encrypt_domain + self.get_url_path(url) + hex_time
|
||||
return f"&wsSecret={self.md5(data)}&wsTime={hex_time}"
|
||||
|
||||
def is_valid_android_id(self, android_id):
|
||||
if not isinstance(android_id, str):
|
||||
return False
|
||||
pattern = r'^[0-9a-f]{16}$'
|
||||
return bool(re.fullmatch(pattern, android_id))
|
||||
|
||||
def random_device_id(self):
|
||||
chars = '0123456789abcdef'
|
||||
android_id = ''.join(random.choice(chars) for _ in range(16))
|
||||
return android_id
|
||||
|
||||
def timestamp(self):
|
||||
return str(int(time.time() * 1000))
|
||||
|
||||
def hex_time(self):
|
||||
return hex(int(time.time()))[2:]
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user