Sync all projects
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import requests
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from urllib.parse import quote, urlparse
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
self.did=self.getdid()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getdata("/api.php/getappapi.index/initV119")
|
||||
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序",
|
||||
"sort": "排序"}
|
||||
filters = {}
|
||||
classes = []
|
||||
json_data = data["type_list"]
|
||||
homedata = data["banner_list"][8:]
|
||||
for item in json_data:
|
||||
if item["type_name"] == "全部":
|
||||
continue
|
||||
has_non_empty_field = False
|
||||
jsontype_extend = json.loads(item["type_extend"])
|
||||
homedata.extend(item["recommend_list"])
|
||||
jsontype_extend["sort"] = "最新,最热,最赞"
|
||||
classes.append({"type_name": item["type_name"], "type_id": item["type_id"]})
|
||||
for key in dy:
|
||||
if key in jsontype_extend and jsontype_extend[key].strip() != "":
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["type_id"])] = []
|
||||
for dkey in jsontype_extend:
|
||||
if dkey in dy and jsontype_extend[dkey].strip() != "":
|
||||
values = jsontype_extend[dkey].split(",")
|
||||
value_array = [{"n": value.strip(), "v": value.strip()} for value in values if
|
||||
value.strip() != ""]
|
||||
filters[str(item["type_id"])].append({"key": dkey, "name": dy[dkey], "value": value_array})
|
||||
result = {}
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
result["list"] = homedata[1:]
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"area": extend.get('area', '全部'), "year": extend.get('year', '全部'), "type_id": tid, "page": pg,
|
||||
"sort": extend.get('sort', '最新'), "lang": extend.get('lang', '全部'),
|
||||
"class": extend.get('class', '全部')}
|
||||
result = {}
|
||||
data = self.getdata("/api.php/getappapi.index/typeFilterVodList", body)
|
||||
result["list"] = data["recommend_list"]
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = f"vod_id={ids[0]}"
|
||||
data = self.getdata("/api.php/getappapi.index/vodDetail", body)
|
||||
vod = data["vod"]
|
||||
play = []
|
||||
names = []
|
||||
for itt in data["vod_play_list"]:
|
||||
a = []
|
||||
names.append(itt["player_info"]["show"])
|
||||
for it in itt['urls']:
|
||||
it['user_agent'] = itt["player_info"].get("user_agent")
|
||||
it["parse"] = itt["player_info"].get("parse")
|
||||
a.append(f"{it['name']}${self.e64(json.dumps(it))}")
|
||||
play.append("#".join(a))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
body = f"keywords={key}&type_id=0&page={pg}"
|
||||
data = self.getdata("/api.php/getappapi.index/searchList", body)
|
||||
result = {"list": data["search_list"], "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = json.loads(self.d64(id))
|
||||
h = {"User-Agent": (ids['user_agent'] or "okhttp/3.14.9")}
|
||||
try:
|
||||
if re.search(r'url=', ids['parse_api_url']):
|
||||
data = self.fetch(ids['parse_api_url'], headers=h, timeout=10).json()
|
||||
url = data.get('url') or data['data'].get('url')
|
||||
else:
|
||||
body = f"parse_api={ids.get('parse') or ids['parse_api_url'].replace(ids['url'], '')}&url={quote(self.aes(ids['url'], True))}&token={ids.get('token')}"
|
||||
b = self.getdata("/api.php/getappapi.index/vodParse", body)['json']
|
||||
url = json.loads(b)['url']
|
||||
if 'error' in url: raise ValueError(f"解析失败: {url}")
|
||||
p = 0
|
||||
except Exception as e:
|
||||
print('错误信息:', e)
|
||||
url, p = ids['url'], 1
|
||||
|
||||
if re.search(r'\.jpg|\.png|\.jpeg', url):
|
||||
url = self.Mproxy(url)
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
return self.Mlocal(param)
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/3.14.9'
|
||||
}
|
||||
response = self.fetch('https://miget-1313189639.cos.ap-guangzhou.myqcloud.com/mifun.txt',headers=headers).text
|
||||
return self.host_late(response.split('\n'))
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
url = url.strip()
|
||||
start_time = time.time()
|
||||
response = requests.head(url, timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def getdid(self):
|
||||
did=self.getCache('did')
|
||||
if not did:
|
||||
t = str(int(time.time()))
|
||||
did = self.md5(t)
|
||||
self.setCache('did', did)
|
||||
return did
|
||||
|
||||
def aes(self, text, b=None):
|
||||
key = b"GETMIFUNGEIMIFUN"
|
||||
cipher = AES.new(key, AES.MODE_CBC, key)
|
||||
if b:
|
||||
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return pt.decode("utf-8")
|
||||
|
||||
def header(self):
|
||||
t = str(int(time.time()))
|
||||
header = {"Referer": self.host,
|
||||
"User-Agent": "okhttp/3.14.9", "app-version-code": "516", "app-ui-mode": "light",
|
||||
"app-api-verify-time": t, "app-user-device-id": self.did,
|
||||
"app-api-verify-sign": self.aes(t, True),
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
return header
|
||||
|
||||
def getdata(self, path, data=None):
|
||||
vdata = self.post(f"{self.host}{path}", headers=self.header(), data=data, timeout=10).json()['data']
|
||||
data1 = self.aes(vdata)
|
||||
return json.loads(data1)
|
||||
|
||||
def Mproxy(self, url):
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=m3u8"
|
||||
|
||||
def Mlocal(self, param, header=None):
|
||||
url = self.d64(param["url"])
|
||||
ydata = self.fetch(url, headers=header, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = self.fetch(url, headers=header).content.decode('utf-8')
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
last_slash_index = string.rfind('/')
|
||||
lpath = string[:last_slash_index + 1]
|
||||
lines[index] = durl + ('' if lpath.startswith('/') else '/') + lpath
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习及爬虫技术交流。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
|
||||
import re,sys,uuid
|
||||
from base.spider import Spider
|
||||
sys.path.append('..')
|
||||
class Spider(Spider):
|
||||
host,config,local_uuid,parsing_config = '','','',[]
|
||||
# 头部添加token认证
|
||||
headers = {
|
||||
'User-Agent': "Dart/2.19 (dart:io)",
|
||||
'Accept-Encoding': "gzip",
|
||||
'appto-local-uuid': local_uuid,
|
||||
'token': "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfY2hlY2siOiI4ZTEyNDE1Y2UyOGQzMGM4MWE3MDBiNWYxMDgzZTU2OCIsInVzZXJfaWQiOjM0NTYsInVzZXJfbmFtZSI6IjEwMTAxMiJ9LCJleHAiOjE4MDQ3MzkyODAuNjA4MTA4MywiaWF0IjoxNzczMjAzMjgxLCJpc3MiOiJBcHBUbyIsImp0aSI6ImZmZDMyYjk4N2VkMTg1ZjNiNGQ5Zjc5NzU2YWRjNGQ5IiwibmJmIjoxNzczMjAzMjgxLCJzdWIiOiJBcHBUbyJ9.tDhURwWVzsPy0-yXvo_d3bgsmoq9Ri5n0Y4fQsvxKy0"
|
||||
}
|
||||
def init(self, extend=''):
|
||||
try:
|
||||
host = extend.strip()
|
||||
if not host.startswith('http'):
|
||||
return {}
|
||||
if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host):
|
||||
host_=self.fetch(host).json()
|
||||
self.host = host_['domain']
|
||||
else:
|
||||
self.host = host
|
||||
self.local_uuid = str(uuid.uuid4())
|
||||
# 动态更新headers中的uuid(避免初始化时uuid为空)
|
||||
self.headers['appto-local-uuid'] = self.local_uuid
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/config/get?p=android&__platform=android', headers=self.headers).json()
|
||||
config = response['data']
|
||||
self.config = config
|
||||
parsing_conf = config['get_parsing']['lists']
|
||||
parsing_config = {}
|
||||
for i in parsing_conf:
|
||||
if len(i['config']) != 0:
|
||||
label = []
|
||||
for j in i['config']:
|
||||
if j['type'] == 'json':
|
||||
label.append(j['label'])
|
||||
parsing_config.update({i['key']:label})
|
||||
self.parsing_config = parsing_config
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f'初始化异常:{e}')
|
||||
return {}
|
||||
def detailContent(self, ids):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/getVod?id={ids[0]}",headers=self.headers).json()
|
||||
data3 = response['data']
|
||||
videos = []
|
||||
vod_play_url = ''
|
||||
vod_play_from = ''
|
||||
for i in data3['vod_play_list']:
|
||||
play_url = ''
|
||||
for j in i['urls']:
|
||||
play_url += f"{j['name']}${i['player_info']['from']}@{j['url']}#"
|
||||
vod_play_from += i['player_info']['show'] + '$$$'
|
||||
vod_play_url += play_url.rstrip('#') + '$$$'
|
||||
vod_play_url = vod_play_url.rstrip('$$$')
|
||||
vod_play_from = vod_play_from.rstrip('$$$')
|
||||
videos.append({
|
||||
'vod_id': data3.get('vod_id'),
|
||||
'vod_name': data3.get('vod_name'),
|
||||
'vod_content': data3.get('vod_content'),
|
||||
'vod_remarks': data3.get('vod_remarks'),
|
||||
'vod_director': data3.get('vod_director'),
|
||||
'vod_actor': data3.get('vod_actor'),
|
||||
'vod_year': data3.get('vod_year'),
|
||||
'vod_area': data3.get('vod_area'),
|
||||
'vod_play_from': vod_play_from,
|
||||
'vod_play_url': vod_play_url
|
||||
})
|
||||
return {'list': videos}
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = f"{self.host}/apptov5/v1/search/lists?wd={key}&page={pg}&type=&__platform=android"
|
||||
response = self.fetch(url, headers=self.headers).json()
|
||||
data = response['data']['data']
|
||||
for i in data:
|
||||
if i.get('vod_pic').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data, 'page': pg, 'total': response['data']['total']}
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
default_ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
parsing_config = self.parsing_config
|
||||
parts = id.split('@')
|
||||
if len(parts) != 2:
|
||||
return {'parse': 0, 'url': id, 'header': {'User-Agent': default_ua}}
|
||||
playfrom, rawurl = parts
|
||||
label_list = parsing_config.get(playfrom)
|
||||
if not label_list:
|
||||
return {'parse': 0, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
result = {'parse': 1, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
for label in label_list:
|
||||
payload = {
|
||||
'play_url': rawurl,
|
||||
'label': label,
|
||||
'key': playfrom
|
||||
}
|
||||
try:
|
||||
response = self.post(
|
||||
f"{self.host}/apptov5/v1/parsing/proxy?__platform=android",
|
||||
data=payload,
|
||||
headers=self.headers
|
||||
).json()
|
||||
except Exception as e:
|
||||
print(f"请求异常: {e}")
|
||||
continue
|
||||
if not isinstance(response, dict):
|
||||
continue
|
||||
if response.get('code') == 422:
|
||||
continue
|
||||
data = response.get('data')
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
url = data.get('url')
|
||||
if not url:
|
||||
continue
|
||||
ua = data.get('UA') or data.get('UserAgent') or default_ua
|
||||
result = {
|
||||
'parse': 0,
|
||||
'url': url,
|
||||
'header': {'User-Agent': ua}
|
||||
}
|
||||
break
|
||||
return result
|
||||
def homeContent(self, filter):
|
||||
config = self.config
|
||||
if not config:
|
||||
return {}
|
||||
home_cate = config['get_home_cate']
|
||||
classes = []
|
||||
for i in home_cate:
|
||||
if isinstance(i.get('extend', []),dict):
|
||||
classes.append({'type_id': i['cate'], 'type_name': i['title']})
|
||||
return {'class': classes}
|
||||
def homeVideoContent(self):
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/home/data?id=1&mold=1&__platform=android',headers=self.headers).json()
|
||||
data = response['data']
|
||||
vod_list = []
|
||||
for i in data['sections']:
|
||||
for j in i['items']:
|
||||
vod_pic = j.get('vod_pic')
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'http://', 1)
|
||||
vod_list.append({
|
||||
"vod_id": j.get('vod_id'),
|
||||
"vod_name": j.get('vod_name'),
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": j.get('vod_remarks')
|
||||
})
|
||||
return {'list': vod_list}
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/lists?area={extend.get('area','')}&lang={extend.get('lang','')}&year={extend.get('year','')}&order={extend.get('sort','time')}&type_id={tid}&type_name=&page={pg}&pageSize=21&__platform=android", headers=self.headers).json()
|
||||
data = response['data']
|
||||
data2 = data['data']
|
||||
for i in data['data']:
|
||||
if i.get('vod_pic','').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data2, 'page': pg, 'total': data['total']}
|
||||
def getName(self):
|
||||
pass
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
def destroy(self):
|
||||
pass
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,948 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import ssl
|
||||
import json
|
||||
import html
|
||||
import base64
|
||||
import urllib3
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
from urllib.parse import quote, unquote, urljoin, urlparse
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
sys.path.append("..")
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class SSLAdapter(HTTPAdapter):
|
||||
def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
kwargs["ssl_context"] = ctx
|
||||
return super().init_poolmanager(connections, maxsize, block=block, **kwargs)
|
||||
|
||||
def proxy_manager_for(self, proxy, **kwargs):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
kwargs["ssl_context"] = ctx
|
||||
return super().proxy_manager_for(proxy, **kwargs)
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
hosts = [
|
||||
"https://www.qwmkv.com",
|
||||
"https://www.qwnull.com",
|
||||
"https://www.qwfilm.com",
|
||||
"https://www.qnmp4.com",
|
||||
"https://www.qn63.com"
|
||||
]
|
||||
host = hosts[0]
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 12; M2012K11AC) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1"
|
||||
}
|
||||
|
||||
CATEGORY_IDS = {"电影": 1, "剧集": 2, "综艺": 3, "动漫": 4, "短剧": 30}
|
||||
KEYWORDS = ["杜比", "dolby", "原盘", "高码", "remux", "蓝光", "hdr10+", "hdr10", "hdr", "4k", "2160p", "uhd"]
|
||||
|
||||
QUARK_CHECK_LIMIT = 100
|
||||
CHECK_TIME_BUDGET = 12.0 # 检测最多12秒
|
||||
|
||||
def getName(self):
|
||||
return "七味-最终稳定版(含大屏分组排序+防串位修复)"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
adapter = SSLAdapter(max_retries=2)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
self.session.verify = False
|
||||
self.session.headers.update(dict(self.headers))
|
||||
|
||||
self.last_vod_pic = ""
|
||||
self.vod_pic_cache = {}
|
||||
|
||||
self.pan_115_cookie = ""
|
||||
self.ack_mp4 = "https://vd2.bdstatic.com/mda-nj5kxa8kr7wgq6ie/sc/cae_h264_nowatermark/1653272065989267185/mda-nj5kxa8kr7wgq6ie.mp4"
|
||||
|
||||
if extend:
|
||||
try:
|
||||
ext = json.loads(extend)
|
||||
self.pan_115_cookie = ext.get("pan_115_cookie", "")
|
||||
self.ack_mp4 = ext.get("ack_mp4", self.ack_mp4)
|
||||
except Exception as e:
|
||||
print(f"init extend error: {e}")
|
||||
|
||||
self._probe_host()
|
||||
|
||||
def destroy(self):
|
||||
try:
|
||||
self.session.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# ---------------- utils ----------------
|
||||
def _probe_host(self):
|
||||
for h in self.hosts:
|
||||
try:
|
||||
r = self.session.get(h + "/", timeout=6, headers=self.headers, verify=False)
|
||||
if r.status_code == 200:
|
||||
self.host = h
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
def _full_url(self, path):
|
||||
if not path:
|
||||
return ""
|
||||
path = html.unescape(str(path)).strip()
|
||||
if path.startswith("//"):
|
||||
return "https:" + path
|
||||
if path.startswith(("http://", "https://", "magnet:?")):
|
||||
return path
|
||||
return urljoin(self.host + "/", path)
|
||||
|
||||
def _full_url_by_host(self, host, path):
|
||||
if not path:
|
||||
return ""
|
||||
path = html.unescape(str(path)).strip()
|
||||
if path.startswith("//"):
|
||||
return "https:" + path
|
||||
if path.startswith(("http://", "https://", "magnet:?")):
|
||||
return path
|
||||
return urljoin(host.rstrip("/") + "/", path.lstrip("/"))
|
||||
|
||||
def _fetch(self, url, timeout=10):
|
||||
tries = [self._full_url(url)]
|
||||
if isinstance(url, str) and not url.startswith(("http://", "https://", "magnet:?")):
|
||||
for h in self.hosts:
|
||||
u = urljoin(h + "/", url)
|
||||
if u not in tries:
|
||||
tries.append(u)
|
||||
|
||||
for u in tries:
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/"
|
||||
r = self.session.get(u, timeout=timeout, headers=h, verify=False)
|
||||
r.encoding = r.apparent_encoding or "utf-8"
|
||||
if r.status_code == 200 and len(r.text or "") > 30:
|
||||
for hh in self.hosts:
|
||||
if u.startswith(hh):
|
||||
self.host = hh
|
||||
break
|
||||
return r
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def _pq(self, url, timeout=10):
|
||||
r = self._fetch(url, timeout=timeout)
|
||||
return pq(r.text if r else "")
|
||||
|
||||
def _clean_text(self, s):
|
||||
return re.sub(r"\s+", " ", html.unescape(s or "")).strip()
|
||||
|
||||
def _clean_name(self, s, max_len=120):
|
||||
s = html.unescape(s or "").replace("#", "#").replace("$", "$")
|
||||
s = re.sub(r"\s+", " ", s).strip()
|
||||
return s[:max_len]
|
||||
|
||||
def _img_src(self, img):
|
||||
return img.attr("data-src") or img.attr("data-original") or img.attr("src") or ""
|
||||
|
||||
def _is_pan(self, u):
|
||||
u = (u or "").lower()
|
||||
return any(k in u for k in [
|
||||
"pan.quark.cn/s/", "pan.baidu.com/s/", "drive.uc.cn/s/",
|
||||
"pan.xunlei.com/s/", "aliyundrive.com/s/", "alipan.com/s/",
|
||||
"cloud.189.cn/", "caiyun.139.com/", "123pan.com/s/",
|
||||
"115.com/s/", "lanzou", "lanzoui", "lanzoux", "lanzoub"
|
||||
])
|
||||
|
||||
def _b64e(self, obj):
|
||||
txt = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
return base64.urlsafe_b64encode(txt.encode()).decode().rstrip("=")
|
||||
|
||||
def _b64d(self, s):
|
||||
try:
|
||||
s += "=" * (-len(s) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(s.encode()).decode())
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _get_mid(self, tid):
|
||||
if str(tid).isdigit():
|
||||
return int(tid)
|
||||
m = re.search(r"/vt/(\d+)", str(tid))
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
return 1
|
||||
|
||||
def _score_name(self, name):
|
||||
n = (name or "").lower()
|
||||
score = 0
|
||||
for i, kw in enumerate(self.KEYWORDS):
|
||||
if kw.lower() in n:
|
||||
score += (len(self.KEYWORDS) - i)
|
||||
return score
|
||||
|
||||
def _extract_video_list(self, doc):
|
||||
videos, seen = [], set()
|
||||
selectors = ["ul.pic-list li", "ul.content-list li", ".pic-list li", ".content-list li"]
|
||||
nodes = []
|
||||
for sel in selectors:
|
||||
n = list(doc(sel).items())
|
||||
if n:
|
||||
nodes = n
|
||||
break
|
||||
|
||||
for li in nodes:
|
||||
a = li("a[href]").eq(0)
|
||||
href = a.attr("href")
|
||||
if not href:
|
||||
continue
|
||||
vid = self._full_url(href)
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
|
||||
img = li("img").eq(0)
|
||||
pic = self._full_url(self._img_src(img))
|
||||
title = a.attr("title") or img.attr("alt") or li("h3 b").text() or li("h3").text() or ""
|
||||
remark = self._clean_text(li("span.s1").text() or li("span.s2").text() or li("p").text() or li(".tag").text())
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid,
|
||||
"vod_name": self._clean_name(title, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
})
|
||||
return videos
|
||||
|
||||
def _is_bad_cover(self, u):
|
||||
if not u:
|
||||
return True
|
||||
s = u.lower()
|
||||
return ("logo.png" in s) or ("loading" in s) or ("/template/piankuwap/image/logo" in s)
|
||||
|
||||
def _normalize_magnet(self, href):
|
||||
try:
|
||||
if not href:
|
||||
return ""
|
||||
href = str(href).strip().replace("&", "&")
|
||||
if href.startswith("push://"):
|
||||
href = href.replace("push://", "", 1).replace("#0agent", "")
|
||||
if "%3A" in href or "%3F" in href or "%26" in href:
|
||||
href = unquote(href)
|
||||
href = re.sub(r"\s+", "", href)
|
||||
if not href.startswith("magnet:") or "urn:btih:" not in href:
|
||||
return ""
|
||||
return href
|
||||
except:
|
||||
return ""
|
||||
|
||||
def _magnet_btih(self, magnet):
|
||||
m = self._normalize_magnet(magnet)
|
||||
if not m:
|
||||
return ""
|
||||
g = re.search(r"xt=urn:btih:([a-zA-Z0-9]+)", m, re.I)
|
||||
return g.group(1).lower() if g else ""
|
||||
|
||||
def _is_verify_page(self, text):
|
||||
t = (text or "").lower()
|
||||
return (
|
||||
("系统安全验证" in t) or
|
||||
("verify_check" in t) or
|
||||
("mac_verify_img" in t) or
|
||||
("请输入验证码" in t)
|
||||
)
|
||||
|
||||
def _mk_vod_id(self, h, raw_id, raw_url=""):
|
||||
if raw_url:
|
||||
u = self._full_url_by_host(h, raw_url)
|
||||
if "/mv/" in u and u.endswith(".html"):
|
||||
return u
|
||||
if str(raw_id).isdigit():
|
||||
return f"{h}/mv/{raw_id}.html"
|
||||
m = re.search(r"/mv/(\d+)\.html", u)
|
||||
if m:
|
||||
return f"{h}/mv/{m.group(1)}.html"
|
||||
return u
|
||||
|
||||
rid = str(raw_id or "").strip()
|
||||
if rid.isdigit():
|
||||
return f"{h}/mv/{rid}.html"
|
||||
if rid.startswith(("http://", "https://", "/")):
|
||||
return self._full_url_by_host(h, rid)
|
||||
return f"{h}/mv/{rid}.html" if rid else ""
|
||||
|
||||
# ---------------- only check quark/115 ----------------
|
||||
def _check_pan_valid(self, url, provider, timeout=3):
|
||||
if not url:
|
||||
return False
|
||||
if provider not in ("quark", "115"):
|
||||
return True
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/"
|
||||
r = requests.get(url, headers=h, timeout=timeout, verify=False, allow_redirects=True)
|
||||
if r.status_code >= 400:
|
||||
return False
|
||||
text = (r.text or "").lower()
|
||||
if provider == "quark":
|
||||
keys = ["分享已失效", "不存在", "已被取消", "取消", "删除", "已被删除", "来晚了", "违规", "无法访问"]
|
||||
else:
|
||||
keys = ["分享已失效", "不存在", "404", "已取消", "链接错误"]
|
||||
return not any(k in text for k in keys)
|
||||
except:
|
||||
return False
|
||||
|
||||
# ---------------- home ----------------
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
{"type_name": "大陆电影", "type_id": "https://www.qwmkv.com/ms/1-大陆-time---------.html"},
|
||||
{"type_name": "大陆剧集", "type_id": "https://www.qwmkv.com/ms/2-大陆-time---------.html"},
|
||||
{"type_name": "大陆综艺", "type_id": "https://www.qwmkv.com/ms/3-大陆-time---------.html"},
|
||||
{"type_name": "大陆动漫", "type_id": "https://www.qwmkv.com/ms/4-大陆-time---------.html"},
|
||||
{"type_name": "电影", "type_id": "/vt/1.html"},
|
||||
{"type_name": "综艺", "type_id": "/vt/3.html"},
|
||||
{"type_name": "剧集", "type_id": "/vt/2.html"},
|
||||
{"type_name": "动漫", "type_id": "/vt/4.html"},
|
||||
{"type_name": "短剧", "type_id": "/vt/30.html"},
|
||||
]
|
||||
return {"class": classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
doc = self._pq(self.host + "/")
|
||||
videos = self._extract_video_list(doc)
|
||||
return {"list": videos, "page": 1, "pagecount": 1, "limit": len(videos), "total": len(videos)}
|
||||
|
||||
# ---------------- category ----------------
|
||||
def _build_category_url(self, tid, pg, fdict):
|
||||
if isinstance(tid, str) and tid.startswith(("http://", "https://")):
|
||||
if pg <= 1:
|
||||
return tid
|
||||
if "---------.html" in tid:
|
||||
return tid.replace("---------.html", f"------{pg}---.html")
|
||||
return tid.replace(".html", f"-{pg}.html")
|
||||
|
||||
mid = self._get_mid(tid)
|
||||
if not fdict:
|
||||
return f"{self.host}/vt/{mid}.html" if pg <= 1 else f"{self.host}/vt/{mid}-{pg}.html"
|
||||
|
||||
area = quote(fdict.get("地区", ""), safe="")
|
||||
sort = ""
|
||||
sv = fdict.get("排序", "")
|
||||
if sv == "按时间":
|
||||
sort = "time"
|
||||
elif sv == "按人气":
|
||||
sort = "hits"
|
||||
elif sv == "按评分":
|
||||
sort = "score"
|
||||
|
||||
typ = quote(fdict.get("类型", ""), safe="")
|
||||
lang = quote(fdict.get("语言", ""), safe="")
|
||||
year = fdict.get("年代", "")
|
||||
fields = [area, sort, typ, lang, "", "", "", "", year]
|
||||
base = f"{self.host}/ms/{mid}-" + "-".join(fields)
|
||||
return base + ".html" if pg <= 1 else base + f"-{pg}.html"
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
fdict = extend if isinstance(extend, dict) else {}
|
||||
url = self._build_category_url(tid, pg, fdict)
|
||||
doc = self._pq(url)
|
||||
|
||||
if len(doc("ul.pic-list li")) == 0 and len(doc("ul.content-list li")) == 0 and pg > 1:
|
||||
doc = self._pq(url.replace(".html", f".html?page={pg}"))
|
||||
|
||||
videos = self._extract_video_list(doc)
|
||||
page_count = pg
|
||||
for a in doc(".pages a").items():
|
||||
t = (a.text() or "").strip()
|
||||
href = a.attr("href") or ""
|
||||
if t.isdigit():
|
||||
page_count = max(page_count, int(t))
|
||||
else:
|
||||
m = re.search(r"-(\d+)\.html", href)
|
||||
if m:
|
||||
page_count = max(page_count, int(m.group(1)))
|
||||
|
||||
return {
|
||||
"list": videos,
|
||||
"page": pg,
|
||||
"pagecount": max(page_count, pg),
|
||||
"limit": 30,
|
||||
"total": max(page_count, pg) * 30
|
||||
}
|
||||
|
||||
# ---------------- detail ----------------
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
vod_id = ids[0] if isinstance(ids, list) and ids else ids
|
||||
vod_id = self._full_url(vod_id)
|
||||
|
||||
doc = self._pq(vod_id)
|
||||
raw = doc.html() or ""
|
||||
|
||||
# 1. 抓取基本影视信息
|
||||
title = self._clean_text(doc("h1").eq(0).text())
|
||||
if not title:
|
||||
tt = self._clean_text(doc("title").text())
|
||||
title = tt.split("在线观看")[0] if tt else "七味资源"
|
||||
|
||||
cover = ""
|
||||
og = self._full_url(doc('meta[property="og:image"]').attr("content") or "")
|
||||
if og and not self._is_bad_cover(og):
|
||||
cover = og
|
||||
if not cover:
|
||||
c1 = self._full_url(self._img_src(doc(".main-left .img img").eq(0)))
|
||||
if c1 and not self._is_bad_cover(c1):
|
||||
cover = c1
|
||||
if not cover:
|
||||
for im in doc("img").items():
|
||||
src = self._full_url(self._img_src(im))
|
||||
if src and not self._is_bad_cover(src):
|
||||
cover = src
|
||||
break
|
||||
if not cover:
|
||||
cover = self.vod_pic_cache.get(vod_id, "")
|
||||
if not cover:
|
||||
cover = self._full_url("/template/piankuwap/image/logo.png")
|
||||
self.last_vod_pic = cover
|
||||
|
||||
content = self._clean_text(
|
||||
doc(".movie-introduce .sqjj_a").text() or
|
||||
doc(".movie-introduce .zkjj_a").text() or
|
||||
doc(".content").text()
|
||||
)
|
||||
|
||||
# =====【优化修复】2. 多线路合并遍历抓取在线资源(彻底杜绝选择器冲突覆盖) =====
|
||||
online_routes = {} # 格式: {"在线线路1": ["第1集$payload", "第2集$payload"]}
|
||||
player_uls = doc("div#url .bd ul.player, ul.player")
|
||||
line_no = 1
|
||||
for ul_node in player_uls.items():
|
||||
links = list(ul_node("a[href]").items())
|
||||
if not links:
|
||||
continue
|
||||
|
||||
route_name = f"📺在线播放-线路{line_no}"
|
||||
episodes = []
|
||||
for a in links:
|
||||
href = a.attr("href")
|
||||
if not href:
|
||||
continue
|
||||
src_name = self._clean_name(self._clean_text(a.text()) or "播放", 50)
|
||||
payload = self._b64e({"type": "py", "url": self._full_url(href), "pic": cover})
|
||||
episodes.append(f"{src_name}${payload}")
|
||||
|
||||
if episodes:
|
||||
online_routes[route_name] = episodes
|
||||
line_no += 1
|
||||
|
||||
# ===== 3. 规整网盘与磁力资源链接 =====
|
||||
pan_resources = []
|
||||
magnet_raw = []
|
||||
seen_pan = set()
|
||||
|
||||
for a in doc("a[href]").items():
|
||||
u = html.unescape(a.attr("href") or "").strip()
|
||||
if not u:
|
||||
continue
|
||||
txt = self._clean_text(a.text())
|
||||
|
||||
if u.lower().startswith("magnet:?"):
|
||||
magnet_raw.append((u, self._clean_name(txt or "磁力资源", 60)))
|
||||
continue
|
||||
|
||||
if self._is_pan(u):
|
||||
low = u.lower()
|
||||
pv = "other"
|
||||
if "pan.quark" in low: pv = "quark"
|
||||
elif "115.com" in low: pv = "115"
|
||||
elif "pan.baidu" in low: pv = "baidu"
|
||||
elif "drive.uc.cn" in low: pv = "uc"
|
||||
elif "pan.xunlei" in low: pv = "xunlei"
|
||||
elif "aliyundrive" in low or "alipan" in low: pv = "ali"
|
||||
elif "cloud.189" in low: pv = "189"
|
||||
elif "123pan" in low: pv = "pan123"
|
||||
|
||||
if u not in seen_pan:
|
||||
seen_pan.add(u)
|
||||
pan_resources.append({
|
||||
"provider": pv,
|
||||
"url": u,
|
||||
"name": txt or "网盘资源",
|
||||
"checked_valid": False
|
||||
})
|
||||
|
||||
for m in re.finditer(r"magnet:\?[^\s\"'<>]+", raw, re.I):
|
||||
magnet_raw.append((html.unescape(m.group(0)), "磁力资源"))
|
||||
|
||||
# 磁力资源精简去重并打分排序
|
||||
magnet_unified = []
|
||||
btih_seen = set()
|
||||
for u, n in magnet_raw:
|
||||
mu = self._normalize_magnet(u)
|
||||
if not mu:
|
||||
continue
|
||||
btih = self._magnet_btih(mu)
|
||||
key = btih if btih else mu.lower()
|
||||
if key in btih_seen:
|
||||
continue
|
||||
btih_seen.add(key)
|
||||
magnet_unified.append({
|
||||
"url": mu,
|
||||
"name": self._clean_name(n or "磁力资源", 60)
|
||||
})
|
||||
magnet_unified.sort(key=lambda x: -self._score_name(x.get("name", "")))
|
||||
|
||||
# ===== 4. 网盘有效性探针(保持原有超时/计数策略) =====
|
||||
check_begin = time.monotonic()
|
||||
quark_checked = 0
|
||||
valid_pan = []
|
||||
|
||||
for p in pan_resources:
|
||||
if time.monotonic() - check_begin >= self.CHECK_TIME_BUDGET:
|
||||
valid_pan.append(p)
|
||||
continue
|
||||
|
||||
pv = p["provider"]
|
||||
if pv == "quark":
|
||||
if quark_checked >= self.QUARK_CHECK_LIMIT:
|
||||
valid_pan.append(p)
|
||||
continue
|
||||
quark_checked += 1
|
||||
if self._check_pan_valid(p["url"], "quark"):
|
||||
p["checked_valid"] = True
|
||||
valid_pan.append(p)
|
||||
elif pv == "115":
|
||||
if self._check_pan_valid(p["url"], "115"):
|
||||
p["checked_valid"] = True
|
||||
valid_pan.append(p)
|
||||
else:
|
||||
valid_pan.append(p)
|
||||
|
||||
# =====【功能修复】5. 网盘资源渠道完全独立隔离,防止错乱混杂 =====
|
||||
pan_routes = {
|
||||
"quark": {"name": "🟢夸克网盘", "list": []},
|
||||
"ali": {"name": "☁️阿里云盘", "list": []},
|
||||
"115": {"name": "固定115网盘", "list": []},
|
||||
"baidu": {"name": "📘百度网盘", "list": []},
|
||||
"uc": {"name": "📱UC网盘", "list": []},
|
||||
"xunlei": {"name": "⚡迅雷网盘", "list": []},
|
||||
"189": {"name": "☎️天翼云盘", "list": []},
|
||||
"pan123": {"name": "📦123网盘", "list": []},
|
||||
"other": {"name": "📦其它网盘", "list": []}
|
||||
}
|
||||
|
||||
for r in valid_pan:
|
||||
prov = r["provider"]
|
||||
if prov not in pan_routes:
|
||||
prov = "other"
|
||||
|
||||
ep_name = self._clean_name(r.get('name', '网盘提取资源'), 60)
|
||||
payload = self._b64e({"type": "pan", "url": r["url"], "pic": cover})
|
||||
pan_routes[prov]["list"].append(f"{ep_name}${payload}")
|
||||
|
||||
# =====【核心修复】6. 按标准大屏壳子1:1规则完美有序组装,防串位 =====
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 分支 A:写入网盘分类线路(依照预设的网盘体验优先级高低呈现)
|
||||
drive_order = ["quark", "ali", "115", "baidu", "uc", "xunlei", "189", "pan123", "other"]
|
||||
for d_key in drive_order:
|
||||
route_info = pan_routes[d_key]
|
||||
if route_info["list"]:
|
||||
play_from.append(route_info["name"])
|
||||
play_url.append("#".join(route_info["list"]))
|
||||
|
||||
# 分支 B:写入磁力解析相关线路(保持互相隔离)
|
||||
if magnet_unified:
|
||||
lines_115 = []
|
||||
lines_play = []
|
||||
for i, m in enumerate(magnet_unified, start=1):
|
||||
nm = self._clean_name(f"磁力源-{i:02d} {m['name']}", 60)
|
||||
encoded_mag = base64.urlsafe_b64encode(m['url'].encode()).decode().rstrip("=")
|
||||
p_payload = self._b64e({"type": "magnet", "url": m['url'], "pic": cover})
|
||||
|
||||
lines_115.append(f"{nm}${encoded_mag}")
|
||||
lines_play.append(f"{nm}${p_payload}")
|
||||
|
||||
# 独立线路一:115离线专用线
|
||||
play_from.append("📥115云下载")
|
||||
play_url.append("#".join(lines_115))
|
||||
|
||||
# 独立一条空白ACK确认交互线
|
||||
play_from.append("0")
|
||||
play_url.append("已提交请到115离线任务查看$__ACK__")
|
||||
|
||||
# 独立线路二:自带流播或通过本地壳嗅探弹磁力
|
||||
play_from.append("🧲磁力播放")
|
||||
play_url.append("#".join(lines_play))
|
||||
|
||||
# 分支 C:写入在线直连/网页采集线路
|
||||
for r_name, r_eps in online_routes.items():
|
||||
play_from.append(r_name)
|
||||
play_url.append("#".join(r_eps))
|
||||
|
||||
# ===== 当前站搜索入口 =====
|
||||
try:
|
||||
search_payload = self._b64e({
|
||||
"type": "search",
|
||||
"wd": title,
|
||||
"pic": cover
|
||||
})
|
||||
|
||||
play_from.insert(0, "🔍点击选择")
|
||||
play_url.insert(0, f"当前站搜索${search_payload}")
|
||||
except Exception as e:
|
||||
print(f"search line add error: {e}")
|
||||
|
||||
# 兜底处理
|
||||
if not play_from:
|
||||
play_from.append("🌐原网页查看")
|
||||
play_url.append(f"点击跳转原详情页${self._b64e({'type': 'web', 'url': vod_id, 'pic': cover})}")
|
||||
|
||||
# 7. 构建标准影视输出字典
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": self._clean_name(title, 100),
|
||||
"vod_pic": cover,
|
||||
"vod_content": content,
|
||||
"vod_play_from": "$$$".join(play_from),
|
||||
"vod_play_url": "$$$".join(play_url)
|
||||
}
|
||||
return {"list": [vod]}
|
||||
except Exception as e:
|
||||
print(f"detailContent error: {e}")
|
||||
return {"list": []}
|
||||
|
||||
# ---------------- player ----------------
|
||||
def _parse_py_page(self, py_url):
|
||||
r = self._fetch(py_url, timeout=10)
|
||||
if not r:
|
||||
return ""
|
||||
txt = r.text or ""
|
||||
|
||||
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*<", txt, re.S)
|
||||
if not m:
|
||||
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*;", txt, re.S)
|
||||
if not m:
|
||||
return ""
|
||||
|
||||
js = m.group(1)
|
||||
try:
|
||||
js2 = re.sub(r"(\w+)\s*:", r'"\1":', js)
|
||||
obj = json.loads(js2)
|
||||
except:
|
||||
try:
|
||||
obj = json.loads(js)
|
||||
except:
|
||||
return ""
|
||||
|
||||
u = obj.get("url", "") or ""
|
||||
enc = str(obj.get("encrypt", "0"))
|
||||
if enc == "1":
|
||||
u = unquote(u)
|
||||
elif enc == "2":
|
||||
try:
|
||||
u = unquote(base64.b64decode(u).decode("utf-8", "ignore"))
|
||||
except:
|
||||
pass
|
||||
|
||||
if u.startswith("//"):
|
||||
u = "https:" + u
|
||||
elif u.startswith("/"):
|
||||
u = self._full_url(u)
|
||||
return u
|
||||
|
||||
def _return_ack_video(self):
|
||||
ret = {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": self.ack_mp4,
|
||||
"header": {
|
||||
"User-Agent": self.headers.get("User-Agent", ""),
|
||||
"Referer": self.host + "/"
|
||||
}
|
||||
}
|
||||
if self.last_vod_pic:
|
||||
ret["pic"] = self.last_vod_pic
|
||||
ret["poster"] = self.last_vod_pic
|
||||
return ret
|
||||
|
||||
def _add_to_115(self, magnet):
|
||||
if not self.pan_115_cookie:
|
||||
print("115添加失败: 未配置 pan_115_cookie")
|
||||
return
|
||||
|
||||
magnet = self._normalize_magnet(magnet)
|
||||
if not magnet:
|
||||
print("115添加失败: 非法磁力")
|
||||
return
|
||||
|
||||
headers = {
|
||||
"User-Agent": self.headers.get("User-Agent", ""),
|
||||
"Cookie": self.pan_115_cookie,
|
||||
"Origin": "https://115.com",
|
||||
"Referer": "https://115.com/web/lixian/",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
|
||||
try:
|
||||
pan_sess = requests.Session()
|
||||
pan_sess.verify = False
|
||||
pan_sess.mount("http://", SSLAdapter(max_retries=2))
|
||||
pan_sess.mount("https://", SSLAdapter(max_retries=2))
|
||||
|
||||
space_resp = pan_sess.get("https://115.com/?ct=offline&ac=space", headers=headers, timeout=10)
|
||||
try:
|
||||
space_json = space_resp.json()
|
||||
except:
|
||||
print(f"115获取签名失败(非JSON): {space_resp.text[:200]}")
|
||||
return
|
||||
|
||||
if not space_json.get("state"):
|
||||
print(f"115获取签名失败(可能Cookie过期): {space_json}")
|
||||
return
|
||||
|
||||
sign = space_json.get("sign", "")
|
||||
req_time = space_json.get("time", "")
|
||||
if not sign or not req_time:
|
||||
print(f"115签名数据异常: {space_json}")
|
||||
return
|
||||
|
||||
uid_match = re.search(r'UID=(\d+)', self.pan_115_cookie)
|
||||
uid = uid_match.group(1) if uid_match else ""
|
||||
|
||||
add_url = "https://115.com/web/lixian/?ct=lixian&ac=add_task_url"
|
||||
post_data = {"url": magnet, "uid": uid, "sign": sign, "time": req_time}
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
|
||||
add_resp = pan_sess.post(add_url, data=post_data, headers=headers, timeout=10)
|
||||
try:
|
||||
add_json = add_resp.json()
|
||||
except:
|
||||
print(f"115添加失败(非JSON): {add_resp.text[:200]}")
|
||||
return
|
||||
|
||||
if add_json.get("state") or add_json.get("errcode") == 0:
|
||||
print(f"115离线添加成功: {magnet[:100]}...")
|
||||
else:
|
||||
err = add_json.get("error_msg") or add_json.get("msg") or add_json.get("error") or str(add_json)
|
||||
print(f"115添加失败: {err}")
|
||||
except Exception as e:
|
||||
print(f"115离线网络异常: {e}")
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if flag == "0" or id == "__ACK__":
|
||||
return self._return_ack_video()
|
||||
|
||||
if flag == "📥115云下载":
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(id.encode() + b"==").decode()
|
||||
magnet = self._normalize_magnet(decoded)
|
||||
if not magnet:
|
||||
return self._return_ack_video()
|
||||
if not self.pan_115_cookie:
|
||||
print("115未配置Cookie")
|
||||
return self._return_ack_video()
|
||||
|
||||
threading.Thread(target=self._add_to_115, args=(magnet,), daemon=True).start()
|
||||
return self._return_ack_video()
|
||||
except Exception as e:
|
||||
print(f"115云下载处理异常: {e}")
|
||||
return self._return_ack_video()
|
||||
|
||||
if flag == "🧲磁力播放":
|
||||
data = self._b64d(id)
|
||||
if data and data.get("type") == "magnet":
|
||||
mu = self._normalize_magnet(data.get("url", ""))
|
||||
if mu:
|
||||
pic = data.get("pic") or self.last_vod_pic
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
|
||||
|
||||
if isinstance(id, str) and id.startswith("push://"):
|
||||
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
mu = self._normalize_magnet(id)
|
||||
if mu:
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
data = self._b64d(id)
|
||||
if not data:
|
||||
if isinstance(id, str) and id.startswith("push://"):
|
||||
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
typ = data.get("type", "")
|
||||
url = data.get("url", "")
|
||||
pic = data.get("pic") or self.last_vod_pic
|
||||
|
||||
if not url:
|
||||
return {"parse": 1, "url": id, "pic": pic, "poster": pic}
|
||||
|
||||
if typ == "search":
|
||||
|
||||
wd = data.get("wd", "").strip()
|
||||
|
||||
if not wd:
|
||||
return {
|
||||
"parse": 1,
|
||||
"url": self.host,
|
||||
"pic": pic,
|
||||
"poster": pic
|
||||
}
|
||||
|
||||
search_url = f"{self.host}/vodsearch/{quote(wd)}----------1---.html"
|
||||
|
||||
return {
|
||||
"parse": 0,
|
||||
"url": "push://" + search_url,
|
||||
"pic": pic,
|
||||
"poster": pic
|
||||
}
|
||||
|
||||
if typ == "pan":
|
||||
return {"parse": 0, "url": "push://" + url, "pic": pic, "poster": pic}
|
||||
if typ == "magnet":
|
||||
mu = self._normalize_magnet(url)
|
||||
if mu:
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
if typ == "py":
|
||||
real = self._parse_py_page(url)
|
||||
if real:
|
||||
return {"parse": 0, "url": real, "pic": pic, "poster": pic}
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
if typ == "web":
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
|
||||
# ---------------- search ----------------
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
wd = quote(key)
|
||||
|
||||
for h in self.hosts:
|
||||
suggest_api = f"{h}/index.php/ajax/suggest?mid=1&limit=20&wd={wd}"
|
||||
try:
|
||||
r = self.session.get(suggest_api, timeout=8, headers=self.headers, verify=False)
|
||||
txt = r.text or ""
|
||||
if not self._is_verify_page(txt) and r.status_code == 200:
|
||||
data = r.json()
|
||||
lst = data.get("list") or []
|
||||
videos = []
|
||||
|
||||
for it in lst:
|
||||
vid = it.get("id") or it.get("vod_id")
|
||||
name = it.get("name") or it.get("vod_name") or ""
|
||||
pic = self._full_url_by_host(h, it.get("pic") or it.get("vod_pic") or "")
|
||||
remarks = self._clean_text(it.get("en") or it.get("remark") or "")
|
||||
jump_url = it.get("url") or it.get("link") or ""
|
||||
|
||||
vid_url = self._mk_vod_id(h, vid, jump_url)
|
||||
if not vid_url:
|
||||
continue
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid_url] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid_url,
|
||||
"vod_name": self._clean_name(name, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
|
||||
if videos:
|
||||
self.host = h
|
||||
return {
|
||||
"list": videos,
|
||||
"page": pg,
|
||||
"pagecount": pg + 1 if len(videos) >= 20 else pg,
|
||||
"limit": len(videos),
|
||||
"total": len(videos)
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
api_list = [
|
||||
f"{h}/api.php/provide/vod/?ac=detail&wd={wd}&pg={pg}",
|
||||
f"{h}/api.php/provide/vod?ac=detail&wd={wd}&pg={pg}",
|
||||
]
|
||||
for api in api_list:
|
||||
try:
|
||||
r = self.session.get(api, timeout=8, headers=self.headers, verify=False)
|
||||
txt = r.text or ""
|
||||
if self._is_verify_page(txt):
|
||||
continue
|
||||
if r.status_code != 200:
|
||||
continue
|
||||
|
||||
data = r.json()
|
||||
lst = data.get("list") or data.get("data") or []
|
||||
videos = []
|
||||
|
||||
for it in lst:
|
||||
vid = it.get("vod_id") or it.get("id")
|
||||
name = it.get("vod_name") or it.get("name") or ""
|
||||
pic = self._full_url_by_host(h, it.get("vod_pic") or it.get("pic") or "")
|
||||
remarks = self._clean_text(it.get("vod_remarks") or it.get("remarks") or "")
|
||||
jump_url = it.get("vod_play_url") or it.get("url") or it.get("link") or ""
|
||||
|
||||
vid_url = self._mk_vod_id(h, vid, jump_url)
|
||||
if not vid_url:
|
||||
continue
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid_url] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid_url,
|
||||
"vod_name": self._clean_name(name, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
|
||||
if videos:
|
||||
self.host = h
|
||||
return {
|
||||
"list": videos,
|
||||
"page": int(data.get("page", pg) or pg),
|
||||
"pagecount": int(data.get("pagecount", 1) or 1),
|
||||
"limit": len(videos),
|
||||
"total": int(data.get("total", len(videos)) or len(videos))
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"list": [],
|
||||
"page": pg,
|
||||
"pagecount": pg,
|
||||
"limit": 0,
|
||||
"total": 0
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class DyuziPanSpider(Spider):
|
||||
"""心跳4k剧场网盘资源搜索爬虫 - 适配Fongmi影视
|
||||
|
||||
网站: https://ppan.dyuzi.com/
|
||||
API分析:
|
||||
1. /api/other/web_search - 搜索API (返回SSE格式)
|
||||
2. /api/frontend/home - 首页数据 (热门关键词)
|
||||
3. /api/frontend/ranking - 热播榜单 (电视剧/电影/综艺/动漫)
|
||||
"""
|
||||
|
||||
# 网站基础配置
|
||||
SITE_URL = "https://ppan.dyuzi.com"
|
||||
WEB_SEARCH_API = f"{SITE_URL}/api/other/web_search"
|
||||
HOME_API = f"{SITE_URL}/api/frontend/home"
|
||||
RANKING_API = f"{SITE_URL}/api/frontend/ranking"
|
||||
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||
"Accept": "text/event-stream, application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Referer": SITE_URL,
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
|
||||
REQUEST_TIMEOUT = 60
|
||||
MAX_RETRIES = 3
|
||||
BACKOFF_FACTOR = 0.5
|
||||
# 请求间隔(秒),防止触发CDN/WAF封IP
|
||||
REQUEST_DELAY = 0.5
|
||||
|
||||
# 网盘类型映射 (is_type -> pan_type)
|
||||
IS_TYPE_MAP = {
|
||||
0: 'quark', # 夸克
|
||||
1: 'uc', # UC
|
||||
2: 'baidu', # 百度
|
||||
3: 'aliyun', # 阿里
|
||||
4: 'xunlei', # 迅雷
|
||||
5: 'a189', # 天翼
|
||||
6: 'quark', # 夸克(另一个标识)
|
||||
}
|
||||
|
||||
# 网盘类型配置
|
||||
PAN_CONFIG = {
|
||||
'quark': {
|
||||
'name': '夸克',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/quark.webp'
|
||||
},
|
||||
'uc': {
|
||||
'name': 'UC',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/uc.webp'
|
||||
},
|
||||
'a189': {
|
||||
'name': '天翼',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/189.webp'
|
||||
},
|
||||
'aliyun': {
|
||||
'name': '阿里',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/aliyun.webp'
|
||||
},
|
||||
'baidu': {
|
||||
'name': '百度',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/baidu.webp'
|
||||
},
|
||||
'xunlei': {
|
||||
'name': '迅雷',
|
||||
'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/xunlei.webp'
|
||||
},
|
||||
'magnet': {
|
||||
'name': '磁力',
|
||||
'icon': ''
|
||||
},
|
||||
'other': {
|
||||
'name': '网盘',
|
||||
'icon': ''
|
||||
}
|
||||
}
|
||||
|
||||
# 频道分类
|
||||
CHANNELS = {
|
||||
'电视剧': '1',
|
||||
'电影': '2',
|
||||
'综艺': '3',
|
||||
'动漫': '4'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pan_priority = ''
|
||||
self._last_request_time = 0
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.HEADERS)
|
||||
|
||||
retries = Retry(
|
||||
total=self.MAX_RETRIES,
|
||||
backoff_factor=self.BACKOFF_FACTOR,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
raise_on_status=False
|
||||
)
|
||||
self.session.mount('http://', HTTPAdapter(max_retries=retries))
|
||||
self.session.mount('https://', HTTPAdapter(max_retries=retries))
|
||||
|
||||
def init(self, extend):
|
||||
"""初始化配置"""
|
||||
try:
|
||||
extend_dict = json.loads(extend) if extend else {}
|
||||
self.pan_priority = extend_dict.get('pan_priority', 'quark,a189,uc')
|
||||
except json.JSONDecodeError:
|
||||
self.pan_priority = 'quark,a189,uc'
|
||||
|
||||
def getName(self):
|
||||
return "盘搜"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""首页内容 - 返回分类"""
|
||||
class_list = [
|
||||
{"type_id": "1", "type_name": "电视剧"},
|
||||
{"type_id": "2", "type_name": "电影"},
|
||||
{"type_id": "3", "type_name": "综艺"},
|
||||
{"type_id": "4", "type_name": "动漫"}
|
||||
]
|
||||
return {
|
||||
'class': class_list,
|
||||
'filters': {},
|
||||
'list': []
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""首页推荐视频 - 获取热播榜单"""
|
||||
try:
|
||||
# 获取电视剧热播榜
|
||||
resp = self.session.get(
|
||||
self.RANKING_API,
|
||||
params={'channel': '电视剧', 'limit': 12},
|
||||
timeout=self.REQUEST_TIMEOUT
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
vod_list = []
|
||||
if data.get('code') == 0 and data.get('data', {}).get('list'):
|
||||
for item in data['data']['list']:
|
||||
vod_list.append({
|
||||
"vod_id": self._b64e({
|
||||
'title': item.get('title', ''),
|
||||
'type': 'ranking',
|
||||
'channel': item.get('channel', '电视剧')
|
||||
}),
|
||||
"vod_name": item.get('title', ''),
|
||||
"vod_pic": item.get('src', ''),
|
||||
"vod_remarks": f"{item.get('episode_count', '')} | 热度:{item.get('hot_score', '0')[:4]}"
|
||||
})
|
||||
|
||||
return {'list': vod_list}
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取首页推荐异常: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
"""分类内容 - 获取各频道热播榜"""
|
||||
try:
|
||||
# 根据cid获取对应频道名称
|
||||
channel_map = {
|
||||
'1': '电视剧',
|
||||
'2': '电影',
|
||||
'3': '综艺',
|
||||
'4': '动漫'
|
||||
}
|
||||
channel = channel_map.get(cid, '电视剧')
|
||||
|
||||
resp = self.session.get(
|
||||
self.RANKING_API,
|
||||
params={'channel': channel, 'limit': 30},
|
||||
timeout=self.REQUEST_TIMEOUT
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
vod_list = []
|
||||
if data.get('code') == 0 and data.get('data', {}).get('list'):
|
||||
for item in data['data']['list']:
|
||||
vod_list.append({
|
||||
"vod_id": self._b64e({
|
||||
'title': item.get('title', ''),
|
||||
'type': 'ranking',
|
||||
'channel': item.get('channel', channel)
|
||||
}),
|
||||
"vod_name": item.get('title', ''),
|
||||
"vod_pic": item.get('src', ''),
|
||||
"vod_remarks": f"{item.get('episode_count', '')} | 评分:{item.get('score_avg', '0')}"
|
||||
})
|
||||
|
||||
return {
|
||||
'list': vod_list,
|
||||
'page': 1,
|
||||
'pagecount': 1,
|
||||
'limit': 30,
|
||||
'total': len(vod_list)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取分类内容异常: {e}")
|
||||
return {
|
||||
'list': [],
|
||||
'page': 1,
|
||||
'pagecount': 1,
|
||||
'limit': 30,
|
||||
'total': 0
|
||||
}
|
||||
|
||||
def _get_pan_type(self, is_type):
|
||||
"""根据is_type获取网盘类型"""
|
||||
return self.IS_TYPE_MAP.get(is_type, 'other')
|
||||
|
||||
def _get_pan_priority_order(self):
|
||||
"""获取网盘优先级顺序"""
|
||||
if self.pan_priority:
|
||||
return [p.strip() for p in self.pan_priority.split(',') if p.strip()]
|
||||
return ['baidu', 'quark', 'uc', 'a189', 'aliyun', 'xunlei']
|
||||
|
||||
def _b64e(self, obj):
|
||||
"""Base64编码"""
|
||||
if isinstance(obj, str):
|
||||
text = obj
|
||||
else:
|
||||
text = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
return base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
|
||||
|
||||
def _b64d(self, s):
|
||||
"""Base64解码"""
|
||||
try:
|
||||
s += "=" * (-len(s) % 4)
|
||||
decoded = base64.urlsafe_b64decode(s.encode()).decode()
|
||||
try:
|
||||
return json.loads(decoded)
|
||||
except:
|
||||
return decoded
|
||||
except:
|
||||
return s
|
||||
|
||||
def _parse_sse_response(self, response_text):
|
||||
"""解析SSE响应,提取数据行"""
|
||||
results = []
|
||||
lines = response_text.strip().split('\n')
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('data:'):
|
||||
data_str = line[5:].strip()
|
||||
if data_str == '[DONE]':
|
||||
continue
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
if 'title' in data and 'url' in data:
|
||||
results.append(data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索内容"""
|
||||
return self._perform_search(key, pg)
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
"""分页搜索"""
|
||||
return self._perform_search(key, page)
|
||||
|
||||
def _perform_search(self, keywords, page_str):
|
||||
"""执行搜索"""
|
||||
try:
|
||||
page = int(page_str)
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
|
||||
result = {
|
||||
'list': [],
|
||||
'page': page,
|
||||
'pagecount': 1,
|
||||
'limit': 100,
|
||||
'total': 0
|
||||
}
|
||||
|
||||
if not keywords:
|
||||
return result
|
||||
|
||||
# 请求间隔控制,防止频繁请求触发封IP
|
||||
elapsed = time.time() - self._last_request_time
|
||||
if elapsed < self.REQUEST_DELAY:
|
||||
time.sleep(self.REQUEST_DELAY - elapsed)
|
||||
|
||||
try:
|
||||
# 调用web_search API (SSE格式)
|
||||
# status=1 只返回有效链接(等同网页版"只看有效")
|
||||
params = {
|
||||
'title': keywords,
|
||||
'is_type': 'all',
|
||||
'is_show': '1',
|
||||
'skip_check': '0',
|
||||
'status': '1',
|
||||
'max': '120'
|
||||
}
|
||||
|
||||
resp = self.session.get(
|
||||
self.WEB_SEARCH_API,
|
||||
params=params,
|
||||
timeout=self.REQUEST_TIMEOUT,
|
||||
stream=True
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
# 记录请求时间
|
||||
self._last_request_time = time.time()
|
||||
|
||||
# 读取SSE响应内容
|
||||
response_text = resp.text
|
||||
items = self._parse_sse_response(response_text)
|
||||
|
||||
if items:
|
||||
all_results = []
|
||||
|
||||
for item in items:
|
||||
title = item.get('title', '')
|
||||
url = item.get('url', '')
|
||||
is_type = item.get('is_type', -1)
|
||||
|
||||
if not url or not title:
|
||||
continue
|
||||
|
||||
# 提取网盘类型
|
||||
pan_type = self._get_pan_type(is_type)
|
||||
pan_config = self.PAN_CONFIG.get(pan_type, self.PAN_CONFIG['other'])
|
||||
pan_name = pan_config['name']
|
||||
|
||||
# 构建显示备注
|
||||
remarks = pan_name
|
||||
|
||||
# 构建vod_id (包含完整信息)
|
||||
vod_data = {
|
||||
'title': title,
|
||||
'url': url,
|
||||
'pan_type': pan_type
|
||||
}
|
||||
|
||||
all_results.append({
|
||||
"vod_id": self._b64e(vod_data),
|
||||
"vod_name": title,
|
||||
"vod_pic": pan_config.get('icon', ''),
|
||||
"vod_remarks": remarks,
|
||||
"_pan_type": pan_type
|
||||
})
|
||||
|
||||
# 按网盘优先级排序
|
||||
priority_order = self._get_pan_priority_order()
|
||||
pan_order_map = {p: i for i, p in enumerate(priority_order)}
|
||||
|
||||
all_results.sort(key=lambda x: pan_order_map.get(x.get('_pan_type', ''), 999))
|
||||
|
||||
# 清理内部字段
|
||||
for item in all_results:
|
||||
item.pop('_pan_type', None)
|
||||
|
||||
# 分页
|
||||
total = len(all_results)
|
||||
page_size = 100
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
paged_results = all_results[start_idx:end_idx]
|
||||
|
||||
result.update({
|
||||
'list': paged_results,
|
||||
'total': total,
|
||||
'pagecount': max(1, (total + page_size - 1) // page_size),
|
||||
'limit': page_size
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情内容"""
|
||||
result = {'list': []}
|
||||
|
||||
if not ids or not ids[0]:
|
||||
return result
|
||||
|
||||
try:
|
||||
vod_data = self._b64d(ids[0])
|
||||
if not isinstance(vod_data, dict):
|
||||
return result
|
||||
|
||||
# 如果是榜单项,需要搜索获取网盘链接
|
||||
if vod_data.get('type') == 'ranking':
|
||||
title = vod_data.get('title', '')
|
||||
channel = vod_data.get('channel', '')
|
||||
if title:
|
||||
# 自动搜索该标题的网盘资源
|
||||
search_result = self._perform_search(title, "1")
|
||||
if search_result.get('list'):
|
||||
# 获取第一条搜索结果
|
||||
first_result = search_result['list'][0]
|
||||
# 解码搜索结果的vod_id获取网盘信息
|
||||
search_vod_data = self._b64d(first_result['vod_id'])
|
||||
if isinstance(search_vod_data, dict):
|
||||
url = search_vod_data.get('url', '')
|
||||
pan_type = search_vod_data.get('pan_type', 'other')
|
||||
pan_config = self.PAN_CONFIG.get(pan_type, self.PAN_CONFIG['other'])
|
||||
pan_name = pan_config['name']
|
||||
|
||||
# 使用搜索结果的vod_id(包含正确的网盘链接)
|
||||
result['list'].append({
|
||||
"vod_id": first_result['vod_id'],
|
||||
"vod_name": title,
|
||||
"vod_pic": first_result.get('vod_pic', ''),
|
||||
"vod_content": f"频道: {channel}\n搜索: {title}\n网盘: {pan_name}",
|
||||
"vod_play_from": pan_name,
|
||||
"vod_play_url": f"{pan_name}${url}",
|
||||
"vod_remarks": first_result.get('vod_remarks', '')
|
||||
})
|
||||
return result
|
||||
return result
|
||||
|
||||
# 普通网盘资源详情
|
||||
title = vod_data.get('title', '未知资源')
|
||||
url = vod_data.get('url', '')
|
||||
pan_type = vod_data.get('pan_type', 'other')
|
||||
|
||||
if not url:
|
||||
return result
|
||||
|
||||
pan_config = self.PAN_CONFIG.get(pan_type, self.PAN_CONFIG['other'])
|
||||
pan_name = pan_config['name']
|
||||
|
||||
# 构建播放URL (使用push协议)
|
||||
play_url = f"{pan_name}${url}"
|
||||
|
||||
result['list'].append({
|
||||
"vod_id": ids[0],
|
||||
"vod_name": title,
|
||||
"vod_pic": pan_config.get('icon', ''),
|
||||
"vod_content": f"网盘类型: {pan_name}\n资源链接: {url}",
|
||||
"vod_play_from": pan_name,
|
||||
"vod_play_url": play_url,
|
||||
"vod_remarks": f"{pan_name}资源"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"详情解析异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
"""播放内容 - 返回push协议让App处理网盘链接"""
|
||||
result = {
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
"url": "",
|
||||
"header": self.HEADERS
|
||||
}
|
||||
|
||||
if not pid:
|
||||
return result
|
||||
|
||||
try:
|
||||
# 解析播放URL
|
||||
if '$' in pid:
|
||||
# 格式: "盘名$URL"
|
||||
parts = pid.split('$', 1)
|
||||
url = parts[1] if len(parts) > 1 else pid
|
||||
else:
|
||||
url = pid
|
||||
|
||||
# 清理URL
|
||||
url = url.strip()
|
||||
url = re.sub(r'\s+', '', url)
|
||||
|
||||
# 确保URL格式正确
|
||||
if not url.startswith(('http://', 'https://', 'magnet:')):
|
||||
if url.startswith('tps://'):
|
||||
url = 'ht' + url
|
||||
elif url.startswith('ps://'):
|
||||
url = 'http' + url
|
||||
else:
|
||||
url = 'https://' + url
|
||||
|
||||
# 使用push协议
|
||||
if not url.startswith('push://'):
|
||||
url = 'push://' + url
|
||||
|
||||
result['url'] = url
|
||||
|
||||
except Exception as e:
|
||||
print(f"播放解析异常: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
"""本地代理"""
|
||||
return None
|
||||
|
||||
|
||||
# Fongmi 爬虫入口
|
||||
Spider = DyuziPanSpider
|
||||
@@ -0,0 +1,360 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class DyuziPanSpider(Spider):
|
||||
"""心跳4k剧场网盘资源搜索爬虫 - 终极修复流解析与新增动漫分组版"""
|
||||
|
||||
SITE_URL = "https://ppan.dyuzi.com"
|
||||
WEB_SEARCH_API = f"{SITE_URL}/api/other/web_search"
|
||||
HOME_API = f"{SITE_URL}/api/frontend/home"
|
||||
RANKING_API = f"{SITE_URL}/api/frontend/ranking"
|
||||
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||
"Accept": "text/event-stream, application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Referer": SITE_URL,
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
|
||||
REQUEST_TIMEOUT = 60
|
||||
MAX_RETRIES = 3
|
||||
BACKOFF_FACTOR = 0.5
|
||||
REQUEST_DELAY = 0.5
|
||||
|
||||
IS_TYPE_MAP = {
|
||||
0: 'quark', 1: 'uc', 2: 'baidu', 3: 'aliyun', 4: 'xunlei', 5: 'a189', 6: 'quark'
|
||||
}
|
||||
|
||||
PAN_CONFIG = {
|
||||
'quark': {'name': '夸克云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/quark.webp'},
|
||||
'uc': {'name': 'UC网盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/uc.webp'},
|
||||
'a189': {'name': '天翼云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/189.webp'},
|
||||
'aliyun': {'name': '阿里云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/aliyun.webp'},
|
||||
'baidu': {'name': '百度网盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/baidu.webp'},
|
||||
'xunlei': {'name': '迅雷云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/xunlei.webp'},
|
||||
'magnet': {'name': '磁力链接', 'icon': ''},
|
||||
'other': {'name': '其他网盘', 'icon': ''}
|
||||
}
|
||||
|
||||
_PSQ_GROUP_ORDER = ["quark", "uc", "aliyun", "a189", "baidu", "xunlei", "magnet", "other"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pan_priority = ''
|
||||
self._last_request_time = 0
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.HEADERS)
|
||||
|
||||
retries = Retry(total=self.MAX_RETRIES, backoff_factor=self.BACKOFF_FACTOR, status_forcelist=[429, 500, 502, 503, 504], raise_on_status=False)
|
||||
self.session.mount('http://', HTTPAdapter(max_retries=retries))
|
||||
self.session.mount('https://', HTTPAdapter(max_retries=retries))
|
||||
|
||||
def init(self, extend):
|
||||
try:
|
||||
extend_dict = json.loads(extend) if extend else {}
|
||||
self.pan_priority = extend_dict.get('pan_priority', 'quark,a189,uc')
|
||||
except json.JSONDecodeError:
|
||||
self.pan_priority = 'quark,a189,uc'
|
||||
|
||||
def getName(self): return "盘搜"
|
||||
def isVideoFormat(self, url): return False
|
||||
def manualVideoCheck(self): return False
|
||||
|
||||
# ======= 1. 分类栏增加动漫类别 =======
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{"type_id": "1", "type_name": "电视剧"},
|
||||
{"type_id": "2", "type_name": "电影"},
|
||||
{"type_id": "3", "type_name": "动漫"} # 新增动漫分类
|
||||
],
|
||||
'filters': {},
|
||||
'list': []
|
||||
}
|
||||
|
||||
# ======= 2. 首页推荐位混合展现(电视剧+动漫) =======
|
||||
def homeVideoContent(self):
|
||||
vod_list = []
|
||||
# 抓取电视剧推荐 (前12个)
|
||||
try:
|
||||
resp = self.session.get(self.RANKING_API, params={'channel': '电视剧', 'limit': 12}, timeout=self.REQUEST_TIMEOUT)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data.get('code') == 0 and data.get('data', {}).get('list'):
|
||||
for item in data['data']['list']:
|
||||
vod_list.append({
|
||||
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
|
||||
"vod_name": item.get('title', ''),
|
||||
"vod_pic": item.get('src', ''),
|
||||
"vod_remarks": f"剧集|热度:{item.get('hot_score', '0')[:4]}"
|
||||
})
|
||||
except: pass
|
||||
|
||||
# 抓取动漫推荐 (追加12个)
|
||||
try:
|
||||
resp = self.session.get(self.RANKING_API, params={'channel': '动漫', 'limit': 12}, timeout=self.REQUEST_TIMEOUT)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data.get('code') == 0 and data.get('data', {}).get('list'):
|
||||
for item in data['data']['list']:
|
||||
vod_list.append({
|
||||
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
|
||||
"vod_name": item.get('title', ''),
|
||||
"vod_pic": item.get('src', ''),
|
||||
"vod_remarks": f"动漫|热度:{item.get('hot_score', '0')[:4]}"
|
||||
})
|
||||
except: pass
|
||||
|
||||
return {'list': vod_list}
|
||||
|
||||
# ======= 3. 分类点击切换逻辑(支持动漫频道) =======
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
try:
|
||||
channel_map = {'1': '电视剧', '2': '电影', '3': '动漫'}
|
||||
channel = channel_map.get(str(cid), '电视剧')
|
||||
|
||||
resp = self.session.get(self.RANKING_API, params={'channel': channel, 'limit': 30}, timeout=self.REQUEST_TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
vod_list = []
|
||||
if data.get('code') == 0 and data.get('data', {}).get('list'):
|
||||
for item in data['data']['list']:
|
||||
vod_list.append({
|
||||
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
|
||||
"vod_name": item.get('title', ''),
|
||||
"vod_pic": item.get('src', ''),
|
||||
"vod_remarks": f"评分:{item.get('score_avg', '0')}"
|
||||
})
|
||||
return {'list': vod_list, 'page': 1, 'pagecount': 1, 'limit': 30, 'total': len(vod_list)}
|
||||
except: return {'list': []}
|
||||
|
||||
def _get_pan_type(self, is_type): return self.IS_TYPE_MAP.get(is_type, 'other')
|
||||
|
||||
def _b64e(self, obj):
|
||||
text = json.dumps(obj, ensure_ascii=False, separators=(",", ":")) if not isinstance(obj, str) else obj
|
||||
return base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
|
||||
|
||||
def _b64d(self, s):
|
||||
try:
|
||||
s += "=" * (-len(s) % 4)
|
||||
decoded = base64.urlsafe_b64decode(s.encode()).decode()
|
||||
try: return json.loads(decoded)
|
||||
except: return decoded
|
||||
except: return s
|
||||
|
||||
def _parse_sse_response(self, response_text):
|
||||
results = []
|
||||
if not response_text: return results
|
||||
for line in response_text.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('data:') and '[DONE]' not in line:
|
||||
try:
|
||||
data = json.loads(line[5:].strip())
|
||||
if 'title' in data and 'url' in data: results.append(data)
|
||||
except: continue
|
||||
return results
|
||||
|
||||
def _psq_quality_score(self, title):
|
||||
score = 0
|
||||
t_upper = title.upper()
|
||||
if "杜比" in title or "DOLBY" in t_upper or "DOVI" in t_upper: score += 120000
|
||||
if "DV" in t_upper: score += 100000
|
||||
if "高码" in title or "HQ" in t_upper: score += 90000
|
||||
if "HDR10+" in t_upper: score += 85000
|
||||
if "HDR10" in t_upper: score += 80000
|
||||
if "HDR" in t_upper: score += 75000
|
||||
if "4K" in t_upper or "2160P" in t_upper or "UHD" in t_upper: score += 65000
|
||||
if "1080P" in t_upper or "FHD" in t_upper: score += 45000
|
||||
if "蓝光" in title or "BLURAY" in t_upper: score += 40000
|
||||
if "REMUX" in t_upper: score += 35000
|
||||
return score
|
||||
|
||||
def _psq_extract_size_gb(self, title):
|
||||
try:
|
||||
match = re.search(r'([0-9]+(?:\.[0-9]+)?)\s*([mMgGtT])[bB]?', title)
|
||||
if match:
|
||||
val = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
if unit == 't': return val * 1024
|
||||
if unit == 'g': return val
|
||||
if unit == 'm': return val / 1024
|
||||
except: pass
|
||||
return 0.0
|
||||
|
||||
def _clean_resource_title(self, title):
|
||||
t = re.sub(r'https?://\S+', '', title)
|
||||
t = re.sub(r'\[夸克网盘\]|\[UC网盘\]|\[天翼云盘\]|微云|百度云', '', t)
|
||||
t = re.sub(r'^\s*【.*?】|^\s*\[.*?\]', '', t)
|
||||
t = t.split('◆')[0].split('▶')[0]
|
||||
t = re.sub(r'\s+', ' ', t).strip()
|
||||
return t if t else title
|
||||
|
||||
def _secure_fetch_items(self, keywords):
|
||||
elapsed = time.time() - self._last_request_time
|
||||
if elapsed < self.REQUEST_DELAY: time.sleep(self.REQUEST_DELAY - elapsed)
|
||||
try:
|
||||
params = {'title': keywords, 'is_type': 'all', 'is_show': '1', 'skip_check': '0', 'status': '1', 'max': '120'}
|
||||
resp = self.session.get(self.WEB_SEARCH_API, params=params, timeout=self.REQUEST_TIMEOUT)
|
||||
self._last_request_time = time.time()
|
||||
if resp.status_code == 200:
|
||||
return self._parse_sse_response(resp.text)
|
||||
except Exception as e:
|
||||
print(f"[DyuziPan] 安全拉取接口异常被拦截: {e}")
|
||||
return []
|
||||
|
||||
def searchContent(self, key, quick, pg="1"): return self._perform_search(key, pg)
|
||||
def searchContentPage(self, key, quick, page): return self._perform_search(key, page)
|
||||
|
||||
def _perform_search(self, keywords, page_str):
|
||||
try: page = int(page_str)
|
||||
except: page = 1
|
||||
result = {'list': [], 'page': page, 'pagecount': 1, 'limit': 60, 'total': 0}
|
||||
if not keywords or page > 1: return result
|
||||
|
||||
items = self._secure_fetch_items(keywords)
|
||||
if not items: return result
|
||||
|
||||
merged_resources = {}
|
||||
for item in items:
|
||||
title, url, is_type = item.get('title', ''), item.get('url', ''), item.get('is_type', -1)
|
||||
if not url or not title: continue
|
||||
|
||||
pan_type = self._get_pan_type(is_type)
|
||||
clean_name = self._clean_resource_title(title)
|
||||
group_key = f"{pan_type}_{clean_name}"
|
||||
|
||||
if group_key not in merged_resources:
|
||||
merged_resources[group_key] = {
|
||||
'clean_name': clean_name,
|
||||
'pan_type': pan_type,
|
||||
'score': self._psq_quality_score(title) + self._psq_extract_size_gb(title),
|
||||
'links': []
|
||||
}
|
||||
merged_resources[group_key]['links'].append({'title': title, 'url': url})
|
||||
|
||||
sorted_resources = list(merged_resources.values())
|
||||
sorted_resources.sort(key=lambda x: x['score'], reverse=True)
|
||||
|
||||
for res in sorted_resources:
|
||||
pan_cfg = self.PAN_CONFIG.get(res['pan_type'], self.PAN_CONFIG['other'])
|
||||
display_name = f"[{pan_cfg['name']}] {res['clean_name']}"
|
||||
result['list'].append({
|
||||
"vod_id": self._b64e({'is_group': True, 'resource_name': display_name, 'pan_type': res['pan_type'], 'links': res['links']}),
|
||||
"vod_name": display_name,
|
||||
"vod_pic": pan_cfg.get('icon', ''),
|
||||
"vod_remarks": f"包含 {len(res['links'])} 个文件"
|
||||
})
|
||||
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
if not ids or not ids[0]: return result
|
||||
try:
|
||||
vod_data = self._b64d(ids[0])
|
||||
if not isinstance(vod_data, dict): return result
|
||||
|
||||
# ─── 首页排行榜/推荐页点击 ───
|
||||
if vod_data.get('type') == 'ranking':
|
||||
search_title = vod_data.get('title', '')
|
||||
if not search_title: return result
|
||||
|
||||
items = self._secure_fetch_items(search_title)
|
||||
if not items:
|
||||
result['list'].append({
|
||||
"vod_id": ids[0], "vod_name": search_title, "vod_pic": "",
|
||||
"vod_content": f"提示:未在当前接口中检索到该动漫/影视的网盘分享。",
|
||||
"vod_play_from": "暂无资源", "vod_play_url": "点击刷新重试$push://https://ppan.dyuzi.com", "vod_remarks": "无资源"
|
||||
})
|
||||
return result
|
||||
|
||||
buckets = {k: [] for k in self._PSQ_GROUP_ORDER}
|
||||
for item in items:
|
||||
title, url, is_type = item.get('title', ''), item.get('url', ''), item.get('is_type', -1)
|
||||
if not url or not title: continue
|
||||
pt = self._get_pan_type(is_type)
|
||||
if pt not in buckets: pt = 'other'
|
||||
buckets[pt].append({'title': title, 'url': url})
|
||||
|
||||
play_from_list = []
|
||||
play_url_list = []
|
||||
|
||||
for group_key in self._PSQ_GROUP_ORDER:
|
||||
b_links = buckets.get(group_key, [])
|
||||
if not b_links: continue
|
||||
play_from_list.append(self.PAN_CONFIG.get(group_key, self.PAN_CONFIG['other'])['name'])
|
||||
|
||||
eps = []
|
||||
for idx, item in enumerate(b_links):
|
||||
clean_ep = item['title'].replace('$', '').replace('#', '').strip()
|
||||
if len(clean_ep) > 60: clean_ep = f"进入云盘播放-{idx+1}"
|
||||
eps.append(f"{clean_ep}${item['url']}")
|
||||
play_url_list.append("#".join(eps))
|
||||
|
||||
if play_from_list:
|
||||
result['list'].append({
|
||||
"vod_id": ids[0],
|
||||
"vod_name": search_title,
|
||||
"vod_pic": "",
|
||||
"vod_content": f"资源名称: {search_title}\n系统已为您全网智能检索相关网盘源。",
|
||||
"vod_play_from": "$$$".join(play_from_list),
|
||||
"vod_play_url": "$$$".join(play_url_list),
|
||||
"vod_remarks": f"聚合 {len(play_from_list)} 个网盘线路"
|
||||
})
|
||||
return result
|
||||
|
||||
# ─── 搜索页点击 ───
|
||||
resource_name = vod_data.get('resource_name', '网盘资源')
|
||||
pan_type = vod_data.get('pan_type', 'other')
|
||||
links = vod_data.get('links', [])
|
||||
|
||||
pan_cfg = self.PAN_CONFIG.get(pan_type, self.PAN_CONFIG['other'])
|
||||
episode_strings = []
|
||||
for idx, item in enumerate(links):
|
||||
clean_ep_title = item['title'].replace('$', '').replace('#', '').strip()
|
||||
if len(clean_ep_title) > 60: clean_ep_title = f"打开云盘-{idx+1}"
|
||||
episode_strings.append(f"{clean_ep_title}${item['url']}")
|
||||
|
||||
result['list'].append({
|
||||
"vod_id": ids[0],
|
||||
"vod_name": resource_name,
|
||||
"vod_pic": pan_cfg.get('icon', ''),
|
||||
"vod_content": f"资源名称: {resource_name}\n专属线路: {pan_cfg['name']}",
|
||||
"vod_play_from": pan_cfg['name'],
|
||||
"vod_play_url": "#".join(episode_strings),
|
||||
"vod_remarks": f"共 {len(links)} 个资源版本"
|
||||
})
|
||||
except Exception as e:
|
||||
print("[DyuziPan] 详情页分栏异常:", e)
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
result = {"parse": 0, "jx": 0, "url": "", "header": self.HEADERS}
|
||||
if not pid: return result
|
||||
try:
|
||||
url = pid.split('$', 1)[1] if '$' in pid else pid
|
||||
url = url.strip().replace(' ', '')
|
||||
if not url.startswith(('http://', 'https://', 'magnet:')):
|
||||
url = 'https://' + url
|
||||
if not url.startswith('push://'):
|
||||
url = 'push://' + url
|
||||
result['url'] = url
|
||||
except: pass
|
||||
return result
|
||||
|
||||
def localProxy(self, params): return None
|
||||
|
||||
Spider = DyuziPanSpider
|
||||
@@ -0,0 +1,343 @@
|
||||
# coding = utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://app.whjzjx.cn"
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Linux; Android 12; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36'
|
||||
}
|
||||
|
||||
headerf = {
|
||||
"platform": "1",
|
||||
"user_agent": "Mozilla/5.0 (Linux; Android 9; V1938T Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36",
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
times = int(time.time() * 1000)
|
||||
|
||||
data = {
|
||||
"device": "2a50580e69d38388c94c93605241fb306",
|
||||
"package_name": "com.jz.xydj",
|
||||
"android_id": "ec1280db12795506",
|
||||
"install_first_open": True,
|
||||
"first_install_time": 1752505243345,
|
||||
"last_update_time": 1752505243345,
|
||||
"report_link_url": "",
|
||||
"authorization": "",
|
||||
"timestamp": times
|
||||
}
|
||||
|
||||
plain_text = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
key = "B@ecf920Od8A4df7"
|
||||
key_bytes = key.encode('utf-8')
|
||||
plain_bytes = plain_text.encode('utf-8')
|
||||
cipher = AES.new(key_bytes, AES.MODE_ECB)
|
||||
padded_data = pad(plain_bytes, AES.block_size)
|
||||
ciphertext = cipher.encrypt(padded_data)
|
||||
encrypted = base64.b64encode(ciphertext).decode('utf-8')
|
||||
|
||||
response = requests.post("https://u.shytkjgs.com/user/v3/account/login", headers=headerf, data=encrypted)
|
||||
response_data = response.json()
|
||||
Authorization = response_data['data']['token']
|
||||
|
||||
headerx = {
|
||||
'authorization': Authorization,
|
||||
'platform': '1',
|
||||
'version_name': '3.8.3.1'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
global headers
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result = {"class": [{"type_id": "1", "type_name": "剧场"},
|
||||
{"type_id": "3", "type_name": "新剧"},
|
||||
{"type_id": "2", "type_name": "热播"},
|
||||
{"type_id": "7", "type_name": "星选"},
|
||||
{"type_id": "5", "type_name": "阳光"}],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
|
||||
url= f'{xurl}/v1/theater/home_page?theater_class_id=1&class2_id=4&page_num=1&page_size=24'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['list']:
|
||||
|
||||
name = vod['theater']['title']
|
||||
|
||||
id = vod['theater']['id']
|
||||
|
||||
pic = vod['theater']['cover_url']
|
||||
|
||||
remark = vod['theater']['play_amount_str']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
url = f'{xurl}/v1/theater/home_page?theater_class_id={cid}&page_num={pg}&page_size=24'
|
||||
detail = requests.get(url=url,headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['list']:
|
||||
|
||||
name = vod['theater']['title']
|
||||
|
||||
id = vod['theater']['id']
|
||||
|
||||
pic = vod['theater']['cover_url']
|
||||
|
||||
remark = vod['theater']['theme']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
xianlu = ''
|
||||
bofang = ''
|
||||
|
||||
url = f'{xurl}/v2/theater_parent/detail?theater_parent_id={did}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
url = 'https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/1732707176882/jiduo.txt'
|
||||
response = requests.get(url)
|
||||
response.encoding = 'utf-8'
|
||||
code = response.text
|
||||
name = self.extract_middle_text(code, "s1='", "'", 0)
|
||||
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
|
||||
|
||||
content = '剧情:' + data['data']['introduction']
|
||||
|
||||
area = data['data']['desc_tags'][0]
|
||||
|
||||
remarks = data['data']['filing']
|
||||
|
||||
# 修复剧集只有一集的问题 - 检查theaters数据是否存在且不为空
|
||||
if 'theaters' in data['data'] and data['data']['theaters']:
|
||||
for sou in data['data']['theaters']:
|
||||
id = sou['son_video_url']
|
||||
name = sou['num']
|
||||
bofang = bofang + str(name) + '$' + id + '#'
|
||||
|
||||
bofang = bofang[:-1] if bofang.endswith('#') else bofang
|
||||
xianlu = '星芽'
|
||||
else:
|
||||
# 如果没有theaters数据,检查是否有单个视频URL
|
||||
if 'video_url' in data['data'] and data['data']['video_url']:
|
||||
bofang = '1$' + data['data']['video_url']
|
||||
xianlu = '星芽'
|
||||
else:
|
||||
bofang = Jumps
|
||||
xianlu = '1'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_content": content,
|
||||
"vod_remarks": remarks,
|
||||
"vod_area": area,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = id
|
||||
result["header"] = headers
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
payload = {
|
||||
"text": key
|
||||
}
|
||||
|
||||
url = f"{xurl}/v3/search"
|
||||
detail = requests.post(url=url, headers=headerx, json=payload)
|
||||
if detail.status_code == 200:
|
||||
detail.encoding = "utf-8"
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['theater']['search_data']:
|
||||
|
||||
name = vod['title']
|
||||
|
||||
id = vod['id']
|
||||
|
||||
pic = vod['cover_url']
|
||||
|
||||
remark = vod['score_str']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = page
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import base64
|
||||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Util.Padding import pad
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend='{}'):
|
||||
self.session = requests.session()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://film.symx.club'
|
||||
RSA_N = "c1e3934d1614465b33053e7f48ee4ec87b14b95ef88947713d25eecbff7e74c7977d02dc1d9451f79dd5d1c10c29acb6a9b4d6fb7d0a0279b6719e1772565f09af627715919221aef91899cae08c0d686d748b20a3603be2318ca6bc2b59706592a9219d0bf05c9f65023a21d2330807252ae0066d59ceefa5f2748ea80bab81"
|
||||
RSA_E = 65537
|
||||
STATIC_BASE = "https://static.geetest.com/"
|
||||
VERIFY_HOST = 'https://gcaptcha4.geetest.com'
|
||||
ClientId=MD5.new(str(int(time.time())).encode()).hexdigest();Token=''
|
||||
|
||||
def rsa_encrypt(self,random_key):
|
||||
pub_key = RSA.construct((int(self.RSA_N, 16), self.RSA_E))
|
||||
cipher = PKCS1_v1_5.new(pub_key)
|
||||
return cipher.encrypt(random_key.encode('utf-8')).hex()
|
||||
|
||||
def aes_encrypt(self,plaintext, key):
|
||||
iv = b"0000000000000000"
|
||||
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv)
|
||||
padded_data = pad(plaintext.encode('utf-8'), AES.block_size)
|
||||
return cipher.encrypt(padded_data).hex()
|
||||
|
||||
def get_w(self, payload_dict):
|
||||
random_key = "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
||||
json_str = json.dumps(payload_dict, separators=(',', ':'))
|
||||
return self.aes_encrypt(json_str, random_key) + self.rsa_encrypt(random_key)
|
||||
|
||||
def get_dynamic_payload(self,lot_number, set_left, passtime, captcha_id, pow_detail):
|
||||
key_name = lot_number[26:30] + lot_number[12:16]
|
||||
sub_key = lot_number[16:24]
|
||||
val = lot_number[6:10]
|
||||
pow_msg = f"1|0|md5|{pow_detail['datetime']}|{captcha_id}|{lot_number}||{secrets.token_hex(8) }"
|
||||
pow_sign = hashlib.md5(pow_msg.encode()).hexdigest()
|
||||
payload = {
|
||||
"setLeft": set_left,
|
||||
"passtime": passtime,
|
||||
"userresponse": set_left / 1.0059466666666665 +2,
|
||||
"device_id": "",
|
||||
"lot_number": lot_number,
|
||||
"pow_msg": pow_msg,
|
||||
"pow_sign": pow_sign,
|
||||
"geetest": "captcha",
|
||||
"lang": "zh",
|
||||
"ep": "123",
|
||||
"biht": "1426265548",
|
||||
"yDWL": "hZGx",
|
||||
key_name: {sub_key: val},
|
||||
"em": {"ph": 0, "cp": 0, "ek": "11", "wd": 1, "nt": 0, "si": 0, "sc": 0}
|
||||
}
|
||||
return payload
|
||||
|
||||
def generate_checksum_timestamp(self):
|
||||
r = str(int(time.time() * 1000))
|
||||
prefix = r[:-1]
|
||||
digit_sum = sum(int(d) for d in prefix)
|
||||
check_digit = digit_sum % 10
|
||||
return prefix + str(check_digit)
|
||||
|
||||
def get_site_headers(self,path, end=0):
|
||||
secret_key = "lslx_sk"
|
||||
timestamp = self.generate_checksum_timestamp()
|
||||
raw_data = f"{timestamp}symx_{secret_key}{path}"
|
||||
arranged = raw_data.replace("1", "i").replace("0", "o").replace("5", "s")
|
||||
if end:
|
||||
secret_key = ''
|
||||
arranged = ''
|
||||
signature = hmac.new(
|
||||
secret_key.encode('utf-8'),
|
||||
arranged.encode('utf-8'),
|
||||
digestmod=hashlib.sha256
|
||||
).hexdigest()
|
||||
header = {
|
||||
'User-Agent': 'SYMX_ANDROID',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 13; M2012K10C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36 uni-app Html5Plus/1.0 (Immersed/30.545454)',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'X-Platform': 'android',
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Sign-X': signature,
|
||||
'X-Client-Id':self.ClientId,
|
||||
'Referer': 'https://film.symx.club/',
|
||||
}
|
||||
if self.Token: header['X-Verify-Token'] = self.Token
|
||||
if end:
|
||||
del header['X-Sign-X']
|
||||
header['X-Report-Id'] = signature
|
||||
return header
|
||||
|
||||
def run_verify(self,i=0):
|
||||
if i>3: return
|
||||
try:
|
||||
config = self.session.get(f'{self.host}/api/auth/verify/config',
|
||||
headers=self.get_site_headers('/auth/verify/config')).json()
|
||||
captcha_id = config['data']['captchaId']
|
||||
params = {
|
||||
'callback': f'geetest_{int(time.time()* 1000)}',
|
||||
'captcha_id': captcha_id,
|
||||
'challenge': str(uuid.uuid4()),
|
||||
'client_type': 'web',
|
||||
'lang': 'zho',
|
||||
}
|
||||
load_res = self.fetch(f'{self.VERIFY_HOST}/load', params=params).text
|
||||
data = json.loads(load_res[len(params['callback']) + 1:-1])['data']
|
||||
t=str(int(time.time() * 1000))
|
||||
heade={
|
||||
'timestamp':t,
|
||||
"sign":MD5.new(f'44344434tffrfeeffgdggdg{t}'.encode()).hexdigest()
|
||||
}
|
||||
|
||||
body={
|
||||
'type':'solve',
|
||||
'bg':base64.b64encode(self.fetch(f"{self.STATIC_BASE}{data['bg']}").content).decode(),
|
||||
'hb':base64.b64encode(self.fetch(f"{self.STATIC_BASE}{data['slice']}").content).decode(),
|
||||
}
|
||||
resp=self.post("http://mytv6688.xyz/aowuapp",json=body,headers=heade).json()
|
||||
print("验证结果1:", resp)
|
||||
pass_time = random.randint(1200, 2200)
|
||||
inner_payload = self.get_dynamic_payload(data['lot_number'], resp["result"], pass_time, captcha_id,
|
||||
data['pow_detail'])
|
||||
verify_params = {
|
||||
"callback": f"geetest_{int(time.time() * 1000)}",
|
||||
"captcha_id": captcha_id,
|
||||
"client_type": "web",
|
||||
"lot_number": data['lot_number'],
|
||||
"payload": data['payload'],
|
||||
"process_token": data['process_token'],
|
||||
"payload_protocol": data['payload_protocol'],
|
||||
"pt": data['pt'],
|
||||
"w": self.get_w(inner_payload)
|
||||
}
|
||||
verify_res_raw = self.fetch(f'{self.VERIFY_HOST}/verify', params=verify_params).text
|
||||
print("验证结果2:", verify_res_raw)
|
||||
verify_data = json.loads(verify_res_raw[len(verify_params['callback']) + 1:-1])
|
||||
sc = verify_data['data']['seccode']
|
||||
json_body = {
|
||||
"captchaId": sc['captcha_id'],
|
||||
"captchaOutput": sc['captcha_output'],
|
||||
"genTime": int(sc['gen_time']),
|
||||
"lotNumber": sc['lot_number'],
|
||||
"passToken": sc['pass_token']
|
||||
}
|
||||
final_res = self.session.post(f'{self.host}/api/auth/verify', headers=self.get_site_headers("/auth/verify"),
|
||||
json=json_body)
|
||||
print("验证结果3:", final_res.text)
|
||||
self.Token = final_res.json()["data"]["token"]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return self.run_verify(i+1)
|
||||
|
||||
def Req(self,path,params,i=0):
|
||||
self.session.headers.update(self.get_site_headers(path.split("/api")[-1], i))
|
||||
resp=self.session.get(f"{self.host}{path}",params=params)
|
||||
if '完成验证' in resp.text:
|
||||
self.run_verify()
|
||||
self.session.headers.update(self.get_site_headers(path.split("/api")[-1], i))
|
||||
resp = self.session.get(f"{self.host}{path}",params=params)
|
||||
print(resp.status_code)
|
||||
# print(resp.text)
|
||||
return resp.json()
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.Req("/api/category/top",{},1)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data['data']:
|
||||
classes.append({
|
||||
'type_name': k['name'],
|
||||
'type_id': k['id']
|
||||
})
|
||||
# fil = []
|
||||
# resp=self.Req("/api/film/category/filter",{'categoryId':k['id']},1)
|
||||
# for i,v in resp['data'].items():
|
||||
# if not isinstance(v,list) or len(v)==0 or i=='sortOptions':continue
|
||||
# fil.append({
|
||||
# 'key': i,
|
||||
# 'name': i,
|
||||
# 'value': [{'n':x,'v':x} for x in v]
|
||||
# })
|
||||
# fil.append(self.ddd)
|
||||
# filters[k['id']] = fil
|
||||
result['class'] = classes
|
||||
result['filters'] = self.fetch("http://mytv6688.xyz/pyplugin/木兮筛选.json").json()
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data=self.Req("/api/poster/list",{},1)
|
||||
vlist = []
|
||||
for k in data['data']:
|
||||
vlist.append({
|
||||
'vod_id': k.get('filmId'),
|
||||
'vod_name': k.get('filmName'),
|
||||
'vod_pic': k.get('poster'),
|
||||
})
|
||||
return {'list':vlist}
|
||||
|
||||
def getList(self,data):
|
||||
vlist = []
|
||||
for k in data:
|
||||
vlist.append({
|
||||
'vod_id': k.get('id'),
|
||||
'vod_name': k.get('name'),
|
||||
'vod_pic': k.get('cover'),
|
||||
'vod_remarks': k.get('updateStatus'),
|
||||
})
|
||||
return vlist
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params={
|
||||
"area": extend.get('areaOptions', ''),
|
||||
"childCategoryId": "",
|
||||
"categoryId": tid,
|
||||
"language": extend.get('languageOptions', ''),
|
||||
"pageNum": pg,
|
||||
"pageSize": "10",
|
||||
"sort": extend.get('sortOptions', ''),
|
||||
"year": extend.get('yearOptions', '')
|
||||
}
|
||||
resp=self.Req("/api/film/category/list", params)
|
||||
result = {}
|
||||
result['list'] =self.getList(resp['data']['list'])
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
resp=self.Req("/api/film/detail/play/app",{'id': ids[0]})
|
||||
v=resp['data']
|
||||
n,p=[],[]
|
||||
for i in v.get('playLineList'):
|
||||
n.append(i['playerName'])
|
||||
m=[f"{j['name']}${j['id']}" for j in i.get('lines')]
|
||||
p.append('#'.join(m))
|
||||
vod = {
|
||||
'type_name': v.get('categoryName'),
|
||||
'vod_year': v.get('year'),
|
||||
'vod_area': v.get('area'),
|
||||
'vod_remarks': v.get('updateStatus'),
|
||||
'vod_actor': v.get('actor'),
|
||||
'vod_director': '云霄仙子(困困版)',
|
||||
'vod_content': v.get('blurb'),
|
||||
'vod_play_from': '$$$'.join(n),
|
||||
'vod_play_url': '$$$'.join(p)
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
params={
|
||||
"pageNum": pg,
|
||||
"pageSize": "10",
|
||||
"keyword": key
|
||||
}
|
||||
resp=self.Req('/api/film/search',params=params)
|
||||
return {'list':self.getList( resp['data']['list']),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
resp=self.Req("/api/line/play/parse", {"lineId": id})
|
||||
return {'parse': 0, 'url': resp['data'], 'header': ''}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sp = Spider()
|
||||
formatJo = sp.init()
|
||||
formatJo = sp.homeContent(False) # 主页,等于真表示启用筛选
|
||||
# formatJo = sp.homeVideoContent() # 主页视频
|
||||
# formatJo = sp.searchContent("斗罗",False,'1') # 搜索{"area":"大陆","by":"hits","class":"国产","lg":"国语"}
|
||||
# formatJo = sp.categoryContent('2', '1', False, {}) # 分类
|
||||
# formatJo = sp.detailContent(['126634']) # 详情
|
||||
# formatJo = sp.playerContent("","https://www.yingmeng.net/vodplay/140148-2-1.html",{}) # 播放
|
||||
# formatJo = sp.localProxy({"":"https://www.yingmeng.net/vodplay/140148-2-1.html"}) # 播放
|
||||
pprint(formatJo)
|
||||
@@ -0,0 +1,392 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re, urllib.parse
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.ht10010.com"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return '枫叶影院'
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {"class": [
|
||||
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
|
||||
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
|
||||
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
|
||||
{"type_id": "2", "type_name": "电视剧"},
|
||||
{"type_id": "1", "type_name": "电影"},
|
||||
{"type_id": "4", "type_name": "动漫"},
|
||||
{"type_id": "3", "type_name": "综艺"},
|
||||
{"type_id": "5", "type_name": "热门短剧"},
|
||||
], "filters": self._build_filters()}
|
||||
|
||||
def _build_filters(self):
|
||||
area = [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "泰国", "v": "泰国"}, {"n": "新加坡", "v": "新加坡"},
|
||||
{"n": "马来西亚", "v": "马来西亚"}, {"n": "印度", "v": "印度"}, {"n": "英国", "v": "英国"},
|
||||
{"n": "法国", "v": "法国"}, {"n": "加拿大", "v": "加拿大"}, {"n": "西班牙", "v": "西班牙"},
|
||||
{"n": "俄罗斯", "v": "俄罗斯"}, {"n": "其它", "v": "其它"}]
|
||||
year = [{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"}, {"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": "2004", "v": "2004"}]
|
||||
lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
|
||||
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
|
||||
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
|
||||
{"n": "其它", "v": "其它"}]
|
||||
sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}]
|
||||
letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"},
|
||||
{"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"},
|
||||
{"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"},
|
||||
{"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"},
|
||||
{"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"},
|
||||
{"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"},
|
||||
{"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}]
|
||||
return {
|
||||
"2": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"},
|
||||
{"n": "海外剧", "v": "16"}]},
|
||||
{"key": "area", "name": "地区", "value": area},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("古装", "古装"), ("战争", "战争"),
|
||||
("青春偶像", "青春偶像"), ("喜剧", "喜剧"),
|
||||
("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"),
|
||||
("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"),
|
||||
("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"),
|
||||
("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"1": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
|
||||
{"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"},
|
||||
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]},
|
||||
{"key": "area", "name": "地区", "value": area},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"),
|
||||
("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"),
|
||||
("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"),
|
||||
("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"),
|
||||
("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"),
|
||||
("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"),
|
||||
("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"),
|
||||
("古装", "古装"), ("历史", "历史"), ("运动", "运动"),
|
||||
("农村", "农村"), ("儿童", "儿童"),
|
||||
("网络电影", "网络电影")]]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"4": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("情感", "情感"), ("科幻", "科幻"),
|
||||
("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"),
|
||||
("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"),
|
||||
("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"),
|
||||
("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"),
|
||||
("励志", "励志"), ("其他", "其他")]]},
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
|
||||
{"n": "其它", "v": "其它"}]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"3": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("选秀", "选秀"), ("情感", "情感"),
|
||||
("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"),
|
||||
("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"),
|
||||
("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]},
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
html = self._fetch('/')
|
||||
return {"list": self._parse_video_list(html)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
# 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter
|
||||
if tid.startswith('/label'):
|
||||
url = f'{tid}/page/{pg}.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
page = int(pg)
|
||||
page_count = page if len(items) < 24 else page + 2
|
||||
return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24}
|
||||
|
||||
args = {}
|
||||
if extend and isinstance(extend, dict):
|
||||
for k, v in extend.items():
|
||||
if v:
|
||||
args[k] = str(v)
|
||||
if isinstance(filter, dict):
|
||||
for k, v in filter.items():
|
||||
if v and k not in args:
|
||||
args[k] = str(v)
|
||||
route_tid = args.get('class', args.get('tid', str(tid)))
|
||||
area = args.get('area', '')
|
||||
genre = args.get('genre', '')
|
||||
year = args.get('year', '')
|
||||
lang = args.get('lang', '')
|
||||
letter = args.get('letter', '')
|
||||
sort = args.get('sort', '')
|
||||
# 无筛选走正常分页
|
||||
if not area and not genre and not year and not lang and not letter and not sort:
|
||||
url = f'/cupfox-list/{route_tid}--------{pg}---.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
page = int(pg)
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
pagecount = page
|
||||
for a in soup.select('a.page-link'):
|
||||
if a.text == '尾页':
|
||||
m = re.search(r'---(\d+)---', a.get('href', ''))
|
||||
if m:
|
||||
pagecount = int(m.group(1))
|
||||
break
|
||||
if not items:
|
||||
pagecount = 0
|
||||
return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999}
|
||||
# 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html
|
||||
segs = [route_tid, area, sort, genre, lang, letter, '', '', year]
|
||||
url = '/cupfox-list/' + '-'.join(segs) + '.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {"list": []}
|
||||
vid = ids[0].split(',')[0].strip()
|
||||
try:
|
||||
html = self._fetch(f'/detail/{vid}.html')
|
||||
if not html: return result
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
vod_name = soup.select_one('h3.slide-info-title')
|
||||
vod_name = vod_name.text.strip() if vod_name else ''
|
||||
vod_pic = soup.select_one('img.lazy')
|
||||
vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else ''
|
||||
vod_director = ''
|
||||
vod_actor = ''
|
||||
for el in soup.select('.slide-info'):
|
||||
text = el.get_text(' ').strip()
|
||||
if text.startswith('导演:'):
|
||||
vod_director = text.replace('导演:', '').strip()
|
||||
elif text.startswith('演员:'):
|
||||
vod_actor = text.replace('演员:', '').strip()
|
||||
vod_content = soup.select_one('#height_limit')
|
||||
vod_content = vod_content.get_text(' ', strip=True) if vod_content else ''
|
||||
play_from, play_url = [], []
|
||||
for tab in soup.select('.anthology-tab a.swiper-slide'):
|
||||
src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip()
|
||||
if src_name:
|
||||
play_from.append(src_name)
|
||||
tab_blocks = soup.select('.anthology-list-box')
|
||||
for i, block in enumerate(tab_blocks):
|
||||
ep_list = []
|
||||
for a in block.select('li a'):
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/play/(.*?)\.html', href)
|
||||
if m:
|
||||
ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}')
|
||||
ep_list.reverse()
|
||||
if ep_list and i < len(play_from):
|
||||
play_url.append('#'.join(ep_list))
|
||||
valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)]
|
||||
result["list"].append({
|
||||
"vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic,
|
||||
"vod_director": vod_director, "vod_actor": vod_actor,
|
||||
"vod_content": vod_content,
|
||||
"vod_play_from": "$$$".join(valid_from),
|
||||
"vod_play_url": "$$$".join(play_url),
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
decoded = urllib.parse.unquote(key)
|
||||
except:
|
||||
decoded = key
|
||||
html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html')
|
||||
items = self._parse_search_list(html)
|
||||
return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = ''
|
||||
try:
|
||||
url = id if id.startswith('http') else f'{self.host}/play/{id}.html'
|
||||
html = self._fetch(url)
|
||||
if html:
|
||||
m = re.search(r'player_aaaa=(.*?)</script>', html, re.S)
|
||||
if m:
|
||||
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pd = {}
|
||||
# print('pd:', pd)
|
||||
play_url = pd.get('url')
|
||||
play_id = pd.get('from')
|
||||
|
||||
api_map = {
|
||||
'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php',
|
||||
'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php',
|
||||
}
|
||||
if not play_url:
|
||||
return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8',
|
||||
"header": {'User-Agent': 'Mozilla/5.0'}}
|
||||
if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')):
|
||||
return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}}
|
||||
|
||||
else:
|
||||
headers = {
|
||||
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
||||
'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
'accept-language': "zh-CN,zh;q=0.9",
|
||||
'cache-control': "no-cache",
|
||||
'pragma': "no-cache",
|
||||
'priority': "u=0, i",
|
||||
'referer': "https://www.ht10010.com/",
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers)
|
||||
token = re.search(r'data-te="(.*?)"', response.text)
|
||||
if token:
|
||||
token = token.group(1)
|
||||
payload = {
|
||||
'url': play_url,
|
||||
'token': token
|
||||
}
|
||||
# print('payload', payload)
|
||||
try:
|
||||
response = self.post(api_map[play_id], data=payload, headers=headers)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
# print('result:', result)
|
||||
if result['code'] == 200 and 'url' in result:
|
||||
play_url = result['url']
|
||||
return {"parse": 0, "url": play_url, "header": {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'}}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {"parse": 1, "url": url}
|
||||
|
||||
def localProxy(self, param=''):
|
||||
return {}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def _fetch(self, url):
|
||||
try:
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
return rsp.text if rsp else ''
|
||||
except:
|
||||
return ''
|
||||
|
||||
def _fix_pic(self, u):
|
||||
if not u: return ''
|
||||
if u.startswith('//'): return 'https:' + u
|
||||
return u.replace('&', '&')
|
||||
|
||||
def _parse_video_list(self, html):
|
||||
videos, seen = [], set()
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
cards = soup.select('a.public-list-exp')
|
||||
for a in cards:
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not m: continue
|
||||
vod_id = m.group(1)
|
||||
if vod_id in seen: continue
|
||||
seen.add(vod_id)
|
||||
span = ','.join([span.text for span in a.select('span.public-prt')])
|
||||
# print('span', span)
|
||||
vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or ''
|
||||
pic_el = a.select_one('img')
|
||||
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
|
||||
remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb')
|
||||
vod_remarks = remark_el.text.strip() if remark_el else ''
|
||||
videos.append(
|
||||
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span})
|
||||
return videos
|
||||
|
||||
def _parse_search_list(self, html):
|
||||
videos, seen = [], set()
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
cards = soup.select('a.public-list-exp')
|
||||
for a in cards:
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not m: continue
|
||||
vod_id = m.group(1)
|
||||
if vod_id in seen: continue
|
||||
seen.add(vod_id)
|
||||
pic_el = a.select_one('img')
|
||||
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
|
||||
title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]')
|
||||
if title_el:
|
||||
vod_name = title_el.text.strip()
|
||||
else:
|
||||
vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or ''
|
||||
remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2')
|
||||
vod_remarks = remark_el.text.strip() if remark_el else ''
|
||||
videos.append(
|
||||
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
|
||||
return videos
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sp = Spider()
|
||||
sp.init()
|
||||
# 20067-5-189
|
||||
print(sp.categoryContent('/label/qq','1',True, {}))
|
||||
# print(sp.playerContent('', '20067-6-189', []))
|
||||
# print(sp.playerContent('', '20067-5-189', []))
|
||||
pass
|
||||
@@ -0,0 +1,267 @@
|
||||
# coding=utf-8
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin, quote
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
def getName(self):
|
||||
return "永乐视频"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; M2102J2SC Build/UKQ1.240624.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.86 Mobile Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Referer': 'https://www.59v.net/',
|
||||
})
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
home_url = "https://www.59v.net"
|
||||
|
||||
classes = [
|
||||
{"type_id": "1", "type_name": "电影"},
|
||||
{"type_id": "2", "type_name": "剧集"},
|
||||
{"type_id": "3", "type_name": "综艺"},
|
||||
{"type_id": "4", "type_name": "动漫"},
|
||||
]
|
||||
|
||||
filters = {
|
||||
"1": [{"key": "sub", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "动作片", "v": "6"},
|
||||
{"n": "喜剧片", "v": "7"}, {"n": "爱情片", "v": "8"},
|
||||
{"n": "科幻片", "v": "9"}, {"n": "恐怖片", "v": "10"},
|
||||
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"},
|
||||
{"n": "动漫电影", "v": "26"},
|
||||
]}],
|
||||
"2": [{"key": "sub", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "国产剧", "v": "13"},
|
||||
{"n": "港台剧", "v": "14"}, {"n": "韩国剧", "v": "15"},
|
||||
{"n": "欧美剧", "v": "16"}, {"n": "日本剧", "v": "17"},
|
||||
{"n": "泰国剧", "v": "27"},
|
||||
]}],
|
||||
"3": [{"key": "sub", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "国内综艺", "v": "18"},
|
||||
{"n": "港台综艺", "v": "19"}, {"n": "日韩综艺", "v": "20"},
|
||||
{"n": "欧美综艺", "v": "21"},
|
||||
]}],
|
||||
"4": [{"key": "sub", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "国产动漫", "v": "22"},
|
||||
{"n": "欧美动漫", "v": "23"}, {"n": "日韩动漫", "v": "24"},
|
||||
{"n": "港台动漫", "v": "25"},
|
||||
]}],
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {"class": self.classes, "filters": self.filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
resp = self.session.get(self.home_url, timeout=10)
|
||||
resp.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
for item in soup.select('.module-item'):
|
||||
a = item if item.name == 'a' else item.select_one('a')
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
if not href.startswith('/voddetail/'):
|
||||
continue
|
||||
title = a.get('title', '') or ''
|
||||
if not title:
|
||||
t = a.select_one('.module-poster-item-title')
|
||||
title = t.text.strip() if t else ''
|
||||
img = a.select_one('img')
|
||||
pic = (img.get('data-original') or img.get('src', '')) if img else ''
|
||||
note = a.select_one('.module-item-note')
|
||||
videos.append({
|
||||
"vod_id": href,
|
||||
"vod_name": title,
|
||||
"vod_pic": urljoin(self.home_url, pic),
|
||||
"vod_remarks": note.text.strip() if note else '',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
videos = []
|
||||
page = int(pg) if str(pg).isdigit() else 1
|
||||
sub = extend.get("sub", "") if isinstance(extend, dict) else ""
|
||||
cid = sub if sub else tid
|
||||
url = f"{self.home_url}/vodshow/{cid}--------{page}---/"
|
||||
try:
|
||||
resp = self.session.get(url, timeout=15)
|
||||
resp.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
for item in soup.select('.module-item'):
|
||||
a = item if item.name == 'a' else item.select_one('a')
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
if not href.startswith('/voddetail/'):
|
||||
continue
|
||||
title = a.get('title', '') or ''
|
||||
img = a.select_one('img')
|
||||
pic = (img.get('data-original') or img.get('src', '')) if img else ''
|
||||
note = a.select_one('.module-item-note')
|
||||
videos.append({
|
||||
"vod_id": href,
|
||||
"vod_name": title,
|
||||
"vod_pic": urljoin(self.home_url, pic),
|
||||
"vod_remarks": note.text.strip() if note else '',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
'list': videos,
|
||||
'page': page,
|
||||
'pagecount': 999 if len(videos) >= 20 else page,
|
||||
'limit': 40,
|
||||
'total': 999999,
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
try:
|
||||
vid = ids[0]
|
||||
url = urljoin(self.home_url, vid)
|
||||
resp = self.session.get(url, timeout=15)
|
||||
resp.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
|
||||
vod = {"vod_id": vid, "vod_name": "未知"}
|
||||
|
||||
h1 = soup.select_one('.module-info-heading h1') or soup.select_one('.page-title')
|
||||
if h1:
|
||||
vod['vod_name'] = h1.text.strip()
|
||||
|
||||
img = soup.select_one('.module-info-poster img') or soup.select_one('.module-item-pic img')
|
||||
if img:
|
||||
vod['vod_pic'] = urljoin(self.home_url, img.get('data-original') or img.get('src', ''))
|
||||
|
||||
intro = soup.select_one('.module-info-introduction-content p')
|
||||
if intro:
|
||||
vod['vod_content'] = intro.text.strip()
|
||||
|
||||
for item in soup.select('.module-info-item'):
|
||||
text = item.text
|
||||
if '导演:' in text:
|
||||
vod['vod_director'] = text.replace('导演:', '').strip()
|
||||
elif '主演:' in text:
|
||||
vod['vod_actor'] = text.replace('主演:', '').strip()
|
||||
elif '上映:' in text:
|
||||
vod['vod_year'] = text.replace('上映:', '').strip()
|
||||
elif '备注:' in text:
|
||||
vod['vod_remarks'] = text.replace('备注:', '').strip()
|
||||
|
||||
play_sources = []
|
||||
tab_box = soup.select_one('.module-tab-items-box')
|
||||
tabs = tab_box.select('.tab-item') if tab_box else soup.select('.module-tab-item.tab-item')
|
||||
for tab in tabs:
|
||||
span = tab.select_one('span')
|
||||
name = span.text.strip() if span else re.sub(r'\d+$', '', tab.text).strip()
|
||||
if name and name not in play_sources:
|
||||
play_sources.append(name)
|
||||
|
||||
play_lists = []
|
||||
for list_div in soup.select('.module-play-list-content'):
|
||||
eps = []
|
||||
for link in list_div.select('a.module-play-list-link'):
|
||||
span = link.select_one('span')
|
||||
ep_name = span.text.strip() if span else link.text.strip()
|
||||
ep_url = link.get('href', '')
|
||||
if ep_url:
|
||||
eps.append(f"{ep_name}${ep_url}")
|
||||
if eps:
|
||||
play_lists.append("#".join(eps))
|
||||
|
||||
if play_sources and play_lists:
|
||||
n = min(len(play_sources), len(play_lists))
|
||||
vod['vod_play_from'] = "$$$".join(play_sources[:n])
|
||||
vod['vod_play_url'] = "$$$".join(play_lists[:n])
|
||||
|
||||
return {'list': [vod]}
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
encoded = quote(key)
|
||||
if str(pg) == "1":
|
||||
url = f"{self.home_url}/vodsearch/{encoded}-------------/"
|
||||
else:
|
||||
url = f"{self.home_url}/vodsearch/{encoded}----------{pg}---/"
|
||||
resp = self.session.get(url, timeout=15)
|
||||
resp.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
videos = []
|
||||
items = soup.select('.module-card-item') or soup.select('.module-item')
|
||||
for item in items:
|
||||
try:
|
||||
a = (item.select_one('.module-card-item-poster') or
|
||||
item.select_one('.module-card-item-title a') or
|
||||
item.select_one('a'))
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
if not href.startswith('/voddetail/'):
|
||||
continue
|
||||
title_tag = (item.select_one('.module-card-item-title strong') or
|
||||
item.select_one('.module-card-item-title a'))
|
||||
title = title_tag.text.strip() if title_tag else ''
|
||||
img = item.select_one('img')
|
||||
if not title and img:
|
||||
title = img.get('alt', '')
|
||||
pic = (img.get('data-original') or img.get('src', '')) if img else ''
|
||||
note = item.select_one('.module-item-note')
|
||||
videos.append({
|
||||
"vod_id": href,
|
||||
"vod_name": title,
|
||||
"vod_pic": urljoin(self.home_url, pic),
|
||||
"vod_remarks": note.text.strip() if note else '',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': videos}
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': []}
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
return self.searchContent(key, quick, pg)
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
url = urljoin(self.home_url, id)
|
||||
resp = self.session.get(url, timeout=15)
|
||||
resp.encoding = 'utf-8'
|
||||
match = re.search(r'var\s+player_aaaa\s*=\s*({.+?})</script>', resp.text)
|
||||
if match:
|
||||
player_data = json.loads(match.group(1))
|
||||
real_url = player_data.get('url', '')
|
||||
if real_url.endswith('.m3u8') or real_url.endswith('.mp4'):
|
||||
return {'parse': 0, 'url': real_url, 'header': ''}
|
||||
return {'parse': 1, 'url': url}
|
||||
except Exception:
|
||||
pass
|
||||
return {'parse': 1, 'url': id}
|
||||
|
||||
def localProxy(self, params):
|
||||
return None
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
from base64 import b64decode, b64encode
|
||||
from Crypto.Hash import MD5
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='http://v.rbotv.cn'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'okhttp-okgo/jeasonlzy',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=requests.post(f'{self.host}/v3/type/top_type',headers=self.headers,files=self.getfiles({'': (None, '')})).json()
|
||||
result = {}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in data['data']['list']:
|
||||
classes.append({
|
||||
'type_name': k['type_name'],
|
||||
'type_id': k['type_id']
|
||||
})
|
||||
fts = []
|
||||
for i,x in k.items():
|
||||
if isinstance(x, list) and len(x)>2:
|
||||
fts.append({
|
||||
'name': i,
|
||||
'key': i,
|
||||
'value': [{'n': j, 'v': j} for j in x if j and j!= '全部']
|
||||
})
|
||||
if len(fts):filters[k['type_id']] = fts
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data=requests.post(f'{self.host}/v3/type/tj_vod',headers=self.headers,files=self.getfiles({'': (None, '')})).json()
|
||||
return {'list':self.getv(data['data']['cai']+data['data']['loop'])}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
files = {
|
||||
'type_id': (None, tid),
|
||||
'limit': (None, '12'),
|
||||
'page': (None, pg)
|
||||
}
|
||||
for k,v in extend.items():
|
||||
if k=='extend':k='class'
|
||||
files[k] = (None, v)
|
||||
data=requests.post(f'{self.host}/v3/home/type_search',headers=self.headers,files=self.getfiles(files)).json()
|
||||
result = {}
|
||||
result['list'] = self.getv(data['data']['list'])
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=requests.post(f'{self.host}/v3/home/vod_details',headers=self.headers,files=self.getfiles({'vod_id': (None, ids[0])})).json()
|
||||
v=data['data']
|
||||
vod = {
|
||||
'vod_name': v.get('vod_name'),
|
||||
'type_name': v.get('type_name'),
|
||||
'vod_year': v.get('vod_year'),
|
||||
'vod_area': v.get('vod_area'),
|
||||
'vod_remarks': v.get('vod_remarks'),
|
||||
'vod_actor': v.get('vod_actor'),
|
||||
'vod_director': v.get('vod_director'),
|
||||
'vod_content': pq(pq(v.get('vod_content','无') or '无').text()).text()
|
||||
}
|
||||
n,p=[],[]
|
||||
for o,i in enumerate(v['vod_play_list']):
|
||||
n.append(f"线路{o+1}({i.get('flag')})")
|
||||
c=[]
|
||||
for j in i.get('urls'):
|
||||
d={'url':j.get('url'),'p':i.get('parse_urls'),'r':i.get('referer'),'u':i.get('ua')}
|
||||
c.append(f"{j.get('name')}${self.e64(json.dumps(d))}")
|
||||
p.append('#'.join(c))
|
||||
vod.update({'vod_play_from':'$$$'.join(n),'vod_play_url':'$$$'.join(p)})
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
files = {
|
||||
'limit': (None, '12'),
|
||||
'page': (None, pg),
|
||||
'keyword': (None, key),
|
||||
}
|
||||
data=requests.post(f'{self.host}/v3/home/search',headers=self.headers,files=self.getfiles(files)).json()
|
||||
return {'list':self.getv(data['data']['list']),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids=json.loads(self.d64(id))
|
||||
url=ids['url']
|
||||
if isinstance(ids['p'],list) and len(ids['p']):
|
||||
url=[]
|
||||
for i,x in enumerate(ids['p']):
|
||||
up={'url':ids['url'],'p':x,'r':ids['r'],'u':ids['u']}
|
||||
url.extend([f"解析{i+1}",f"{self.getProxyUrl()}&data={self.e64(json.dumps(up))}"])
|
||||
h={}
|
||||
if ids.get('r'):
|
||||
h['Referer'] = ids['r']
|
||||
if ids.get('u'):
|
||||
h['User-Agent'] = ids['u']
|
||||
return {'parse': 0, 'url': url, 'header': h}
|
||||
|
||||
def localProxy(self, param):
|
||||
data=json.loads(self.d64(param['data']))
|
||||
h = {}
|
||||
if data.get('r'):
|
||||
h['Referer'] = data['r']
|
||||
if data.get('u'):
|
||||
h['User-Agent'] = data['u']
|
||||
res=self.fetch(f"{data['p']}{data['url']}",headers=h).json()
|
||||
url=res.get('url') or res['data'].get('url')
|
||||
return [302,'video/MP2T',None,{'Location':url}]
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def getfiles(self, p=None):
|
||||
if p is None:p = {}
|
||||
t=str(int(time.time()))
|
||||
h = MD5.new()
|
||||
h.update(f"7gp0bnd2sr85ydii2j32pcypscoc4w6c7g5spl{t}".encode('utf-8'))
|
||||
s = h.hexdigest()
|
||||
files = {
|
||||
'sign': (None, s),
|
||||
'timestamp': (None, t)
|
||||
}
|
||||
p.update(files)
|
||||
return p
|
||||
|
||||
def getv(self,data):
|
||||
videos = []
|
||||
for i in data:
|
||||
if i.get('vod_id') and str(i['vod_id']) != '0':
|
||||
videos.append({
|
||||
'vod_id': i['vod_id'],
|
||||
'vod_name': i.get('vod_name'),
|
||||
'vod_pic': i.get('vod_pic') or i.get('vod_pic_thumb'),
|
||||
'vod_year': i.get('tag'),
|
||||
'vod_remarks': i.get('vod_remarks')
|
||||
})
|
||||
return videos
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
return ""
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys,requests,base64,json,time,re
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as BaseSpider
|
||||
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site="https://pinglian.lol"
|
||||
self.api_list=self.site+"/api/get_videos.php"
|
||||
self.api_pan=self.site+"/api/search_pan_links.php"
|
||||
self.username=""
|
||||
self.password=""
|
||||
self.cookie=""
|
||||
self.check_api=""
|
||||
self.enable_check=False
|
||||
self.ua="Mozilla/5.0 (Linux; Android 16; Pixel 9 Pro Build/BP1A.250305.019) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7743.101 Mobile Safari/537.36"
|
||||
self.channels={"1":"电影","2":"电视剧","3":"综艺","4":"动漫"}
|
||||
self.session=requests.Session()
|
||||
self.session.verify=False
|
||||
self.session.headers.update({"User-Agent":self.ua,"X-Requested-With":"XMLHttpRequest","Accept":"*/*","Referer":self.site+"/all-videos.php","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"})
|
||||
def getName(self):return "盘链"
|
||||
def init(self,extend=""):
|
||||
if extend:
|
||||
try:
|
||||
cfg=json.loads(extend)
|
||||
if isinstance(cfg,dict):
|
||||
self.username=cfg.get("username",self.username)
|
||||
self.password=cfg.get("password",self.password)
|
||||
self.cookie=cfg.get("cookie",self.cookie)
|
||||
self.check_api=cfg.get("check_api",self.check_api)
|
||||
self.enable_check=bool(cfg.get("enable_check",self.enable_check)) and bool(self.check_api)
|
||||
except Exception:0
|
||||
self.session.cookies.set("announcement_dismissed","true",domain="pinglian.lol",path="/")
|
||||
if self.cookie:self.session.headers.update({"Cookie":self.cookie})
|
||||
elif self.username and self.password:self._login()
|
||||
return self
|
||||
def destroy(self):self.session.close()
|
||||
def _login(self):
|
||||
try:
|
||||
self.session.get(self.site+"/pages/login.php",timeout=12)
|
||||
return bool(self.session.post(self.site+"/api/login.php",data={"username":self.username,"password":self.password,"remember":"on"},timeout=12).json().get("success"))
|
||||
except Exception:return False
|
||||
def _b64e(self,obj):
|
||||
text=obj if isinstance(obj,str) else json.dumps(obj,ensure_ascii=False,separators=(",",":"))
|
||||
return base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
|
||||
def _b64d(self,s):
|
||||
try:
|
||||
text=base64.urlsafe_b64decode((s+"="*(-len(s)%4)).encode()).decode()
|
||||
try:return json.loads(text)
|
||||
except Exception:return text
|
||||
except Exception:return s
|
||||
def _safe(self,t):return str(t or "").replace("#","#").replace("$","¥")
|
||||
def _fetch_list(self,t=None,wd=None,page=1):
|
||||
p={"pg":page}
|
||||
if wd:p["wd"]=wd
|
||||
elif t:p["t"]=t
|
||||
else:return {"list":[],"page":page,"pagecount":0,"total":0}
|
||||
try:
|
||||
d=self.session.get(self.api_list,params=p,timeout=15).json()
|
||||
if d.get("code")==1:return {"list":d.get("list",[]),"page":d.get("page",page),"pagecount":d.get("pagecount",1),"total":d.get("total",0)}
|
||||
except Exception:0
|
||||
return {"list":[],"page":page,"pagecount":0,"total":0}
|
||||
def _check_links(self,links,disk_type,batch_size=30):
|
||||
if not links or not self.check_api:return links
|
||||
ok=[]
|
||||
for i in range(0,len(links),batch_size):
|
||||
b=links[i:i+batch_size]
|
||||
try:
|
||||
d=requests.post(self.check_api,json={"items":[{"disk_type":disk_type,"url":u} for u in b]},headers={"User-Agent":self.ua,"Accept":"application/json, text/plain, */*"},timeout=15,verify=False).json()
|
||||
ok.extend([x.get("url","") for x in d.get("results",[]) if x.get("state")=="ok" and x.get("url")])
|
||||
except Exception:ok.extend(b)
|
||||
return ok
|
||||
def _pan_url(self,x):return x.get("url","") or (self.site+"/api/go.php?t="+x.get("token","") if x.get("token") else "")
|
||||
def _process_disk(self,k,v):
|
||||
links=v.get("links",[]) if isinstance(v,dict) else []
|
||||
raw,seen=[],set()
|
||||
for x in links:
|
||||
u=self._pan_url(x)
|
||||
if u and u not in seen:raw.append(u);seen.add(u)
|
||||
if not raw:return None,None
|
||||
valid=raw if not self.enable_check or k in {"others","guangya"} else self._check_links(raw,k)
|
||||
if not valid:return None,None
|
||||
s=set(valid);eps=[]
|
||||
for x in links:
|
||||
u=self._pan_url(x)
|
||||
if u not in s:continue
|
||||
pwd=x.get("password","")
|
||||
if pwd and "pwd=" not in u and "password=" not in u:u+=("&" if "?" in u else "?")+"pwd="+str(pwd)
|
||||
eps.append(self._safe(x.get("title") or v.get("name") or k)+"$"+self._b64e(u))
|
||||
if not eps:return None,None
|
||||
eps.insert(0,"点击选择$noop")
|
||||
return v.get("name",k),"#".join(eps)
|
||||
def _fetch_pan_links(self,name,vid):
|
||||
try:
|
||||
d=self.session.get(self.api_pan,params={"keyword":name,"vod_id":vid,"_t":int(time.time()*1000)},timeout=20).json()
|
||||
if not d.get("success"):return "",""
|
||||
pan=d.get("data",{})
|
||||
order=["quark","uc","xunlei","aliyun","baidu","115","123","tianyi","others"]
|
||||
items=[]
|
||||
for k in order:
|
||||
if k in pan:items.append((k,pan.pop(k)))
|
||||
items.extend(pan.items())
|
||||
fs,us=[],[]
|
||||
for k,v in items:
|
||||
f,u=self._process_disk(k,v)
|
||||
if f and u:fs.append(f);us.append(u)
|
||||
return "$$$".join(fs),"$$$".join(us)
|
||||
except Exception:return "",""
|
||||
def _vod(self,x):return {"vod_id":self._b64e(x),"vod_name":x.get("vod_name",""),"vod_pic":x.get("vod_pic",""),"vod_remarks":x.get("vod_remarks","") or x.get("type_name","")}
|
||||
def homeContent(self,filter):return {"class":[{"type_name":v,"type_id":k} for k,v in self.channels.items()],"list":[],"filters":{}}
|
||||
def homeVideoContent(self):
|
||||
r=self._fetch_list(t="1",page=1)
|
||||
return {"list":[self._vod(x) for x in r.get("list",[])[:12]]}
|
||||
def categoryContent(self,tid,pg,filter,extend):
|
||||
if tid not in self.channels:return {"list":[],"page":1,"pagecount":0,"limit":30,"total":0}
|
||||
page=int(pg) if str(pg).isdigit() else 1
|
||||
r=self._fetch_list(t=tid,page=page)
|
||||
return {"list":[self._vod(x) for x in r.get("list",[])],"page":page,"pagecount":r.get("pagecount",0),"limit":30,"total":r.get("total",0)}
|
||||
def searchContent(self,key,quick,pg="1"):
|
||||
page=int(pg) if str(pg).isdigit() else 1
|
||||
if not key:return {"list":[],"page":page,"pagecount":0,"limit":30,"total":0}
|
||||
r=self._fetch_list(wd=key,page=page)
|
||||
return {"list":[self._vod(x) for x in r.get("list",[])],"page":page,"pagecount":r.get("pagecount",0),"limit":30,"total":r.get("total",0)}
|
||||
def detailContent(self,ids):
|
||||
x=self._b64d(ids[0])
|
||||
if not isinstance(x,dict):return {"list":[]}
|
||||
name=x.get("vod_name","")
|
||||
fs,us=[],[]
|
||||
pf=x.get("vod_play_from","");pu=x.get("vod_play_url","")
|
||||
if pf and pu:fs.append(pf);us.append(pu)
|
||||
pan_f,pan_u=self._fetch_pan_links(name,x.get("vod_id")) if name and x.get("vod_id") is not None else ("","")
|
||||
if pan_f and pan_u:fs.append(pan_f);us.append(pan_u)
|
||||
if not fs:fs,us=["提示"],["需要有效登录或暂无资源$noop"]
|
||||
return {"list":[{"vod_id":ids[0],"vod_name":name,"vod_pic":x.get("vod_pic",""),"vod_year":x.get("vod_year",""),"vod_area":x.get("vod_area",""),"vod_actor":x.get("vod_actor",""),"vod_director":x.get("vod_director",""),"vod_content":x.get("vod_content",""),"vod_remarks":(str(x.get("vod_remarks",""))+" "+str(x.get("vod_score",""))).strip(),"vod_play_from":"$$$".join(fs),"vod_play_url":"$$$".join(us)}]}
|
||||
def _real_pan_url(self,u):
|
||||
if not isinstance(u,str) or "api/go.php" not in u:return u
|
||||
try:
|
||||
r=self.session.get(u,timeout=12,allow_redirects=True)
|
||||
m=re.search("https?://(?:pan\\.quark\\.cn|drive\\.uc\\.cn|pan\\.baidu\\.com|www\\.aliyundrive\\.com|www\\.alipan\\.com|alipan\\.com|cloud\\.189\\.cn|www\\.123pan\\.com|123pan\\.com|pan\\.xunlei\\.com|115\\.com)[^\\s\"\'<>]+",r.text)
|
||||
return m.group(0).replace("&","&") if m else u
|
||||
except Exception:return u
|
||||
def playerContent(self,flag,id,vipFlags):
|
||||
if not id or id=="noop":return {"parse":0,"jx":0,"url":""}
|
||||
u=self._b64d(id)
|
||||
if isinstance(u,dict):u=u.get("url","")
|
||||
if not isinstance(u,str):return {"parse":0,"jx":0,"url":""}
|
||||
u=self._real_pan_url(u)
|
||||
pans=["pan.quark.cn","drive.uc.cn","pan.baidu.com","aliyundrive.com","alipan.com","cloud.189.cn","123pan.com","pan.xunlei.com","115.com"]
|
||||
if any(x in u for x in pans):return {"parse":0,"jx":0,"url":"push://"+u,"header":{"User-Agent":self.ua,"Referer":self.site+"/"}}
|
||||
if u.startswith("magnet:"):return {"parse":0,"jx":0,"url":u}
|
||||
if ".m3u8" in u or ".mp4" in u:return {"parse":0,"jx":0,"url":u,"header":{"User-Agent":self.ua}}
|
||||
if u.startswith("http"):return {"parse":0,"jx":0,"url":"push://"+u,"header":{"User-Agent":self.ua,"Referer":self.site+"/"}}
|
||||
return {"parse":0,"jx":0,"url":""}
|
||||
@@ -0,0 +1,848 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 短剧聚合 Spider - 支持七猫、星芽、西饭、围观、河马
|
||||
import re
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
import random
|
||||
import requests
|
||||
from urllib.parse import quote, unquote
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.keys = 'd3dGiJc651gSQ8w1'
|
||||
self.char_map = {
|
||||
'+': 'P', '/': 'X', '0': 'M', '1': 'U', '2': 'l', '3': 'E', '4': 'r', '5': 'Y', '6': 'W', '7': 'b', '8': 'd', '9': 'J',
|
||||
'A': '9', 'B': 's', 'C': 'a', 'D': 'I', 'E': '0', 'F': 'o', 'G': 'y', 'H': '_', 'I': 'H', 'J': 'G', 'K': 'i', 'L': 't',
|
||||
'M': 'g', 'N': 'N', 'O': 'A', 'P': '8', 'Q': 'F', 'R': 'k', 'S': '3', 'T': 'h', 'U': 'f', 'V': 'R', 'W': 'q', 'X': 'C',
|
||||
'Y': '4', 'Z': 'p', 'a': 'm', 'b': 'B', 'c': 'O', 'd': 'u', 'e': 'c', 'f': '6', 'g': 'K', 'h': 'x', 'i': '5', 'j': 'T',
|
||||
'k': '-', 'l': '2', 'm': 'z', 'n': 'S', 'o': 'Z', 'p': '1', 'q': 'V', 'r': 'v', 's': 'j', 't': 'Q', 'u': '7', 'v': 'D',
|
||||
'w': 'w', 'x': 'n', 'y': 'L', 'z': 'e'
|
||||
}
|
||||
self.headers_default = {
|
||||
'User-Agent': 'okhttp/3.12.11',
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
}
|
||||
self.platform = {
|
||||
'星芽': {
|
||||
'host': 'https://app.whjzjx.cn',
|
||||
'url1': '/cloud/v2/theater/home_page?theater_class_id',
|
||||
'url2': '/v2/theater_parent/detail',
|
||||
'search': '/v3/search',
|
||||
'classes': '/cloud/v2/theater/classes',
|
||||
'rankDetail': '/cloud/v1/first_level_ranking/detail',
|
||||
'loginUrl': 'https://u.shytkjgs.com/user/v1/account/login'
|
||||
},
|
||||
'西饭': {
|
||||
'host': 'https://xifan-api-cn.youlishipin.com',
|
||||
'url1': '/xifan/drama/portalPage',
|
||||
'url2': '/xifan/drama/getDuanjuInfo',
|
||||
'search': '/xifan/search/getSearchList'
|
||||
},
|
||||
'七猫': {
|
||||
'host': 'https://api-store.qmplaylet.com',
|
||||
'url1': '/api/v1/playlet/index',
|
||||
'url2': 'https://api-read.qmplaylet.com/player/api/v1/playlet/info',
|
||||
'search': '/api/v1/playlet/search'
|
||||
},
|
||||
'围观': {
|
||||
'host': 'https://api.drama.9ddm.com',
|
||||
'url1': '/drama/home/shortVideoTags',
|
||||
'url2': '/drama/home/shortVideoDetail',
|
||||
'search': '/drama/home/search'
|
||||
},
|
||||
'河马': {
|
||||
'host': 'https://www.kuaikaw.cn',
|
||||
'search': '/seo/video/6007'
|
||||
}
|
||||
}
|
||||
self.platform_list = [
|
||||
{'name': '七猫短剧', 'id': '七猫'},
|
||||
{'name': '星芽短剧', 'id': '星芽'},
|
||||
{'name': '西饭短剧', 'id': '西饭'},
|
||||
{'name': '围观短剧', 'id': '围观'},
|
||||
{'name': '河马短剧', 'id': '河马'}
|
||||
]
|
||||
self.rule_filter_def = {
|
||||
'星芽': {'area': '1', 'class2': '0', 'rank': '1'},
|
||||
'西饭': {'area': '都市'},
|
||||
'七猫': {'area': '0'},
|
||||
'围观': {'area': ''},
|
||||
'河马': {'area': '462'}
|
||||
}
|
||||
self.filter_options = {
|
||||
'七猫': [{
|
||||
'key': 'area',
|
||||
'name': '分类',
|
||||
'value': [
|
||||
{'n': '全部', 'v': '0'},
|
||||
{'n': '男频', 'v': '1'},
|
||||
{'n': '新剧', 'v': '3'},
|
||||
{'n': '现代言情', 'v': '21'},
|
||||
{'n': '神豪', 'v': '37'},
|
||||
{'n': '萌宝', 'v': '356'},
|
||||
{'n': '穿越', 'v': '373'},
|
||||
{'n': '战神', 'v': '527'},
|
||||
{'n': '神医', 'v': '1269'},
|
||||
{'n': '古装', 'v': '1272'}
|
||||
]
|
||||
}],
|
||||
'星芽': [{
|
||||
'key': 'area',
|
||||
'name': '剧场',
|
||||
'value': [
|
||||
{'n': '剧场', 'v': '1'},
|
||||
{'n': '热播短剧', 'v': '2'},
|
||||
{'n': '会员专享', 'v': '8'},
|
||||
{'n': '星选好剧', 'v': '7'},
|
||||
{'n': '新剧', 'v': '3'},
|
||||
{'n': '阳光剧场', 'v': '5'},
|
||||
{'n': '排行榜', 'v': '9'}
|
||||
]
|
||||
}, {
|
||||
'key': 'class2',
|
||||
'name': '类型',
|
||||
'value': [
|
||||
{'n': '全部', 'v': '0'},
|
||||
{'n': '都市', 'v': '4'},
|
||||
{'n': '逆袭', 'v': '7'},
|
||||
{'n': '古装', 'v': '5'},
|
||||
{'n': '亲情', 'v': '41'},
|
||||
{'n': '现代言情', 'v': '15'},
|
||||
{'n': '重生', 'v': '6'},
|
||||
{'n': '虐恋', 'v': '8'},
|
||||
{'n': '玄幻', 'v': '35'},
|
||||
{'n': '穿越', 'v': '17'},
|
||||
{'n': '脑洞', 'v': '32'},
|
||||
{'n': '甜宠', 'v': '33'},
|
||||
{'n': '古代言情', 'v': '37'},
|
||||
{'n': '战神', 'v': '24'},
|
||||
{'n': '历史', 'v': '40'},
|
||||
{'n': '赘婿', 'v': '26'},
|
||||
{'n': '萌宝', 'v': '9'},
|
||||
{'n': '神医', 'v': '25'}
|
||||
]
|
||||
}, {
|
||||
'key': 'rank',
|
||||
'name': '榜单',
|
||||
'value': [
|
||||
{'n': '实时热榜', 'v': '1'},
|
||||
{'n': '热搜榜', 'v': '2'},
|
||||
{'n': '新剧榜', 'v': '3'},
|
||||
{'n': '剧单榜', 'v': '4'},
|
||||
{'n': '口碑榜', 'v': '5'}
|
||||
]
|
||||
}],
|
||||
'西饭': [{
|
||||
'key': 'area',
|
||||
'name': '分类',
|
||||
'value': [
|
||||
{'n': '都市', 'v': '都市'},
|
||||
{'n': '甜宠', 'v': '甜宠'},
|
||||
{'n': '逆袭', 'v': '逆袭'},
|
||||
{'n': '战神', 'v': '战神'},
|
||||
{'n': '古装', 'v': '古装'},
|
||||
{'n': '穿越', 'v': '穿越'},
|
||||
{'n': '萌宝', 'v': '萌宝'}
|
||||
]
|
||||
}],
|
||||
'围观': [{
|
||||
'key': 'area',
|
||||
'name': '分类',
|
||||
'value': [
|
||||
{'n': '全部', 'v': ''},
|
||||
{'n': '都市', 'v': '都市'},
|
||||
{'n': '逆袭', 'v': '逆袭'},
|
||||
{'n': '家庭', 'v': '家庭'},
|
||||
{'n': '古装', 'v': '古装'},
|
||||
{'n': '复仇', 'v': '复仇'},
|
||||
{'n': '甜宠', 'v': '甜宠'},
|
||||
{'n': '悬疑', 'v': '悬疑'},
|
||||
{'n': '爱情', 'v': '爱情'},
|
||||
{'n': '重生', 'v': '重生'},
|
||||
{'n': '总裁', 'v': '总裁'},
|
||||
{'n': '穿越', 'v': '穿越'},
|
||||
{'n': '萌宝', 'v': '萌宝'},
|
||||
{'n': '战神', 'v': '战神'},
|
||||
{'n': '职场', 'v': '职场'},
|
||||
{'n': '神豪', 'v': '神豪'},
|
||||
{'n': '神医', 'v': '神医'},
|
||||
{'n': '赘婿', 'v': '赘婿'}
|
||||
]
|
||||
}],
|
||||
'河马': [{
|
||||
'key': 'area',
|
||||
'name': '分类',
|
||||
'value': [
|
||||
{'n': '甜宠', 'v': '462'},
|
||||
{'n': '古装仙侠', 'v': '1102'},
|
||||
{'n': '现代言情', 'v': '1145'},
|
||||
{'n': '青春', 'v': '1170'},
|
||||
{'n': '豪门恩怨', 'v': '585'},
|
||||
{'n': '逆袭', 'v': '417-464'},
|
||||
{'n': '重生', 'v': '439-465'},
|
||||
{'n': '系统', 'v': '1159'},
|
||||
{'n': '总裁', 'v': '1147'},
|
||||
{'n': '职场商战', 'v': '943'}
|
||||
]
|
||||
}]
|
||||
}
|
||||
# 缓存
|
||||
self.qm_header = {'value': None, 'timestamp': 0}
|
||||
self.xingya_token = None
|
||||
self.xingya_headers = self.headers_default.copy()
|
||||
|
||||
def init(self, extend=""):
|
||||
self.extend = extend
|
||||
return self
|
||||
|
||||
def getName(self):
|
||||
return "短剧聚合"
|
||||
|
||||
def _md5(self, text):
|
||||
return hashlib.md5(text.encode()).hexdigest().lower()
|
||||
|
||||
def _base64_encode(self, text):
|
||||
return base64.b64encode(text.encode()).decode()
|
||||
|
||||
def _base64_decode(self, text):
|
||||
try:
|
||||
return base64.b64decode(text).decode()
|
||||
except:
|
||||
return text
|
||||
|
||||
def _get_qm_params_and_sign(self):
|
||||
now = int(time.time() * 1000)
|
||||
if self.qm_header['value'] and now - self.qm_header['timestamp'] < 300000:
|
||||
return self.qm_header['value']
|
||||
|
||||
session_id = str(now)
|
||||
data = {
|
||||
"static_score": "0.8",
|
||||
"uuid": "00000000-7fc7-08dc-0000-000000000000",
|
||||
"device-id": "20250220125449b9b8cac84c2dd3d035c9052a2572f7dd0122edde3cc42a70",
|
||||
"sourceuid": "aa7de295aad621a6",
|
||||
"refresh-type": "0",
|
||||
"model": "22021211RC",
|
||||
"client-id": "aa7de295aad621a6",
|
||||
"brand": "Redmi",
|
||||
"sys-ver": "12",
|
||||
"phone-level": "H",
|
||||
"wlb-uid": "aa7de295aad621a6",
|
||||
"session-id": session_id
|
||||
}
|
||||
|
||||
json_str = json.dumps(data, separators=(',', ':'))
|
||||
base64_str = self._base64_encode(json_str)
|
||||
qm_params = ''
|
||||
|
||||
for char in base64_str:
|
||||
qm_params += self.char_map.get(char, char)
|
||||
|
||||
params_str = f"AUTHORIZATION=app-version=10001application-id=com.duoduo.readchannel=unknownis-white=net-env=5platform=androidqm-params={qm_params}reg={self.keys}"
|
||||
sign = self._md5(params_str)
|
||||
|
||||
self.qm_header['value'] = {'qmParams': qm_params, 'sign': sign}
|
||||
self.qm_header['timestamp'] = now
|
||||
return self.qm_header['value']
|
||||
|
||||
def _get_header_x(self):
|
||||
qm = self._get_qm_params_and_sign()
|
||||
return {
|
||||
'net-env': '5',
|
||||
'reg': '',
|
||||
'channel': 'unknown',
|
||||
'is-white': '',
|
||||
'platform': 'android',
|
||||
'application-id': 'com.duoduo.read',
|
||||
'authorization': '',
|
||||
'app-version': '10001',
|
||||
'user-agent': 'webviewversion/0',
|
||||
'qm-params': qm['qmParams'],
|
||||
'sign': qm['sign']
|
||||
}
|
||||
|
||||
def _ensure_xingya_auth(self):
|
||||
if self.xingya_headers.get('authorization'):
|
||||
return self.xingya_headers
|
||||
try:
|
||||
plat = self.platform['星芽']
|
||||
res = requests.post(
|
||||
plat['loginUrl'],
|
||||
headers={'User-Agent': 'okhttp/4.10.0', 'platform': '1', 'Content-Type': 'application/json'},
|
||||
json={'device': '24250683a3bdb3f118dff25ba4b1cba1a'},
|
||||
timeout=10,
|
||||
verify=False
|
||||
)
|
||||
data = res.json()
|
||||
token = data.get('data', {}).get('token') or data.get('token')
|
||||
if token:
|
||||
self.xingya_headers = {**self.headers_default, 'authorization': token}
|
||||
self.xingya_token = token
|
||||
except Exception as e:
|
||||
pass
|
||||
return self.xingya_headers
|
||||
|
||||
def _request(self, url, method='GET', headers=None, data=None, timeout=5000):
|
||||
try:
|
||||
headers = {**self.headers_default, **(headers or {})}
|
||||
if method.upper() == 'POST':
|
||||
res = requests.post(url, headers=headers, json=data, timeout=timeout/1000, verify=False)
|
||||
else:
|
||||
res = requests.get(url, headers=headers, timeout=timeout/1000, verify=False)
|
||||
return res.json()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
classes = [{'type_name': p['name'], 'type_id': p['id']} for p in self.platform_list]
|
||||
filters = {}
|
||||
for item in self.platform_list:
|
||||
platform_id = item['id']
|
||||
if platform_id in self.filter_options:
|
||||
filters[platform_id] = self.filter_options[platform_id]
|
||||
return {'class': classes, 'filters': filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.categoryContent('七猫', '1', False, {})
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if pg else 1
|
||||
plat = self.platform.get(tid)
|
||||
area = extend.get('area') if extend.get('area') is not None else self.rule_filter_def.get(tid, {}).get('area', '')
|
||||
videos = []
|
||||
|
||||
if not plat:
|
||||
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
|
||||
|
||||
try:
|
||||
if tid == '七猫':
|
||||
if pg > 1:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
|
||||
sign = self._md5(f"operation=1playlet_privacy=1tag_id={area}{self.keys}")
|
||||
url = f"{plat['host']}{plat['url1']}?tag_id={area}&playlet_privacy=1&operation=1&sign={sign}"
|
||||
header_x = self._get_header_x()
|
||||
res = self._request(url, headers={**header_x, **self.headers_default}, timeout=3000)
|
||||
if res and res.get('data', {}).get('list'):
|
||||
for i in res['data']['list'][:6]:
|
||||
videos.append({
|
||||
'vod_id': f"七猫@{quote(str(i['playlet_id']))}",
|
||||
'vod_name': i['title'],
|
||||
'vod_pic': i['image_link'],
|
||||
'vod_remarks': f"{i['total_episode_num']}集",
|
||||
'vod_content': f"七猫短剧 | {i['total_episode_num']}集"
|
||||
})
|
||||
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
|
||||
|
||||
elif tid == '星芽':
|
||||
headers = self._ensure_xingya_auth()
|
||||
if area == '9':
|
||||
rank = extend.get('rank') or extend.get('class2') or self.rule_filter_def['星芽'].get('rank', '1')
|
||||
if pg > 1:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
|
||||
res = self._request(f"{plat['host']}{plat['rankDetail']}?id={rank}", headers=headers, timeout=10000)
|
||||
for item in res.get('data', {}).get('list', []):
|
||||
i = item.get('theater') or item
|
||||
if not i or not i.get('id'):
|
||||
continue
|
||||
videos.append({
|
||||
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={i['id']}",
|
||||
'vod_name': i['title'],
|
||||
'vod_pic': i['cover_url'],
|
||||
'vod_remarks': f"{i.get('total', '')}集"
|
||||
})
|
||||
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
|
||||
|
||||
class2 = extend.get('class2') or self.rule_filter_def['星芽'].get('class2', '0')
|
||||
url = f"{plat['host']}{plat['url1']}={area}&type=1&class2_ids={class2}&page_num={pg}&page_size=24"
|
||||
res = self._request(url, headers=headers, timeout=10000)
|
||||
data = res.get('data', {})
|
||||
for i in data.get('list', []):
|
||||
item = i.get('theater') or i
|
||||
if not item or not item.get('id'):
|
||||
continue
|
||||
videos.append({
|
||||
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={item['id']}",
|
||||
'vod_name': item['title'],
|
||||
'vod_pic': item['cover_url'],
|
||||
'vod_remarks': f"{item.get('total', '')}集"
|
||||
})
|
||||
total = int(data.get('total') or len(videos))
|
||||
is_single_page = not videos or data.get('is_end') or total <= len(videos) or len(videos) > 24
|
||||
if is_single_page:
|
||||
return {'list': videos if pg == 1 else [], 'page': pg, 'pagecount': 1, 'limit': len(videos) if pg == 1 else 0, 'total': total}
|
||||
pagecount = max(1, (total + 23) // 24)
|
||||
return {'list': videos, 'page': pg, 'pagecount': pagecount, 'limit': 24, 'total': total}
|
||||
|
||||
elif tid == '西饭':
|
||||
if pg > 1:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
|
||||
search_url = f"{plat['host']}{plat['search']}?reqType=search&offset=0&keyword={quote(area or '')}&quickEngineVersion=-1&scene="
|
||||
search_res = self._request(search_url, timeout=10000)
|
||||
for block in search_res.get('result', {}).get('elements', []):
|
||||
for item in block.get('contents', []):
|
||||
dj = item.get('duanjuVo') or {}
|
||||
if not dj.get('duanjuId'):
|
||||
continue
|
||||
categories = dj.get('categories', [])
|
||||
if area and area not in categories:
|
||||
continue
|
||||
videos.append({
|
||||
'vod_id': f"西饭@{dj['duanjuId']}#{dj['source']}",
|
||||
'vod_name': dj['title'],
|
||||
'vod_pic': dj['coverImageUrl'],
|
||||
'vod_remarks': f"{dj.get('total', '')}集"
|
||||
})
|
||||
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
|
||||
|
||||
elif tid == '围观':
|
||||
device_name = 'Pixel 8 Pro'
|
||||
device_firm = 'Google'
|
||||
client_info = self._md5(str(int(time.time() * 1000))[-10:])
|
||||
url = f"{plat['host']}{plat['search']}?version_code=1500&version_name=1.5.0&device_name={quote(device_name)}&device_type=phone&is_first_day=true&is_first_24h=true&app_launch_way=icon&default_homepage=homepage_interaction&device_owning_firm={quote(device_firm)}&font_scale=default&os_type=1&clientInfo={client_info}"
|
||||
res = self._request(url, method='POST', headers={'User-Agent': 'okhttp/5.1.0', 'Content-Type': 'application/json; charset=utf-8'}, data={'audience': '全部', 'order': '最新', 'page': pg, 'pageSize': 30, 'searchWord': '', 'subject': area or ''}, timeout=10000)
|
||||
for i in res.get('data', []):
|
||||
videos.append({
|
||||
'vod_id': f"围观@{i['oneId']}",
|
||||
'vod_name': i['title'],
|
||||
'vod_pic': i.get('horzPoster') or i.get('vertPoster'),
|
||||
'vod_remarks': f"{i.get('episodeCount', '')}集"
|
||||
})
|
||||
return {'list': videos, 'page': pg, 'pagecount': pg if len(videos) < 30 else pg + 1, 'limit': 30, 'total': (pg - 1) * 30 + len(videos)}
|
||||
|
||||
elif tid == '河马':
|
||||
url = f"{plat['host']}/browse/{area or self.rule_filter_def['河马']['area']}/{pg}"
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
|
||||
'Referer': url,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
res = requests.get(url, headers=headers, timeout=10, verify=False)
|
||||
html = res.text
|
||||
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
|
||||
if match:
|
||||
json_data = json.loads(match.group(1))
|
||||
page_props = json_data.get('props', {}).get('pageProps', {})
|
||||
for book in page_props.get('bookList', []):
|
||||
if not book.get('bookId'):
|
||||
continue
|
||||
videos.append({
|
||||
'vod_id': f"河马@/drama/{book['bookId']}",
|
||||
'vod_name': book['bookName'],
|
||||
'vod_pic': book.get('coverWap'),
|
||||
'vod_remarks': f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
pages = int(page_props.get('pages') or pg)
|
||||
return {'list': videos, 'page': pg, 'pagecount': pages, 'limit': len(videos), 'total': pages * len(videos)}
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
|
||||
|
||||
def detailContent(self, ids):
|
||||
videos = []
|
||||
for id in ids if isinstance(ids, list) else [ids]:
|
||||
if not id:
|
||||
continue
|
||||
parts = id.split('@', 1)
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
plat_id, did = parts[0], parts[1]
|
||||
plat = self.platform.get(plat_id)
|
||||
if not plat:
|
||||
videos.append({'vod_id': id, 'vod_name': '平台不支持', 'vod_play_url': ''})
|
||||
continue
|
||||
|
||||
vod = {'vod_id': id, 'vod_name': '未知', 'vod_pic': '', 'vod_remarks': '', 'vod_content': '', 'vod_play_from': '', 'vod_play_url': ''}
|
||||
|
||||
try:
|
||||
if plat_id == '七猫':
|
||||
did_decoded = unquote(did)
|
||||
sign = self._md5(f"playlet_id={did_decoded}{self.keys}")
|
||||
url = f"{plat['url2']}?playlet_id={did_decoded}&sign={sign}"
|
||||
header_x = self._get_header_x()
|
||||
res = self._request(url, headers={**header_x, **self.headers_default})
|
||||
if res and res.get('data'):
|
||||
d = res['data']
|
||||
play_list = d.get('play_list', [])
|
||||
play_url = '#'.join([f"{i['sort']}${i['video_url']}" for i in play_list])
|
||||
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['image_link'], 'vod_remarks': f"{d['total_episode_num']}集", 'vod_content': d.get('intro', ''), 'vod_play_from': '七猫短剧', 'vod_play_url': play_url}
|
||||
|
||||
elif plat_id == '星芽':
|
||||
headers = self._ensure_xingya_auth()
|
||||
res = self._request(did, headers=headers, timeout=10000)
|
||||
if res and res.get('data'):
|
||||
d = res['data']
|
||||
theaters = d.get('theaters', [])
|
||||
play_url = '#'.join([f"{i['num']}${i['son_video_url']}" for i in theaters])
|
||||
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['cover_url'], 'vod_remarks': str(d.get('desc_tags', '')), 'vod_play_from': '星芽短剧', 'vod_play_url': play_url}
|
||||
|
||||
elif plat_id == '西饭':
|
||||
duanju_id, source = did.split('#', 1)
|
||||
url = f"{plat['host']}{plat['url2']}?duanjuId={duanju_id}&source={source}"
|
||||
res = self._request(url)
|
||||
if res and res.get('result'):
|
||||
d = res['result']
|
||||
episode_list = d.get('episodeList', [])
|
||||
play_url = '#'.join([f"{e['index']}${e['playUrl']}" for e in episode_list])
|
||||
status = '已完结' if d.get('updateStatus') == 'over' else f"更新{d.get('total', '')}集"
|
||||
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['coverImageUrl'], 'vod_remarks': f"{d.get('total', '')}集 {status}", 'vod_play_from': '西饭短剧', 'vod_play_url': play_url}
|
||||
|
||||
elif plat_id == '围观':
|
||||
device_name = 'Pixel 8 Pro'
|
||||
device_firm = 'Google'
|
||||
client_info = self._md5(str(int(time.time() * 1000))[-10:])
|
||||
url = f"{plat['host']}{plat['url2']}?version_code=1500&version_name=1.5.0&device_name={quote(device_name)}&device_type=phone&is_first_day=true&is_first_24h=true&app_launch_way=icon&default_homepage=homepage_interaction&device_owning_firm={quote(device_firm)}&font_scale=default&os_type=1&clientInfo={client_info}&oneId={did}&page=1&pageSize=1000&userId=0&queryAll=true"
|
||||
res = self._request(url, headers={'User-Agent': 'okhttp/5.1.0', 'Content-Type': 'application/json; charset=utf-8'}, timeout=10000)
|
||||
episodes = res.get('data', [])
|
||||
if episodes:
|
||||
play_url = '#'.join([f"{e.get('playOrder') or e.get('title')}${self._base64_encode(json.dumps(e.get('videoClarityList', [])))}" for e in episodes])
|
||||
vod = {**vod, 'vod_name': res.get('title') or episodes[0].get('title') or vod['vod_name'], 'vod_pic': res.get('vertPoster') or episodes[0].get('vertPoster') or '', 'vod_remarks': f"共{len(episodes)}集", 'vod_content': res.get('description', ''), 'vod_play_from': '围观短剧', 'vod_play_url': play_url}
|
||||
|
||||
elif plat_id == '河马':
|
||||
did_path = did if did.startswith('/drama/') else f"/drama/{did}"
|
||||
full_url = f"{plat['host']}{did_path}"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
|
||||
'Referer': full_url,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
res = requests.get(full_url, headers=headers, timeout=10, verify=False)
|
||||
html = res.text
|
||||
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
|
||||
if match:
|
||||
json_data = json.loads(match.group(1))
|
||||
page_props = json_data.get('props', {}).get('pageProps', {})
|
||||
book_info = page_props.get('bookInfoVo', {})
|
||||
chapter_list = page_props.get('chapterList', [])
|
||||
play_urls = []
|
||||
for chapter in chapter_list:
|
||||
chapter_id = chapter.get('chapterId')
|
||||
chapter_name = chapter.get('chapterName')
|
||||
video_vo = chapter.get('chapterVideoVo', {})
|
||||
direct_url = video_vo.get('mp4') or video_vo.get('mp4720p') or video_vo.get('vodMp4Url')
|
||||
if direct_url and re.search(r'\.(mp4|m3u8)', direct_url, re.I):
|
||||
play_urls.append(f"{chapter_name}${direct_url}")
|
||||
else:
|
||||
drama_id = did_path.replace('/drama/', '')
|
||||
play_urls.append(f"{chapter_name}${drama_id}+{chapter_id}")
|
||||
vod = {**vod, 'vod_name': book_info.get('title') or book_info.get('bookName') or vod['vod_name'], 'vod_pic': book_info.get('coverWap') or '', 'vod_remarks': f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}集".strip(), 'vod_content': book_info.get('introduction', ''), 'vod_play_from': '河马短剧', 'vod_play_url': '#'.join(play_urls)}
|
||||
|
||||
except Exception as e:
|
||||
vod['vod_name'] = '加载失败'
|
||||
|
||||
videos.append(vod)
|
||||
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if '七猫' in flag:
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
if '西饭' in flag:
|
||||
try:
|
||||
res = requests.get(id, headers={'User-Agent': 'Mozilla/5.0'}, timeout=10, verify=False, allow_redirects=True)
|
||||
final_url = res.url
|
||||
return {'parse': 0, 'url': final_url or id}
|
||||
except:
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
if '围观' in flag:
|
||||
try:
|
||||
ps = json.loads(self._base64_decode(id))
|
||||
urls = []
|
||||
for item in ps or []:
|
||||
if item.get('name') and item.get('url'):
|
||||
urls.extend([item['name'], item['url']])
|
||||
return {'parse': 0, 'url': urls if urls else id, 'headers': {'User-Agent': 'okhttp/5.1.0'}}
|
||||
except:
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
if '河马' in flag:
|
||||
if re.search(r'\.(mp4|m3u8)', id, re.I):
|
||||
return {'parse': 0, 'url': id}
|
||||
parts = id.split('+', 1)
|
||||
if len(parts) >= 2:
|
||||
drama_id, chapter_id = parts
|
||||
episode_url = f"{self.platform['河马']['host']}/episode/{drama_id}/{chapter_id}"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
|
||||
'Referer': episode_url,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
try:
|
||||
res = requests.get(episode_url, headers=headers, timeout=10, verify=False)
|
||||
html = res.text
|
||||
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
|
||||
if match:
|
||||
json_data = json.loads(match.group(1))
|
||||
video_info = json_data.get('props', {}).get('pageProps', {}).get('chapterInfo', {}).get('chapterVideoVo', {})
|
||||
video_url = video_info.get('mp4') or video_info.get('mp4720p') or video_info.get('vodMp4Url')
|
||||
if not video_url:
|
||||
m = re.search(r'(https?://[^"\']+\.mp4[^"\']*)', html)
|
||||
video_url = m.group(1) if m else ''
|
||||
return {'parse': 0, 'url': video_url}
|
||||
except:
|
||||
pass
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pg = int(pg) if pg else 1
|
||||
|
||||
if not key:
|
||||
return {
|
||||
'list': [],
|
||||
'page': pg,
|
||||
'pagecount': 1,
|
||||
'limit': 0,
|
||||
'total': 0
|
||||
}
|
||||
|
||||
videos = []
|
||||
seen = set()
|
||||
|
||||
def safe_push(item):
|
||||
if not item:
|
||||
return
|
||||
|
||||
vod_id = str(item.get('vod_id', '')).strip()
|
||||
vod_name = str(item.get('vod_name', '')).strip()
|
||||
|
||||
if not vod_id or not vod_name:
|
||||
return
|
||||
|
||||
if vod_id in seen:
|
||||
return
|
||||
|
||||
item['vod_id'] = vod_id
|
||||
item['vod_name'] = vod_name
|
||||
|
||||
seen.add(vod_id)
|
||||
videos.append(item)
|
||||
|
||||
# 七猫搜索
|
||||
try:
|
||||
sign = self._md5(
|
||||
f"operation=2playlet_privacy=1search_word={key}{self.keys}"
|
||||
)
|
||||
|
||||
url = (
|
||||
f"{self.platform['七猫']['host']}"
|
||||
f"{self.platform['七猫']['search']}"
|
||||
f"?search_word={quote(key)}"
|
||||
f"&playlet_privacy=1"
|
||||
f"&operation=2"
|
||||
f"&sign={sign}"
|
||||
)
|
||||
|
||||
header_x = self._get_header_x()
|
||||
|
||||
res = self._request(
|
||||
url,
|
||||
headers={**header_x, **self.headers_default},
|
||||
timeout=6000
|
||||
)
|
||||
if res:
|
||||
for i in res.get('data', {}).get('list', []):
|
||||
safe_push({
|
||||
'vod_id': f"七猫@{quote(str(i['playlet_id']))}",
|
||||
'vod_name': i.get('title', ''),
|
||||
'vod_pic': i.get('image_link', ''),
|
||||
'vod_remarks': f"七猫短剧|{i.get('total_episode_num', '')}集"
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 星芽搜索
|
||||
try:
|
||||
plat = self.platform['星芽']
|
||||
headers = self._ensure_xingya_auth()
|
||||
|
||||
res = self._request(
|
||||
plat['host'] + plat['search'],
|
||||
method='POST',
|
||||
headers=headers,
|
||||
data={'text': key},
|
||||
timeout=10000
|
||||
)
|
||||
if res:
|
||||
data = res.get('data', {})
|
||||
|
||||
search_list = (
|
||||
data.get('theater', {}).get('search_data', [])
|
||||
or data.get('search_data', [])
|
||||
or data.get('list', [])
|
||||
)
|
||||
|
||||
for i in search_list:
|
||||
if not i.get('id'):
|
||||
continue
|
||||
|
||||
safe_push({
|
||||
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={i['id']}",
|
||||
'vod_name': i.get('title', ''),
|
||||
'vod_pic': i.get('cover_url', ''),
|
||||
'vod_remarks': f"星芽短剧|{i.get('total', '')}集"
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 西饭搜索
|
||||
try:
|
||||
plat = self.platform['西饭']
|
||||
|
||||
url = (
|
||||
f"{plat['host']}{plat['search']}"
|
||||
f"?reqType=search"
|
||||
f"&offset={(pg - 1) * 30}"
|
||||
f"&keyword={quote(key)}"
|
||||
f"&quickEngineVersion=-1"
|
||||
f"&scene="
|
||||
)
|
||||
|
||||
res = self._request(url)
|
||||
if res:
|
||||
elements = res.get('result', {}).get('elements', [])
|
||||
|
||||
for block in elements:
|
||||
if block.get('duanjuVo'):
|
||||
contents = [block]
|
||||
else:
|
||||
contents = block.get('contents', [])
|
||||
|
||||
for item in contents:
|
||||
dj = item.get('duanjuVo') or {}
|
||||
if not dj.get('duanjuId'):
|
||||
continue
|
||||
|
||||
safe_push({
|
||||
'vod_id': f"西饭@{dj['duanjuId']}#{dj['source']}",
|
||||
'vod_name': dj.get('title', ''),
|
||||
'vod_pic': dj.get('coverImageUrl', ''),
|
||||
'vod_remarks': f"西饭短剧|{dj.get('total', '')}集"
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 围观搜索
|
||||
try:
|
||||
plat = self.platform['围观']
|
||||
|
||||
device_name = 'Pixel 8 Pro'
|
||||
device_firm = 'Google'
|
||||
client_info = self._md5(str(int(time.time() * 1000))[-10:])
|
||||
|
||||
url = (
|
||||
f"{plat['host']}{plat['search']}"
|
||||
f"?version_code=1500"
|
||||
f"&version_name=1.5.0"
|
||||
f"&device_name={quote(device_name)}"
|
||||
f"&device_type=phone"
|
||||
f"&is_first_day=true"
|
||||
f"&is_first_24h=true"
|
||||
f"&app_launch_way=icon"
|
||||
f"&default_homepage=homepage_interaction"
|
||||
f"&device_owning_firm={quote(device_firm)}"
|
||||
f"&font_scale=default"
|
||||
f"&os_type=1"
|
||||
f"&clientInfo={client_info}"
|
||||
)
|
||||
|
||||
res = self._request(
|
||||
url,
|
||||
method='POST',
|
||||
headers={
|
||||
'User-Agent': 'okhttp/5.1.0',
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data={
|
||||
'audience': '',
|
||||
'order': '',
|
||||
'page': pg,
|
||||
'pageSize': 30,
|
||||
'searchWord': key,
|
||||
'subject': ''
|
||||
},
|
||||
timeout=10000
|
||||
)
|
||||
if res:
|
||||
for i in res.get('data', []):
|
||||
safe_push({
|
||||
'vod_id': f"围观@{i['oneId']}",
|
||||
'vod_name': i.get('title', ''),
|
||||
'vod_pic': i.get('horzPoster') or i.get('vertPoster'),
|
||||
'vod_remarks': f"围观短剧|{i.get('episodeCount', '')}集"
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 河马搜索
|
||||
try:
|
||||
plat = self.platform['河马']
|
||||
|
||||
tmpid = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=16))
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Referer': f"{plat['host']}/search?searchValue={quote(key)}",
|
||||
'Origin': 'https://www.kuaikaw.cn',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'pname': 'www.kuaikaw.cn',
|
||||
'tmpid': tmpid
|
||||
}
|
||||
|
||||
res = self._request(
|
||||
f"{plat['host']}{plat['search']}",
|
||||
method='POST',
|
||||
headers=headers,
|
||||
data={
|
||||
'sourceType': 1,
|
||||
'keyword': key,
|
||||
'index': pg,
|
||||
'page': pg
|
||||
},
|
||||
timeout=10000
|
||||
)
|
||||
if res:
|
||||
for book in res.get('data', {}).get('bookList', []):
|
||||
if not book.get('bookId'):
|
||||
continue
|
||||
|
||||
safe_push({
|
||||
'vod_id': f"河马@/drama/{book['bookId']}",
|
||||
'vod_name': book.get('bookName', ''),
|
||||
'vod_pic': book.get('coverWap', ''),
|
||||
'vod_remarks': (
|
||||
f"{book.get('statusDesc', '')} "
|
||||
f"{book.get('totalChapterNum', '')}集"
|
||||
).strip()
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 1,
|
||||
'limit': len(videos),
|
||||
'total': len(videos)
|
||||
}
|
||||
Reference in New Issue
Block a user