Sync all projects

This commit is contained in:
github-actions[bot]
2026-06-29 03:41:14 +00:00
parent 529cd7c625
commit 8b025e0766
11717 changed files with 6370622 additions and 0 deletions
+248
View File
@@ -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()
+169
View File
@@ -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
+948
View File
@@ -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("&amp;", "&")
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
}
+532
View File
@@ -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
+360
View File
@@ -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
+343
View File
@@ -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
+318
View File
@@ -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)
+392
View File
@@ -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('&amp;', '&')
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
+267
View File
@@ -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
+184
View File
@@ -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 ""
+155
View File
@@ -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("&amp;","&") 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":""}
+848
View File
@@ -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)
}