Sync all projects
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import requests
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from urllib.parse import quote, urlparse
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
self.did=self.getdid()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getdata("/api.php/getappapi.index/initV119")
|
||||
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序",
|
||||
"sort": "排序"}
|
||||
filters = {}
|
||||
classes = []
|
||||
json_data = data["type_list"]
|
||||
homedata = data["banner_list"][8:]
|
||||
for item in json_data:
|
||||
if item["type_name"] == "全部":
|
||||
continue
|
||||
has_non_empty_field = False
|
||||
jsontype_extend = json.loads(item["type_extend"])
|
||||
homedata.extend(item["recommend_list"])
|
||||
jsontype_extend["sort"] = "最新,最热,最赞"
|
||||
classes.append({"type_name": item["type_name"], "type_id": item["type_id"]})
|
||||
for key in dy:
|
||||
if key in jsontype_extend and jsontype_extend[key].strip() != "":
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["type_id"])] = []
|
||||
for dkey in jsontype_extend:
|
||||
if dkey in dy and jsontype_extend[dkey].strip() != "":
|
||||
values = jsontype_extend[dkey].split(",")
|
||||
value_array = [{"n": value.strip(), "v": value.strip()} for value in values if
|
||||
value.strip() != ""]
|
||||
filters[str(item["type_id"])].append({"key": dkey, "name": dy[dkey], "value": value_array})
|
||||
result = {}
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
result["list"] = homedata[1:]
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"area": extend.get('area', '全部'), "year": extend.get('year', '全部'), "type_id": tid, "page": pg,
|
||||
"sort": extend.get('sort', '最新'), "lang": extend.get('lang', '全部'),
|
||||
"class": extend.get('class', '全部')}
|
||||
result = {}
|
||||
data = self.getdata("/api.php/getappapi.index/typeFilterVodList", body)
|
||||
result["list"] = data["recommend_list"]
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = f"vod_id={ids[0]}"
|
||||
data = self.getdata("/api.php/getappapi.index/vodDetail", body)
|
||||
vod = data["vod"]
|
||||
play = []
|
||||
names = []
|
||||
for itt in data["vod_play_list"]:
|
||||
a = []
|
||||
names.append(itt["player_info"]["show"])
|
||||
for it in itt['urls']:
|
||||
it['user_agent'] = itt["player_info"].get("user_agent")
|
||||
it["parse"] = itt["player_info"].get("parse")
|
||||
a.append(f"{it['name']}${self.e64(json.dumps(it))}")
|
||||
play.append("#".join(a))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
body = f"keywords={key}&type_id=0&page={pg}"
|
||||
data = self.getdata("/api.php/getappapi.index/searchList", body)
|
||||
result = {"list": data["search_list"], "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = json.loads(self.d64(id))
|
||||
h = {"User-Agent": (ids['user_agent'] or "okhttp/3.14.9")}
|
||||
try:
|
||||
if re.search(r'url=', ids['parse_api_url']):
|
||||
data = self.fetch(ids['parse_api_url'], headers=h, timeout=10).json()
|
||||
url = data.get('url') or data['data'].get('url')
|
||||
else:
|
||||
body = f"parse_api={ids.get('parse') or ids['parse_api_url'].replace(ids['url'], '')}&url={quote(self.aes(ids['url'], True))}&token={ids.get('token')}"
|
||||
b = self.getdata("/api.php/getappapi.index/vodParse", body)['json']
|
||||
url = json.loads(b)['url']
|
||||
if 'error' in url: raise ValueError(f"解析失败: {url}")
|
||||
p = 0
|
||||
except Exception as e:
|
||||
print('错误信息:', e)
|
||||
url, p = ids['url'], 1
|
||||
|
||||
if re.search(r'\.jpg|\.png|\.jpeg', url):
|
||||
url = self.Mproxy(url)
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
return self.Mlocal(param)
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/3.14.9'
|
||||
}
|
||||
response = self.fetch('https://miget-1313189639.cos.ap-guangzhou.myqcloud.com/mifun.txt',headers=headers).text
|
||||
return self.host_late(response.split('\n'))
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
url = url.strip()
|
||||
start_time = time.time()
|
||||
response = requests.head(url, timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def getdid(self):
|
||||
did=self.getCache('did')
|
||||
if not did:
|
||||
t = str(int(time.time()))
|
||||
did = self.md5(t)
|
||||
self.setCache('did', did)
|
||||
return did
|
||||
|
||||
def aes(self, text, b=None):
|
||||
key = b"GETMIFUNGEIMIFUN"
|
||||
cipher = AES.new(key, AES.MODE_CBC, key)
|
||||
if b:
|
||||
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return pt.decode("utf-8")
|
||||
|
||||
def header(self):
|
||||
t = str(int(time.time()))
|
||||
header = {"Referer": self.host,
|
||||
"User-Agent": "okhttp/3.14.9", "app-version-code": "516", "app-ui-mode": "light",
|
||||
"app-api-verify-time": t, "app-user-device-id": self.did,
|
||||
"app-api-verify-sign": self.aes(t, True),
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
return header
|
||||
|
||||
def getdata(self, path, data=None):
|
||||
vdata = self.post(f"{self.host}{path}", headers=self.header(), data=data, timeout=10).json()['data']
|
||||
data1 = self.aes(vdata)
|
||||
return json.loads(data1)
|
||||
|
||||
def Mproxy(self, url):
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=m3u8"
|
||||
|
||||
def Mlocal(self, param, header=None):
|
||||
url = self.d64(param["url"])
|
||||
ydata = self.fetch(url, headers=header, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = self.fetch(url, headers=header).content.decode('utf-8')
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
last_slash_index = string.rfind('/')
|
||||
lpath = string[:last_slash_index + 1]
|
||||
lines[index] = durl + ('' if lpath.startswith('/') else '/') + lpath
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
@@ -1,300 +0,0 @@
|
||||
# coding=utf-8
|
||||
# hanime1.py —— TVBox / FongMi Python 爬虫
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
host = 'https://hanime1.me'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://hanime1.me/',
|
||||
'Accept-Language': 'zh-TW,zh;q=0.9,zh-CN;q=0.8',
|
||||
}
|
||||
|
||||
cates = [
|
||||
('最新里番', 'genre_sort:裏番|最新上市'),
|
||||
('最新上市', 'sort:最新上市'),
|
||||
('最新上传', 'sort:最新上傳'),
|
||||
('里番', '裏番'),
|
||||
('泡面番', '泡麵番'),
|
||||
('Motion Anime', 'Motion Anime'),
|
||||
('3DCG', '3DCG'),
|
||||
('2.5D', '2.5D'),
|
||||
('2D动画', '2D動畫'),
|
||||
('AI生成', 'AI生成'),
|
||||
('MMD', 'MMD'),
|
||||
('Cosplay', 'Cosplay'),
|
||||
('新番预告', '新番預告'),
|
||||
]
|
||||
|
||||
sorts = [
|
||||
{'n': '最新上市', 'v': '最新上市'},
|
||||
{'n': '最新上传', 'v': '最新上傳'},
|
||||
{'n': '本日排行', 'v': '本日排行'},
|
||||
{'n': '本周排行', 'v': '本週排行'},
|
||||
{'n': '本月排行', 'v': '本月排行'},
|
||||
]
|
||||
|
||||
# ---------- 基础 ----------
|
||||
def getName(self):
|
||||
return 'hanime1'
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
return self.fetch(url, headers=self.headers, timeout=15).text
|
||||
except Exception:
|
||||
import requests
|
||||
return requests.get(url, headers=self.headers, timeout=15).text
|
||||
|
||||
def _meta(self, html, prop):
|
||||
m = re.search(r'<meta[^>]+property="%s"[^>]+content="([^"]*)"' % prop, html)
|
||||
if not m:
|
||||
m = re.search(r'<meta[^>]+content="([^"]*)"[^>]+property="%s"' % prop, html)
|
||||
return m.group(1).strip() if m else ''
|
||||
|
||||
# ---------- 列表解析 ----------
|
||||
def parse_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
|
||||
card_pattern = re.compile(
|
||||
r'<a[^>]+href=["\'](?:https?://hanime1\.me)?/watch\?v=(\d+)["\'][^>]*>(.*?)</a>',
|
||||
re.S
|
||||
)
|
||||
|
||||
for m in card_pattern.finditer(html):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
inner = m.group(2)
|
||||
|
||||
title = ''
|
||||
t = re.search(
|
||||
r'class="[^"]*(?:title|name)[^"]*"[^>]*>\s*([^<]+?)\s*<',
|
||||
inner, re.S
|
||||
)
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
t = re.search(r'<img[^>]+alt="([^"]+)"', inner)
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
t = re.search(r'title="([^"]+)"', m.group(0))
|
||||
if t:
|
||||
title = t.group(1).strip()
|
||||
if not title:
|
||||
continue
|
||||
|
||||
pic = ''
|
||||
for img in re.findall(r'<img[^>]+(?:data-src|src)="(http[^"]+)"', inner):
|
||||
if '.gif' in img or 'icon' in img or 'avatar' in img:
|
||||
continue
|
||||
pic = img
|
||||
break
|
||||
|
||||
seen.add(vid)
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': ''
|
||||
})
|
||||
|
||||
if not videos:
|
||||
videos = self._parse_list_fallback(html)
|
||||
return videos
|
||||
|
||||
def _parse_list_fallback(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(
|
||||
r'<a[^>]+href=["\'][^"\']*watch\?v=(\d+)["\']([^>]*)>(.*?)</a>',
|
||||
html, re.S
|
||||
):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
attrs = m.group(2)
|
||||
inner = m.group(3)
|
||||
t = re.search(r'title="([^"]+)"', attrs)
|
||||
title = t.group(1).strip() if t else ''
|
||||
if not title:
|
||||
title = re.sub(r'<[^>]+>', '', inner).strip()
|
||||
if not title:
|
||||
continue
|
||||
seen.add(vid)
|
||||
videos.append({'vod_id': vid, 'vod_name': title, 'vod_pic': '', 'vod_remarks': ''})
|
||||
return videos
|
||||
|
||||
# ---------- 首页 ----------
|
||||
def homeContent(self, filter):
|
||||
classes = [{'type_name': n, 'type_id': v} for n, v in self.cates]
|
||||
filters = {}
|
||||
for _, v in self.cates:
|
||||
if not v.startswith('sort:') and not v.startswith('genre_sort:'):
|
||||
filters[v] = [{'key': 'sort', 'name': '排序', 'value': self.sorts}]
|
||||
return {'class': classes, 'filters': filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
html = self._get(self.host)
|
||||
return {'list': self.parse_list(html)[:30]}
|
||||
|
||||
# ---------- 分类 ----------
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid.startswith('genre_sort:'):
|
||||
parts = tid[len('genre_sort:'):].split('|', 1)
|
||||
genre = parts[0]
|
||||
sort = parts[1] if len(parts) > 1 else ''
|
||||
url = '{}/search?genre={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(genre), pg)
|
||||
if sort:
|
||||
url += '&sort=' + urllib.parse.quote(sort)
|
||||
elif tid.startswith('sort:'):
|
||||
sort_val = tid[len('sort:'):]
|
||||
url = '{}/search?sort={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(sort_val), pg)
|
||||
else:
|
||||
url = '{}/search?genre={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(tid), pg)
|
||||
if extend and extend.get('sort'):
|
||||
url += '&sort=' + urllib.parse.quote(extend['sort'])
|
||||
|
||||
html = self._get(url)
|
||||
videos = self.parse_list(html)
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': int(pg) + (1 if len(videos) >= 20 else 0),
|
||||
'limit': 30,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
# ---------- 详情(核心修复:把当前请求的这一集放到播放列表首位) ----------
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]
|
||||
html = self._get('{}/watch?v={}'.format(self.host, vid))
|
||||
|
||||
title = self._meta(html, 'og:title')
|
||||
pic = self._meta(html, 'og:image')
|
||||
desc = self._meta(html, 'og:description')
|
||||
|
||||
episodes = self._extract_playlist(html, vid, title)
|
||||
|
||||
play_url = '#'.join(
|
||||
'{}${}'.format(ep['name'], ep['vid'])
|
||||
for ep in episodes
|
||||
)
|
||||
|
||||
vod = {
|
||||
'vod_id' : vid,
|
||||
'vod_name' : title,
|
||||
'vod_pic' : pic,
|
||||
'vod_content' : desc,
|
||||
'vod_play_from': 'Hanime1',
|
||||
'vod_play_url' : play_url,
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def _extract_playlist(self, html, current_vid, current_title):
|
||||
"""
|
||||
提取播放列表(选集),并把当前请求的这一集放到列表最前面,
|
||||
这样无论用户从外层列表点的是第几集,进详情页后默认播放的
|
||||
都是用户实际点击的那一集,而不是固定播放第一集。
|
||||
"""
|
||||
episodes = []
|
||||
seen = set()
|
||||
|
||||
ep_pattern = re.compile(
|
||||
r'<a class="overlay" href="https?://hanime1\.me/watch\?v=(\d+)"></a>\s*'
|
||||
r'<div class="card-mobile-panel inner">.*?'
|
||||
r'<div class="card-mobile-title"[^>]*>([^<]+)</div>',
|
||||
re.S
|
||||
)
|
||||
|
||||
for m in ep_pattern.finditer(html):
|
||||
ep_vid = m.group(1)
|
||||
ep_name = m.group(2).strip()
|
||||
if ep_vid in seen:
|
||||
continue
|
||||
seen.add(ep_vid)
|
||||
episodes.append({'name': ep_name, 'vid': ep_vid})
|
||||
|
||||
# 没有播放列表说明是单集视频,直接返回当前这一集
|
||||
if not episodes:
|
||||
return [{'name': current_title or current_vid, 'vid': current_vid}]
|
||||
|
||||
# 确保当前请求的这一集一定在列表里(理论上必然存在,兜底一下)
|
||||
if not any(e['vid'] == current_vid for e in episodes):
|
||||
episodes.append({'name': current_title or current_vid, 'vid': current_vid})
|
||||
|
||||
# 先按集数数字升序排好,方便用户在选集列表里查看顺序
|
||||
def _ep_num(ep):
|
||||
m = re.search(r'(\d+)\s*$', ep['name'])
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
try:
|
||||
episodes.sort(key=_ep_num)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 核心修复:把当前请求的这一集挪到最前面,作为默认播放项
|
||||
current_index = next(
|
||||
(i for i, e in enumerate(episodes) if e['vid'] == current_vid), None
|
||||
)
|
||||
if current_index is not None and current_index != 0:
|
||||
current_ep = episodes.pop(current_index)
|
||||
episodes.insert(0, current_ep)
|
||||
|
||||
return episodes
|
||||
|
||||
# ---------- 搜索 ----------
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = '{}/search?query={}&page={}'.format(
|
||||
self.host, urllib.parse.quote(key), pg)
|
||||
html = self._get(url)
|
||||
return {'list': self.parse_list(html), 'page': int(pg)}
|
||||
|
||||
# ---------- 播放 ----------
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
html = self._get('{}/watch?v={}'.format(self.host, id))
|
||||
play = ''
|
||||
sources = re.findall(r'<source[^>]+src="([^"]+)"[^>]*size="(\d+)"', html)
|
||||
if sources:
|
||||
sources.sort(key=lambda x: int(x[1]), reverse=True)
|
||||
play = sources[0][0]
|
||||
if not play:
|
||||
m = re.search(r'"contentUrl"\s*:\s*"([^"]+)"', html)
|
||||
if m:
|
||||
play = m.group(1).replace('\\/', '/')
|
||||
if not play:
|
||||
m = re.search(r'(https?://[^\s\'"]+\.(?:m3u8|mp4)[^\s\'"]*)', html)
|
||||
if m:
|
||||
play = m.group(1)
|
||||
return {
|
||||
'parse': 0 if play else 1,
|
||||
'url' : play or '{}/watch?v={}'.format(self.host, id),
|
||||
'header': self.headers
|
||||
}
|
||||
+169
@@ -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
@@ -0,0 +1,948 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import ssl
|
||||
import json
|
||||
import html
|
||||
import base64
|
||||
import urllib3
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
from urllib.parse import quote, unquote, urljoin, urlparse
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
sys.path.append("..")
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class SSLAdapter(HTTPAdapter):
|
||||
def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
kwargs["ssl_context"] = ctx
|
||||
return super().init_poolmanager(connections, maxsize, block=block, **kwargs)
|
||||
|
||||
def proxy_manager_for(self, proxy, **kwargs):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
kwargs["ssl_context"] = ctx
|
||||
return super().proxy_manager_for(proxy, **kwargs)
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
hosts = [
|
||||
"https://www.qwmkv.com",
|
||||
"https://www.qwnull.com",
|
||||
"https://www.qwfilm.com",
|
||||
"https://www.qnmp4.com",
|
||||
"https://www.qn63.com"
|
||||
]
|
||||
host = hosts[0]
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 12; M2012K11AC) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1"
|
||||
}
|
||||
|
||||
CATEGORY_IDS = {"电影": 1, "剧集": 2, "综艺": 3, "动漫": 4, "短剧": 30}
|
||||
KEYWORDS = ["杜比", "dolby", "原盘", "高码", "remux", "蓝光", "hdr10+", "hdr10", "hdr", "4k", "2160p", "uhd"]
|
||||
|
||||
QUARK_CHECK_LIMIT = 100
|
||||
CHECK_TIME_BUDGET = 12.0 # 检测最多12秒
|
||||
|
||||
def getName(self):
|
||||
return "七味-最终稳定版(含大屏分组排序+防串位修复)"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
adapter = SSLAdapter(max_retries=2)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
self.session.verify = False
|
||||
self.session.headers.update(dict(self.headers))
|
||||
|
||||
self.last_vod_pic = ""
|
||||
self.vod_pic_cache = {}
|
||||
|
||||
self.pan_115_cookie = ""
|
||||
self.ack_mp4 = "https://vd2.bdstatic.com/mda-nj5kxa8kr7wgq6ie/sc/cae_h264_nowatermark/1653272065989267185/mda-nj5kxa8kr7wgq6ie.mp4"
|
||||
|
||||
if extend:
|
||||
try:
|
||||
ext = json.loads(extend)
|
||||
self.pan_115_cookie = ext.get("pan_115_cookie", "")
|
||||
self.ack_mp4 = ext.get("ack_mp4", self.ack_mp4)
|
||||
except Exception as e:
|
||||
print(f"init extend error: {e}")
|
||||
|
||||
self._probe_host()
|
||||
|
||||
def destroy(self):
|
||||
try:
|
||||
self.session.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# ---------------- utils ----------------
|
||||
def _probe_host(self):
|
||||
for h in self.hosts:
|
||||
try:
|
||||
r = self.session.get(h + "/", timeout=6, headers=self.headers, verify=False)
|
||||
if r.status_code == 200:
|
||||
self.host = h
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
def _full_url(self, path):
|
||||
if not path:
|
||||
return ""
|
||||
path = html.unescape(str(path)).strip()
|
||||
if path.startswith("//"):
|
||||
return "https:" + path
|
||||
if path.startswith(("http://", "https://", "magnet:?")):
|
||||
return path
|
||||
return urljoin(self.host + "/", path)
|
||||
|
||||
def _full_url_by_host(self, host, path):
|
||||
if not path:
|
||||
return ""
|
||||
path = html.unescape(str(path)).strip()
|
||||
if path.startswith("//"):
|
||||
return "https:" + path
|
||||
if path.startswith(("http://", "https://", "magnet:?")):
|
||||
return path
|
||||
return urljoin(host.rstrip("/") + "/", path.lstrip("/"))
|
||||
|
||||
def _fetch(self, url, timeout=10):
|
||||
tries = [self._full_url(url)]
|
||||
if isinstance(url, str) and not url.startswith(("http://", "https://", "magnet:?")):
|
||||
for h in self.hosts:
|
||||
u = urljoin(h + "/", url)
|
||||
if u not in tries:
|
||||
tries.append(u)
|
||||
|
||||
for u in tries:
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/"
|
||||
r = self.session.get(u, timeout=timeout, headers=h, verify=False)
|
||||
r.encoding = r.apparent_encoding or "utf-8"
|
||||
if r.status_code == 200 and len(r.text or "") > 30:
|
||||
for hh in self.hosts:
|
||||
if u.startswith(hh):
|
||||
self.host = hh
|
||||
break
|
||||
return r
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def _pq(self, url, timeout=10):
|
||||
r = self._fetch(url, timeout=timeout)
|
||||
return pq(r.text if r else "")
|
||||
|
||||
def _clean_text(self, s):
|
||||
return re.sub(r"\s+", " ", html.unescape(s or "")).strip()
|
||||
|
||||
def _clean_name(self, s, max_len=120):
|
||||
s = html.unescape(s or "").replace("#", "#").replace("$", "$")
|
||||
s = re.sub(r"\s+", " ", s).strip()
|
||||
return s[:max_len]
|
||||
|
||||
def _img_src(self, img):
|
||||
return img.attr("data-src") or img.attr("data-original") or img.attr("src") or ""
|
||||
|
||||
def _is_pan(self, u):
|
||||
u = (u or "").lower()
|
||||
return any(k in u for k in [
|
||||
"pan.quark.cn/s/", "pan.baidu.com/s/", "drive.uc.cn/s/",
|
||||
"pan.xunlei.com/s/", "aliyundrive.com/s/", "alipan.com/s/",
|
||||
"cloud.189.cn/", "caiyun.139.com/", "123pan.com/s/",
|
||||
"115.com/s/", "lanzou", "lanzoui", "lanzoux", "lanzoub"
|
||||
])
|
||||
|
||||
def _b64e(self, obj):
|
||||
txt = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
return base64.urlsafe_b64encode(txt.encode()).decode().rstrip("=")
|
||||
|
||||
def _b64d(self, s):
|
||||
try:
|
||||
s += "=" * (-len(s) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(s.encode()).decode())
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _get_mid(self, tid):
|
||||
if str(tid).isdigit():
|
||||
return int(tid)
|
||||
m = re.search(r"/vt/(\d+)", str(tid))
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
return 1
|
||||
|
||||
def _score_name(self, name):
|
||||
n = (name or "").lower()
|
||||
score = 0
|
||||
for i, kw in enumerate(self.KEYWORDS):
|
||||
if kw.lower() in n:
|
||||
score += (len(self.KEYWORDS) - i)
|
||||
return score
|
||||
|
||||
def _extract_video_list(self, doc):
|
||||
videos, seen = [], set()
|
||||
selectors = ["ul.pic-list li", "ul.content-list li", ".pic-list li", ".content-list li"]
|
||||
nodes = []
|
||||
for sel in selectors:
|
||||
n = list(doc(sel).items())
|
||||
if n:
|
||||
nodes = n
|
||||
break
|
||||
|
||||
for li in nodes:
|
||||
a = li("a[href]").eq(0)
|
||||
href = a.attr("href")
|
||||
if not href:
|
||||
continue
|
||||
vid = self._full_url(href)
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
|
||||
img = li("img").eq(0)
|
||||
pic = self._full_url(self._img_src(img))
|
||||
title = a.attr("title") or img.attr("alt") or li("h3 b").text() or li("h3").text() or ""
|
||||
remark = self._clean_text(li("span.s1").text() or li("span.s2").text() or li("p").text() or li(".tag").text())
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid,
|
||||
"vod_name": self._clean_name(title, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
})
|
||||
return videos
|
||||
|
||||
def _is_bad_cover(self, u):
|
||||
if not u:
|
||||
return True
|
||||
s = u.lower()
|
||||
return ("logo.png" in s) or ("loading" in s) or ("/template/piankuwap/image/logo" in s)
|
||||
|
||||
def _normalize_magnet(self, href):
|
||||
try:
|
||||
if not href:
|
||||
return ""
|
||||
href = str(href).strip().replace("&", "&")
|
||||
if href.startswith("push://"):
|
||||
href = href.replace("push://", "", 1).replace("#0agent", "")
|
||||
if "%3A" in href or "%3F" in href or "%26" in href:
|
||||
href = unquote(href)
|
||||
href = re.sub(r"\s+", "", href)
|
||||
if not href.startswith("magnet:") or "urn:btih:" not in href:
|
||||
return ""
|
||||
return href
|
||||
except:
|
||||
return ""
|
||||
|
||||
def _magnet_btih(self, magnet):
|
||||
m = self._normalize_magnet(magnet)
|
||||
if not m:
|
||||
return ""
|
||||
g = re.search(r"xt=urn:btih:([a-zA-Z0-9]+)", m, re.I)
|
||||
return g.group(1).lower() if g else ""
|
||||
|
||||
def _is_verify_page(self, text):
|
||||
t = (text or "").lower()
|
||||
return (
|
||||
("系统安全验证" in t) or
|
||||
("verify_check" in t) or
|
||||
("mac_verify_img" in t) or
|
||||
("请输入验证码" in t)
|
||||
)
|
||||
|
||||
def _mk_vod_id(self, h, raw_id, raw_url=""):
|
||||
if raw_url:
|
||||
u = self._full_url_by_host(h, raw_url)
|
||||
if "/mv/" in u and u.endswith(".html"):
|
||||
return u
|
||||
if str(raw_id).isdigit():
|
||||
return f"{h}/mv/{raw_id}.html"
|
||||
m = re.search(r"/mv/(\d+)\.html", u)
|
||||
if m:
|
||||
return f"{h}/mv/{m.group(1)}.html"
|
||||
return u
|
||||
|
||||
rid = str(raw_id or "").strip()
|
||||
if rid.isdigit():
|
||||
return f"{h}/mv/{rid}.html"
|
||||
if rid.startswith(("http://", "https://", "/")):
|
||||
return self._full_url_by_host(h, rid)
|
||||
return f"{h}/mv/{rid}.html" if rid else ""
|
||||
|
||||
# ---------------- only check quark/115 ----------------
|
||||
def _check_pan_valid(self, url, provider, timeout=3):
|
||||
if not url:
|
||||
return False
|
||||
if provider not in ("quark", "115"):
|
||||
return True
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/"
|
||||
r = requests.get(url, headers=h, timeout=timeout, verify=False, allow_redirects=True)
|
||||
if r.status_code >= 400:
|
||||
return False
|
||||
text = (r.text or "").lower()
|
||||
if provider == "quark":
|
||||
keys = ["分享已失效", "不存在", "已被取消", "取消", "删除", "已被删除", "来晚了", "违规", "无法访问"]
|
||||
else:
|
||||
keys = ["分享已失效", "不存在", "404", "已取消", "链接错误"]
|
||||
return not any(k in text for k in keys)
|
||||
except:
|
||||
return False
|
||||
|
||||
# ---------------- home ----------------
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
{"type_name": "大陆电影", "type_id": "https://www.qwmkv.com/ms/1-大陆-time---------.html"},
|
||||
{"type_name": "大陆剧集", "type_id": "https://www.qwmkv.com/ms/2-大陆-time---------.html"},
|
||||
{"type_name": "大陆综艺", "type_id": "https://www.qwmkv.com/ms/3-大陆-time---------.html"},
|
||||
{"type_name": "大陆动漫", "type_id": "https://www.qwmkv.com/ms/4-大陆-time---------.html"},
|
||||
{"type_name": "电影", "type_id": "/vt/1.html"},
|
||||
{"type_name": "综艺", "type_id": "/vt/3.html"},
|
||||
{"type_name": "剧集", "type_id": "/vt/2.html"},
|
||||
{"type_name": "动漫", "type_id": "/vt/4.html"},
|
||||
{"type_name": "短剧", "type_id": "/vt/30.html"},
|
||||
]
|
||||
return {"class": classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
doc = self._pq(self.host + "/")
|
||||
videos = self._extract_video_list(doc)
|
||||
return {"list": videos, "page": 1, "pagecount": 1, "limit": len(videos), "total": len(videos)}
|
||||
|
||||
# ---------------- category ----------------
|
||||
def _build_category_url(self, tid, pg, fdict):
|
||||
if isinstance(tid, str) and tid.startswith(("http://", "https://")):
|
||||
if pg <= 1:
|
||||
return tid
|
||||
if "---------.html" in tid:
|
||||
return tid.replace("---------.html", f"------{pg}---.html")
|
||||
return tid.replace(".html", f"-{pg}.html")
|
||||
|
||||
mid = self._get_mid(tid)
|
||||
if not fdict:
|
||||
return f"{self.host}/vt/{mid}.html" if pg <= 1 else f"{self.host}/vt/{mid}-{pg}.html"
|
||||
|
||||
area = quote(fdict.get("地区", ""), safe="")
|
||||
sort = ""
|
||||
sv = fdict.get("排序", "")
|
||||
if sv == "按时间":
|
||||
sort = "time"
|
||||
elif sv == "按人气":
|
||||
sort = "hits"
|
||||
elif sv == "按评分":
|
||||
sort = "score"
|
||||
|
||||
typ = quote(fdict.get("类型", ""), safe="")
|
||||
lang = quote(fdict.get("语言", ""), safe="")
|
||||
year = fdict.get("年代", "")
|
||||
fields = [area, sort, typ, lang, "", "", "", "", year]
|
||||
base = f"{self.host}/ms/{mid}-" + "-".join(fields)
|
||||
return base + ".html" if pg <= 1 else base + f"-{pg}.html"
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
fdict = extend if isinstance(extend, dict) else {}
|
||||
url = self._build_category_url(tid, pg, fdict)
|
||||
doc = self._pq(url)
|
||||
|
||||
if len(doc("ul.pic-list li")) == 0 and len(doc("ul.content-list li")) == 0 and pg > 1:
|
||||
doc = self._pq(url.replace(".html", f".html?page={pg}"))
|
||||
|
||||
videos = self._extract_video_list(doc)
|
||||
page_count = pg
|
||||
for a in doc(".pages a").items():
|
||||
t = (a.text() or "").strip()
|
||||
href = a.attr("href") or ""
|
||||
if t.isdigit():
|
||||
page_count = max(page_count, int(t))
|
||||
else:
|
||||
m = re.search(r"-(\d+)\.html", href)
|
||||
if m:
|
||||
page_count = max(page_count, int(m.group(1)))
|
||||
|
||||
return {
|
||||
"list": videos,
|
||||
"page": pg,
|
||||
"pagecount": max(page_count, pg),
|
||||
"limit": 30,
|
||||
"total": max(page_count, pg) * 30
|
||||
}
|
||||
|
||||
# ---------------- detail ----------------
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
vod_id = ids[0] if isinstance(ids, list) and ids else ids
|
||||
vod_id = self._full_url(vod_id)
|
||||
|
||||
doc = self._pq(vod_id)
|
||||
raw = doc.html() or ""
|
||||
|
||||
# 1. 抓取基本影视信息
|
||||
title = self._clean_text(doc("h1").eq(0).text())
|
||||
if not title:
|
||||
tt = self._clean_text(doc("title").text())
|
||||
title = tt.split("在线观看")[0] if tt else "七味资源"
|
||||
|
||||
cover = ""
|
||||
og = self._full_url(doc('meta[property="og:image"]').attr("content") or "")
|
||||
if og and not self._is_bad_cover(og):
|
||||
cover = og
|
||||
if not cover:
|
||||
c1 = self._full_url(self._img_src(doc(".main-left .img img").eq(0)))
|
||||
if c1 and not self._is_bad_cover(c1):
|
||||
cover = c1
|
||||
if not cover:
|
||||
for im in doc("img").items():
|
||||
src = self._full_url(self._img_src(im))
|
||||
if src and not self._is_bad_cover(src):
|
||||
cover = src
|
||||
break
|
||||
if not cover:
|
||||
cover = self.vod_pic_cache.get(vod_id, "")
|
||||
if not cover:
|
||||
cover = self._full_url("/template/piankuwap/image/logo.png")
|
||||
self.last_vod_pic = cover
|
||||
|
||||
content = self._clean_text(
|
||||
doc(".movie-introduce .sqjj_a").text() or
|
||||
doc(".movie-introduce .zkjj_a").text() or
|
||||
doc(".content").text()
|
||||
)
|
||||
|
||||
# =====【优化修复】2. 多线路合并遍历抓取在线资源(彻底杜绝选择器冲突覆盖) =====
|
||||
online_routes = {} # 格式: {"在线线路1": ["第1集$payload", "第2集$payload"]}
|
||||
player_uls = doc("div#url .bd ul.player, ul.player")
|
||||
line_no = 1
|
||||
for ul_node in player_uls.items():
|
||||
links = list(ul_node("a[href]").items())
|
||||
if not links:
|
||||
continue
|
||||
|
||||
route_name = f"📺在线播放-线路{line_no}"
|
||||
episodes = []
|
||||
for a in links:
|
||||
href = a.attr("href")
|
||||
if not href:
|
||||
continue
|
||||
src_name = self._clean_name(self._clean_text(a.text()) or "播放", 50)
|
||||
payload = self._b64e({"type": "py", "url": self._full_url(href), "pic": cover})
|
||||
episodes.append(f"{src_name}${payload}")
|
||||
|
||||
if episodes:
|
||||
online_routes[route_name] = episodes
|
||||
line_no += 1
|
||||
|
||||
# ===== 3. 规整网盘与磁力资源链接 =====
|
||||
pan_resources = []
|
||||
magnet_raw = []
|
||||
seen_pan = set()
|
||||
|
||||
for a in doc("a[href]").items():
|
||||
u = html.unescape(a.attr("href") or "").strip()
|
||||
if not u:
|
||||
continue
|
||||
txt = self._clean_text(a.text())
|
||||
|
||||
if u.lower().startswith("magnet:?"):
|
||||
magnet_raw.append((u, self._clean_name(txt or "磁力资源", 60)))
|
||||
continue
|
||||
|
||||
if self._is_pan(u):
|
||||
low = u.lower()
|
||||
pv = "other"
|
||||
if "pan.quark" in low: pv = "quark"
|
||||
elif "115.com" in low: pv = "115"
|
||||
elif "pan.baidu" in low: pv = "baidu"
|
||||
elif "drive.uc.cn" in low: pv = "uc"
|
||||
elif "pan.xunlei" in low: pv = "xunlei"
|
||||
elif "aliyundrive" in low or "alipan" in low: pv = "ali"
|
||||
elif "cloud.189" in low: pv = "189"
|
||||
elif "123pan" in low: pv = "pan123"
|
||||
|
||||
if u not in seen_pan:
|
||||
seen_pan.add(u)
|
||||
pan_resources.append({
|
||||
"provider": pv,
|
||||
"url": u,
|
||||
"name": txt or "网盘资源",
|
||||
"checked_valid": False
|
||||
})
|
||||
|
||||
for m in re.finditer(r"magnet:\?[^\s\"'<>]+", raw, re.I):
|
||||
magnet_raw.append((html.unescape(m.group(0)), "磁力资源"))
|
||||
|
||||
# 磁力资源精简去重并打分排序
|
||||
magnet_unified = []
|
||||
btih_seen = set()
|
||||
for u, n in magnet_raw:
|
||||
mu = self._normalize_magnet(u)
|
||||
if not mu:
|
||||
continue
|
||||
btih = self._magnet_btih(mu)
|
||||
key = btih if btih else mu.lower()
|
||||
if key in btih_seen:
|
||||
continue
|
||||
btih_seen.add(key)
|
||||
magnet_unified.append({
|
||||
"url": mu,
|
||||
"name": self._clean_name(n or "磁力资源", 60)
|
||||
})
|
||||
magnet_unified.sort(key=lambda x: -self._score_name(x.get("name", "")))
|
||||
|
||||
# ===== 4. 网盘有效性探针(保持原有超时/计数策略) =====
|
||||
check_begin = time.monotonic()
|
||||
quark_checked = 0
|
||||
valid_pan = []
|
||||
|
||||
for p in pan_resources:
|
||||
if time.monotonic() - check_begin >= self.CHECK_TIME_BUDGET:
|
||||
valid_pan.append(p)
|
||||
continue
|
||||
|
||||
pv = p["provider"]
|
||||
if pv == "quark":
|
||||
if quark_checked >= self.QUARK_CHECK_LIMIT:
|
||||
valid_pan.append(p)
|
||||
continue
|
||||
quark_checked += 1
|
||||
if self._check_pan_valid(p["url"], "quark"):
|
||||
p["checked_valid"] = True
|
||||
valid_pan.append(p)
|
||||
elif pv == "115":
|
||||
if self._check_pan_valid(p["url"], "115"):
|
||||
p["checked_valid"] = True
|
||||
valid_pan.append(p)
|
||||
else:
|
||||
valid_pan.append(p)
|
||||
|
||||
# =====【功能修复】5. 网盘资源渠道完全独立隔离,防止错乱混杂 =====
|
||||
pan_routes = {
|
||||
"quark": {"name": "🟢夸克网盘", "list": []},
|
||||
"ali": {"name": "☁️阿里云盘", "list": []},
|
||||
"115": {"name": "固定115网盘", "list": []},
|
||||
"baidu": {"name": "📘百度网盘", "list": []},
|
||||
"uc": {"name": "📱UC网盘", "list": []},
|
||||
"xunlei": {"name": "⚡迅雷网盘", "list": []},
|
||||
"189": {"name": "☎️天翼云盘", "list": []},
|
||||
"pan123": {"name": "📦123网盘", "list": []},
|
||||
"other": {"name": "📦其它网盘", "list": []}
|
||||
}
|
||||
|
||||
for r in valid_pan:
|
||||
prov = r["provider"]
|
||||
if prov not in pan_routes:
|
||||
prov = "other"
|
||||
|
||||
ep_name = self._clean_name(r.get('name', '网盘提取资源'), 60)
|
||||
payload = self._b64e({"type": "pan", "url": r["url"], "pic": cover})
|
||||
pan_routes[prov]["list"].append(f"{ep_name}${payload}")
|
||||
|
||||
# =====【核心修复】6. 按标准大屏壳子1:1规则完美有序组装,防串位 =====
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 分支 A:写入网盘分类线路(依照预设的网盘体验优先级高低呈现)
|
||||
drive_order = ["quark", "ali", "115", "baidu", "uc", "xunlei", "189", "pan123", "other"]
|
||||
for d_key in drive_order:
|
||||
route_info = pan_routes[d_key]
|
||||
if route_info["list"]:
|
||||
play_from.append(route_info["name"])
|
||||
play_url.append("#".join(route_info["list"]))
|
||||
|
||||
# 分支 B:写入磁力解析相关线路(保持互相隔离)
|
||||
if magnet_unified:
|
||||
lines_115 = []
|
||||
lines_play = []
|
||||
for i, m in enumerate(magnet_unified, start=1):
|
||||
nm = self._clean_name(f"磁力源-{i:02d} {m['name']}", 60)
|
||||
encoded_mag = base64.urlsafe_b64encode(m['url'].encode()).decode().rstrip("=")
|
||||
p_payload = self._b64e({"type": "magnet", "url": m['url'], "pic": cover})
|
||||
|
||||
lines_115.append(f"{nm}${encoded_mag}")
|
||||
lines_play.append(f"{nm}${p_payload}")
|
||||
|
||||
# 独立线路一:115离线专用线
|
||||
play_from.append("📥115云下载")
|
||||
play_url.append("#".join(lines_115))
|
||||
|
||||
# 独立一条空白ACK确认交互线
|
||||
play_from.append("0")
|
||||
play_url.append("已提交请到115离线任务查看$__ACK__")
|
||||
|
||||
# 独立线路二:自带流播或通过本地壳嗅探弹磁力
|
||||
play_from.append("🧲磁力播放")
|
||||
play_url.append("#".join(lines_play))
|
||||
|
||||
# 分支 C:写入在线直连/网页采集线路
|
||||
for r_name, r_eps in online_routes.items():
|
||||
play_from.append(r_name)
|
||||
play_url.append("#".join(r_eps))
|
||||
|
||||
# ===== 当前站搜索入口 =====
|
||||
try:
|
||||
search_payload = self._b64e({
|
||||
"type": "search",
|
||||
"wd": title,
|
||||
"pic": cover
|
||||
})
|
||||
|
||||
play_from.insert(0, "🔍点击选择")
|
||||
play_url.insert(0, f"当前站搜索${search_payload}")
|
||||
except Exception as e:
|
||||
print(f"search line add error: {e}")
|
||||
|
||||
# 兜底处理
|
||||
if not play_from:
|
||||
play_from.append("🌐原网页查看")
|
||||
play_url.append(f"点击跳转原详情页${self._b64e({'type': 'web', 'url': vod_id, 'pic': cover})}")
|
||||
|
||||
# 7. 构建标准影视输出字典
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": self._clean_name(title, 100),
|
||||
"vod_pic": cover,
|
||||
"vod_content": content,
|
||||
"vod_play_from": "$$$".join(play_from),
|
||||
"vod_play_url": "$$$".join(play_url)
|
||||
}
|
||||
return {"list": [vod]}
|
||||
except Exception as e:
|
||||
print(f"detailContent error: {e}")
|
||||
return {"list": []}
|
||||
|
||||
# ---------------- player ----------------
|
||||
def _parse_py_page(self, py_url):
|
||||
r = self._fetch(py_url, timeout=10)
|
||||
if not r:
|
||||
return ""
|
||||
txt = r.text or ""
|
||||
|
||||
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*<", txt, re.S)
|
||||
if not m:
|
||||
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*;", txt, re.S)
|
||||
if not m:
|
||||
return ""
|
||||
|
||||
js = m.group(1)
|
||||
try:
|
||||
js2 = re.sub(r"(\w+)\s*:", r'"\1":', js)
|
||||
obj = json.loads(js2)
|
||||
except:
|
||||
try:
|
||||
obj = json.loads(js)
|
||||
except:
|
||||
return ""
|
||||
|
||||
u = obj.get("url", "") or ""
|
||||
enc = str(obj.get("encrypt", "0"))
|
||||
if enc == "1":
|
||||
u = unquote(u)
|
||||
elif enc == "2":
|
||||
try:
|
||||
u = unquote(base64.b64decode(u).decode("utf-8", "ignore"))
|
||||
except:
|
||||
pass
|
||||
|
||||
if u.startswith("//"):
|
||||
u = "https:" + u
|
||||
elif u.startswith("/"):
|
||||
u = self._full_url(u)
|
||||
return u
|
||||
|
||||
def _return_ack_video(self):
|
||||
ret = {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": self.ack_mp4,
|
||||
"header": {
|
||||
"User-Agent": self.headers.get("User-Agent", ""),
|
||||
"Referer": self.host + "/"
|
||||
}
|
||||
}
|
||||
if self.last_vod_pic:
|
||||
ret["pic"] = self.last_vod_pic
|
||||
ret["poster"] = self.last_vod_pic
|
||||
return ret
|
||||
|
||||
def _add_to_115(self, magnet):
|
||||
if not self.pan_115_cookie:
|
||||
print("115添加失败: 未配置 pan_115_cookie")
|
||||
return
|
||||
|
||||
magnet = self._normalize_magnet(magnet)
|
||||
if not magnet:
|
||||
print("115添加失败: 非法磁力")
|
||||
return
|
||||
|
||||
headers = {
|
||||
"User-Agent": self.headers.get("User-Agent", ""),
|
||||
"Cookie": self.pan_115_cookie,
|
||||
"Origin": "https://115.com",
|
||||
"Referer": "https://115.com/web/lixian/",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
|
||||
try:
|
||||
pan_sess = requests.Session()
|
||||
pan_sess.verify = False
|
||||
pan_sess.mount("http://", SSLAdapter(max_retries=2))
|
||||
pan_sess.mount("https://", SSLAdapter(max_retries=2))
|
||||
|
||||
space_resp = pan_sess.get("https://115.com/?ct=offline&ac=space", headers=headers, timeout=10)
|
||||
try:
|
||||
space_json = space_resp.json()
|
||||
except:
|
||||
print(f"115获取签名失败(非JSON): {space_resp.text[:200]}")
|
||||
return
|
||||
|
||||
if not space_json.get("state"):
|
||||
print(f"115获取签名失败(可能Cookie过期): {space_json}")
|
||||
return
|
||||
|
||||
sign = space_json.get("sign", "")
|
||||
req_time = space_json.get("time", "")
|
||||
if not sign or not req_time:
|
||||
print(f"115签名数据异常: {space_json}")
|
||||
return
|
||||
|
||||
uid_match = re.search(r'UID=(\d+)', self.pan_115_cookie)
|
||||
uid = uid_match.group(1) if uid_match else ""
|
||||
|
||||
add_url = "https://115.com/web/lixian/?ct=lixian&ac=add_task_url"
|
||||
post_data = {"url": magnet, "uid": uid, "sign": sign, "time": req_time}
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
|
||||
add_resp = pan_sess.post(add_url, data=post_data, headers=headers, timeout=10)
|
||||
try:
|
||||
add_json = add_resp.json()
|
||||
except:
|
||||
print(f"115添加失败(非JSON): {add_resp.text[:200]}")
|
||||
return
|
||||
|
||||
if add_json.get("state") or add_json.get("errcode") == 0:
|
||||
print(f"115离线添加成功: {magnet[:100]}...")
|
||||
else:
|
||||
err = add_json.get("error_msg") or add_json.get("msg") or add_json.get("error") or str(add_json)
|
||||
print(f"115添加失败: {err}")
|
||||
except Exception as e:
|
||||
print(f"115离线网络异常: {e}")
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if flag == "0" or id == "__ACK__":
|
||||
return self._return_ack_video()
|
||||
|
||||
if flag == "📥115云下载":
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(id.encode() + b"==").decode()
|
||||
magnet = self._normalize_magnet(decoded)
|
||||
if not magnet:
|
||||
return self._return_ack_video()
|
||||
if not self.pan_115_cookie:
|
||||
print("115未配置Cookie")
|
||||
return self._return_ack_video()
|
||||
|
||||
threading.Thread(target=self._add_to_115, args=(magnet,), daemon=True).start()
|
||||
return self._return_ack_video()
|
||||
except Exception as e:
|
||||
print(f"115云下载处理异常: {e}")
|
||||
return self._return_ack_video()
|
||||
|
||||
if flag == "🧲磁力播放":
|
||||
data = self._b64d(id)
|
||||
if data and data.get("type") == "magnet":
|
||||
mu = self._normalize_magnet(data.get("url", ""))
|
||||
if mu:
|
||||
pic = data.get("pic") or self.last_vod_pic
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
|
||||
|
||||
if isinstance(id, str) and id.startswith("push://"):
|
||||
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
mu = self._normalize_magnet(id)
|
||||
if mu:
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
data = self._b64d(id)
|
||||
if not data:
|
||||
if isinstance(id, str) and id.startswith("push://"):
|
||||
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
|
||||
|
||||
typ = data.get("type", "")
|
||||
url = data.get("url", "")
|
||||
pic = data.get("pic") or self.last_vod_pic
|
||||
|
||||
if not url:
|
||||
return {"parse": 1, "url": id, "pic": pic, "poster": pic}
|
||||
|
||||
if typ == "search":
|
||||
|
||||
wd = data.get("wd", "").strip()
|
||||
|
||||
if not wd:
|
||||
return {
|
||||
"parse": 1,
|
||||
"url": self.host,
|
||||
"pic": pic,
|
||||
"poster": pic
|
||||
}
|
||||
|
||||
search_url = f"{self.host}/vodsearch/{quote(wd)}----------1---.html"
|
||||
|
||||
return {
|
||||
"parse": 0,
|
||||
"url": "push://" + search_url,
|
||||
"pic": pic,
|
||||
"poster": pic
|
||||
}
|
||||
|
||||
if typ == "pan":
|
||||
return {"parse": 0, "url": "push://" + url, "pic": pic, "poster": pic}
|
||||
if typ == "magnet":
|
||||
mu = self._normalize_magnet(url)
|
||||
if mu:
|
||||
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
if typ == "py":
|
||||
real = self._parse_py_page(url)
|
||||
if real:
|
||||
return {"parse": 0, "url": real, "pic": pic, "poster": pic}
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
if typ == "web":
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
|
||||
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
|
||||
|
||||
# ---------------- search ----------------
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
wd = quote(key)
|
||||
|
||||
for h in self.hosts:
|
||||
suggest_api = f"{h}/index.php/ajax/suggest?mid=1&limit=20&wd={wd}"
|
||||
try:
|
||||
r = self.session.get(suggest_api, timeout=8, headers=self.headers, verify=False)
|
||||
txt = r.text or ""
|
||||
if not self._is_verify_page(txt) and r.status_code == 200:
|
||||
data = r.json()
|
||||
lst = data.get("list") or []
|
||||
videos = []
|
||||
|
||||
for it in lst:
|
||||
vid = it.get("id") or it.get("vod_id")
|
||||
name = it.get("name") or it.get("vod_name") or ""
|
||||
pic = self._full_url_by_host(h, it.get("pic") or it.get("vod_pic") or "")
|
||||
remarks = self._clean_text(it.get("en") or it.get("remark") or "")
|
||||
jump_url = it.get("url") or it.get("link") or ""
|
||||
|
||||
vid_url = self._mk_vod_id(h, vid, jump_url)
|
||||
if not vid_url:
|
||||
continue
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid_url] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid_url,
|
||||
"vod_name": self._clean_name(name, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
|
||||
if videos:
|
||||
self.host = h
|
||||
return {
|
||||
"list": videos,
|
||||
"page": pg,
|
||||
"pagecount": pg + 1 if len(videos) >= 20 else pg,
|
||||
"limit": len(videos),
|
||||
"total": len(videos)
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
api_list = [
|
||||
f"{h}/api.php/provide/vod/?ac=detail&wd={wd}&pg={pg}",
|
||||
f"{h}/api.php/provide/vod?ac=detail&wd={wd}&pg={pg}",
|
||||
]
|
||||
for api in api_list:
|
||||
try:
|
||||
r = self.session.get(api, timeout=8, headers=self.headers, verify=False)
|
||||
txt = r.text or ""
|
||||
if self._is_verify_page(txt):
|
||||
continue
|
||||
if r.status_code != 200:
|
||||
continue
|
||||
|
||||
data = r.json()
|
||||
lst = data.get("list") or data.get("data") or []
|
||||
videos = []
|
||||
|
||||
for it in lst:
|
||||
vid = it.get("vod_id") or it.get("id")
|
||||
name = it.get("vod_name") or it.get("name") or ""
|
||||
pic = self._full_url_by_host(h, it.get("vod_pic") or it.get("pic") or "")
|
||||
remarks = self._clean_text(it.get("vod_remarks") or it.get("remarks") or "")
|
||||
jump_url = it.get("vod_play_url") or it.get("url") or it.get("link") or ""
|
||||
|
||||
vid_url = self._mk_vod_id(h, vid, jump_url)
|
||||
if not vid_url:
|
||||
continue
|
||||
|
||||
if pic:
|
||||
self.vod_pic_cache[vid_url] = pic
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid_url,
|
||||
"vod_name": self._clean_name(name, 80),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
|
||||
if videos:
|
||||
self.host = h
|
||||
return {
|
||||
"list": videos,
|
||||
"page": int(data.get("page", pg) or pg),
|
||||
"pagecount": int(data.get("pagecount", 1) or 1),
|
||||
"limit": len(videos),
|
||||
"total": int(data.get("total", len(videos)) or len(videos))
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"list": [],
|
||||
"page": pg,
|
||||
"pagecount": pg,
|
||||
"limit": 0,
|
||||
"total": 0
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 专属全网聚合 Python版
|
||||
# 适配常见 Cat/TVBox Python Spider
|
||||
#本地py适配 😂
|
||||
|
||||
import json
|
||||
import requests
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
sources = {
|
||||
's1': {'name': '🎬电影天堂', 'api': 'http://caiji.dyttzyapi.com/api.php/provide/vod/from/dyttm3u8/at/json'},
|
||||
's2': {'name': '💧无水印', 'api': 'https://api.wsyzy.net/api.php/provide/vod'},
|
||||
's3': {'name': '🧸量子', 'api': 'https://cj.lziapi.com/api.php/provide/vod'},
|
||||
's4': {'name': '📺1080资源', 'api': 'https://api.1080zyku.com/inc/api_mac10.php'},
|
||||
's5': {'name': '🔥155资源', 'api': 'https://155api.com/api.php/provide/vod'},
|
||||
's6': {'name': '📺天涯', 'api': 'https://tyyszy.com/api.php/provide/vod'},
|
||||
's7': {'name': '📺暴风', 'api': 'https://bfzyapi.com/api.php/provide/vod'},
|
||||
's8': {'name': '⚡索尼闪电', 'api': 'https://xsd.sdzyapi.com/api.php/provide/vod'},
|
||||
's9': {'name': '📺索尼', 'api': 'https://suoniapi.com/api.php/provide/vod'},
|
||||
's10': {'name': '📺红牛', 'api': 'https://www.hongniuzy2.com/api.php/provide/vod'},
|
||||
's11': {'name': '📺茅台', 'api': 'https://caiji.maotaizy.cc/api.php/provide/vod'},
|
||||
's12': {'name': '🐯虎牙', 'api': 'https://www.huyaapi.com/api.php/provide/vod'},
|
||||
's13': {'name': '📺豆瓣', 'api': 'https://caiji.dbzy.tv/api.php/provide/vod'},
|
||||
's14': {'name': '📺豆瓣2', 'api': 'https://dbzy.tv/api.php/provide/vod'},
|
||||
's15': {'name': '📺豪华', 'api': 'https://hhzyapi.com/api.php/provide/vod'},
|
||||
's16': {'name': '📺CK资源', 'api': 'https://ckzy.me/api.php/provide/vod'},
|
||||
's17': {'name': '📺U酷', 'api': 'https://api.ukuapi.com/api.php/provide/vod'},
|
||||
's18': {'name': '📺ikun', 'api': 'https://ikunzyapi.com/api.php/provide/vod'},
|
||||
's19': {'name': '📺无尽', 'api': 'https://api.wujinapi.cc/api.php/provide/vod'},
|
||||
's20': {'name': '🌕光速', 'api': 'https://api.guangsuapi.com/api.php/provide/vod'},
|
||||
's21': {'name': '📺卧龙', 'api': 'https://collect.wolongzyw.com/api.php/provide/vod'},
|
||||
's22': {'name': '📺新浪', 'api': 'https://api.xinlangapi.com/xinlangapi.php/provide/vod'},
|
||||
's23': {'name': '📺旺旺', 'api': 'https://api.wwzy.tv/api.php/provide/vod'},
|
||||
's24': {'name': '📺最大', 'api': 'https://api.zuidapi.com/api.php/provide/vod'},
|
||||
's25': {'name': '🌸樱花', 'api': 'https://m3u8.apiyhzy.com/api.php/provide/vod'},
|
||||
's26': {'name': '🐮牛牛', 'api': 'https://api.niuniuzy.me/api.php/provide/vod'},
|
||||
's27': {'name': '☁️百度云', 'api': 'https://api.apibdzy.com/api.php/provide/vod'},
|
||||
's28': {'name': '🏎速播', 'api': 'https://subocaiji.com/api.php/provide/vod'},
|
||||
's29': {'name': '🦅金鹰', 'api': 'https://jinyingzy.com/api.php/provide/vod'},
|
||||
's30': {'name': '⚡闪电', 'api': 'https://sdzyapi.com/api.php/provide/vod'},
|
||||
's31': {'name': '👑非凡', 'api': 'https://cj.ffzyapi.com/api.php/provide/vod'},
|
||||
's32': {'name': '🍃飘零', 'api': 'https://p2100.net/api.php/provide/vod'},
|
||||
's33': {'name': '🐾魔爪', 'api': 'https://mozhuazy.com/api.php/provide/vod'},
|
||||
's34': {'name': '📺魔都', 'api': 'https://www.mdzyapi.com/api.php/provide/vod'},
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return "影视+专属全网聚合"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def fetch(self, url, timeout=8):
|
||||
try:
|
||||
r = requests.get(
|
||||
url,
|
||||
headers=self.headers,
|
||||
timeout=timeout,
|
||||
verify=False
|
||||
)
|
||||
return r.text
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def clean_item(self, item, source_key, source_name, is_detail=False):
|
||||
item = dict(item)
|
||||
|
||||
if not is_detail:
|
||||
item["vod_id"] = f"{source_key}@@{item.get('vod_id', '')}"
|
||||
|
||||
remarks = item.get("vod_remarks", "")
|
||||
item["vod_remarks"] = f"{source_name} | {remarks}"
|
||||
|
||||
if item.get("vod_play_from"):
|
||||
froms = item["vod_play_from"].split("$$$")
|
||||
froms = [f"{source_name}-{x}" for x in froms]
|
||||
item["vod_play_from"] = "$$$".join(froms)
|
||||
|
||||
item.pop("vod_down_from", None)
|
||||
item.pop("vod_down_url", None)
|
||||
|
||||
return item
|
||||
|
||||
def homeContent(self, filter):
|
||||
classes = []
|
||||
filters = {}
|
||||
|
||||
def load_class(key, source):
|
||||
url = f"{source['api']}?ac=list"
|
||||
html = self.fetch(url, 4)
|
||||
|
||||
try:
|
||||
data = json.loads(html)
|
||||
except:
|
||||
data = {}
|
||||
|
||||
vals = [{"n": "全部(最新)", "v": ""}]
|
||||
|
||||
for c in data.get("class", []):
|
||||
vals.append({
|
||||
"n": c.get("type_name", ""),
|
||||
"v": c.get("type_id", "")
|
||||
})
|
||||
|
||||
return key, vals
|
||||
|
||||
with ThreadPoolExecutor(max_workers=16) as executor:
|
||||
futures = []
|
||||
|
||||
for key, source in self.sources.items():
|
||||
classes.append({
|
||||
"type_id": key,
|
||||
"type_name": source["name"]
|
||||
})
|
||||
|
||||
futures.append(executor.submit(load_class, key, source))
|
||||
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
key, vals = future.result()
|
||||
|
||||
filters[key] = [{
|
||||
"key": "cateId",
|
||||
"name": "分类",
|
||||
"value": vals
|
||||
}]
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"class": classes,
|
||||
"filters": filters,
|
||||
"list": []
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid not in self.sources:
|
||||
return {"list": []}
|
||||
|
||||
source = self.sources[tid]
|
||||
|
||||
cate_id = ""
|
||||
if isinstance(extend, dict):
|
||||
cate_id = extend.get("cateId", "")
|
||||
|
||||
url = f"{source['api']}?ac=detail&pg={pg}"
|
||||
|
||||
if cate_id:
|
||||
url += f"&t={cate_id}"
|
||||
|
||||
html = self.fetch(url)
|
||||
|
||||
try:
|
||||
data = json.loads(html)
|
||||
except:
|
||||
data = {}
|
||||
|
||||
result = []
|
||||
|
||||
for item in data.get("list", []):
|
||||
result.append(
|
||||
self.clean_item(
|
||||
item,
|
||||
tid,
|
||||
source["name"],
|
||||
False
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"list": result,
|
||||
"page": data.get("page", pg),
|
||||
"pagecount": data.get("pagecount", 1),
|
||||
"limit": data.get("limit", 20),
|
||||
"total": data.get("total", len(result))
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
if isinstance(ids, list):
|
||||
ids = ids[0]
|
||||
|
||||
if "@@" not in ids:
|
||||
return {"list": []}
|
||||
|
||||
source_key, real_id = ids.split("@@", 1)
|
||||
|
||||
if source_key not in self.sources:
|
||||
return {"list": []}
|
||||
|
||||
source = self.sources[source_key]
|
||||
|
||||
url = f"{source['api']}?ac=detail&ids={real_id}"
|
||||
|
||||
html = self.fetch(url)
|
||||
|
||||
try:
|
||||
data = json.loads(html)
|
||||
except:
|
||||
data = {}
|
||||
|
||||
result = []
|
||||
|
||||
for item in data.get("list", []):
|
||||
cleaned = self.clean_item(
|
||||
item,
|
||||
source_key,
|
||||
source["name"],
|
||||
True
|
||||
)
|
||||
|
||||
cleaned["vod_id"] = ids
|
||||
|
||||
result.append(cleaned)
|
||||
|
||||
return {"list": result}
|
||||
|
||||
def search_one(self, source_key, source, keyword, pg):
|
||||
url = f"{source['api']}?ac=detail&wd={keyword}&pg={pg}"
|
||||
|
||||
html = self.fetch(url, 6)
|
||||
|
||||
try:
|
||||
data = json.loads(html)
|
||||
except:
|
||||
data = {}
|
||||
|
||||
result = []
|
||||
|
||||
for item in data.get("list", []):
|
||||
result.append(
|
||||
self.clean_item(
|
||||
item,
|
||||
source_key,
|
||||
source["name"],
|
||||
False
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"list": result,
|
||||
"pagecount": data.get("pagecount", 1)
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick=False, pg=1):
|
||||
result = []
|
||||
max_page = 1
|
||||
|
||||
with ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = []
|
||||
|
||||
for source_key, source in self.sources.items():
|
||||
futures.append(
|
||||
executor.submit(
|
||||
self.search_one,
|
||||
source_key,
|
||||
source,
|
||||
key,
|
||||
pg
|
||||
)
|
||||
)
|
||||
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
data = future.result()
|
||||
|
||||
result.extend(data["list"])
|
||||
|
||||
if data["pagecount"] > max_page:
|
||||
max_page = data["pagecount"]
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"list": result,
|
||||
"page": pg,
|
||||
"pagecount": max_page,
|
||||
"limit": 40,
|
||||
"total": 9999
|
||||
}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": id,
|
||||
"header": self.headers
|
||||
}
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, "text/plain", "ok"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Spider().run()
|
||||
+24
-19
@@ -46,17 +46,17 @@
|
||||
"ext": "https://ghfast.top/https://raw.githubusercontent.com/IY-CPU/IY/main/lib/茫茫.png"
|
||||
},
|
||||
{
|
||||
"key": "MGtv",
|
||||
"name": "🐬芒果TV.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/芒果TV.py"
|
||||
"key": "MGtv",
|
||||
"name": "🐬芒果TV.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/芒果TV.py"
|
||||
},
|
||||
{
|
||||
"key": "ppx",
|
||||
"name": "🐬皮皮虾.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
"key": "ppx",
|
||||
"name": "🐬皮皮虾.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
},
|
||||
{
|
||||
"key": "fY",
|
||||
@@ -112,6 +112,18 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/毒舌影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qw",
|
||||
"name": "🐬七味.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七味.py"
|
||||
},
|
||||
{
|
||||
"key": "cjjh",
|
||||
"name": "🐬采集聚合.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/采集聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kf",
|
||||
"name": "🐬咖啡体育直播.py",
|
||||
@@ -387,10 +399,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "XFDM",
|
||||
"name": "🐬稀饭动漫.py",
|
||||
"key": "MiFun",
|
||||
"name": "🐬MiFun动漫.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/稀饭动漫.py"
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/MiFun动漫.py"
|
||||
},
|
||||
{
|
||||
"key": "cj_360资源",
|
||||
@@ -536,13 +548,6 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/123AV.py"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "hanime1",
|
||||
"name": "🐬hanime1.py|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/hanime1.py"
|
||||
},
|
||||
{
|
||||
"key": "Xvideos",
|
||||
"name": "🐬Xvideos.py|🔞",
|
||||
|
||||
+14
-2
@@ -78,7 +78,7 @@
|
||||
},
|
||||
{
|
||||
"key": "GZ",
|
||||
"name": "🐬瓜子APP.py(关梯)",
|
||||
"name": "🐬瓜子APP.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
@@ -112,6 +112,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/毒舌影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qw",
|
||||
"name": "🐬七味.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七味.py"
|
||||
},
|
||||
{
|
||||
"key": "kf",
|
||||
"name": "🐬咖啡体育直播.py",
|
||||
@@ -364,7 +370,13 @@
|
||||
"港台动漫",
|
||||
"海外动漫"
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "MiFun",
|
||||
"name": "🐬MiFun动漫.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/MiFun动漫.py"
|
||||
},
|
||||
{
|
||||
"key": "cj_360资源",
|
||||
"name": "🐬360丨短剧",
|
||||
|
||||
+19
-1
@@ -139,10 +139,22 @@
|
||||
},
|
||||
{
|
||||
"key": "dsys",
|
||||
"name": "🐬毒舌影视.py(关梯)",
|
||||
"name": "🐬毒舌影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/毒舌影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qw",
|
||||
"name": "🐬七味.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七味.py"
|
||||
},
|
||||
{
|
||||
"key": "cjjh",
|
||||
"name": "🐬采集聚合.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/采集聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "fY",
|
||||
"name": "🐬枫叶影院.py(关梯)[追剧]",
|
||||
@@ -469,6 +481,12 @@
|
||||
"海外动漫"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "MiFun",
|
||||
"name": "🐬MiFun动漫.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/MiFun动漫.py"
|
||||
},
|
||||
{
|
||||
"key": "cj_360资源",
|
||||
"name": "🐬360[短剧]",
|
||||
|
||||
+14
-2
@@ -102,7 +102,7 @@
|
||||
},
|
||||
{
|
||||
"key": "GZ",
|
||||
"name": "🐬瓜子APP.py(关梯)",
|
||||
"name": "🐬瓜子APP.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
@@ -136,6 +136,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/毒舌影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qw",
|
||||
"name": "🐬七味.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七味.py"
|
||||
},
|
||||
{
|
||||
"key": "fY",
|
||||
"name": "🐬枫叶影院.py(关梯子使用)",
|
||||
@@ -440,7 +446,13 @@
|
||||
"港台动漫",
|
||||
"海外动漫"
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "MiFun",
|
||||
"name": "🐬MiFun动漫.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/MiFun动漫.py"
|
||||
},
|
||||
{
|
||||
"key": "cj_360资源",
|
||||
"name": "🐬360丨短剧",
|
||||
|
||||
Reference in New Issue
Block a user