上传文件至「py」
This commit is contained in:
+329
@@ -0,0 +1,329 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者的内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
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 datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://search.bilibili.com"
|
||||
|
||||
xurl1 = "https://api.live.bilibili.com"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global xurl1
|
||||
global headerx
|
||||
|
||||
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": "舞", "type_name": "舞蹈"},
|
||||
{"type_id": "音乐", "type_name": "音乐"},
|
||||
{"type_id": "手游", "type_name": "手游"},
|
||||
{"type_id": "网游", "type_name": "网游"},
|
||||
{"type_id": "单机游戏", "type_name": "单机游戏"},
|
||||
{"type_id": "虚拟主播", "type_name": "虚拟主播"},
|
||||
{"type_id": "电台", "type_name": "电台"},
|
||||
{"type_id": "体育", "type_name": "体育"},
|
||||
{"type_id": "聊天", "type_name": "聊天"},
|
||||
{"type_id": "娱乐", "type_name": "娱乐"},
|
||||
{"type_id": "电影", "type_name": "影视"},
|
||||
{"type_id": "新闻", "type_name": "新闻"}]
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{xurl}/live?keyword={cid}&page={str(page)}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', class_="video-list-item")
|
||||
|
||||
for vod in soups:
|
||||
|
||||
names = vod.find('h3', class_="bili-live-card__info--tit")
|
||||
name = names.text.strip().replace('直播中', '')
|
||||
|
||||
href = names.find('a')['href']
|
||||
id = self.extract_middle_text(href, 'bilibili.com/', '?', 0)
|
||||
# 兜底:如果链接没有 ? 参数,用正则直接提取房间号
|
||||
if not id:
|
||||
m = re.search(r'bilibili\.com/(\d+)', href)
|
||||
if m:
|
||||
id = m.group(1)
|
||||
|
||||
pic = vod.find('img')['src']
|
||||
if 'http' not in pic:
|
||||
pic = "https:" + pic
|
||||
|
||||
remarks = vod.find('a', class_="bili-live-card__info--uname")
|
||||
remark = remarks.text.strip()
|
||||
|
||||
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 = []
|
||||
bofang = ''
|
||||
|
||||
url = f'{xurl1}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={did}&platform=web&protocol=0,1&format=0,1,2&codec=0,1'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
data = detail.json()
|
||||
|
||||
content = '欢迎观看哔哩直播'
|
||||
|
||||
try:
|
||||
setup = data['data']['playurl_info']['playurl']['stream']
|
||||
except (KeyError, TypeError):
|
||||
setup = []
|
||||
|
||||
line_count = 0
|
||||
for stream in setup:
|
||||
for fmt in stream.get('format', []):
|
||||
for codec in fmt.get('codec', []):
|
||||
base_url = codec.get('base_url', '')
|
||||
url_info_list = codec.get('url_info', [])
|
||||
if not base_url or not url_info_list:
|
||||
continue
|
||||
|
||||
# 遍历所有CDN节点,取第一个可用的
|
||||
for uinfo in url_info_list:
|
||||
host = uinfo.get('host', '')
|
||||
extra = uinfo.get('extra', '')
|
||||
if not host or not extra:
|
||||
continue
|
||||
|
||||
# 处理 host 和 base_url 之间可能出现的双斜杠
|
||||
if host.endswith('/') and base_url.startswith('/'):
|
||||
base_url = base_url[1:]
|
||||
play_url = host + base_url + extra
|
||||
|
||||
line_count += 1
|
||||
namc = f"{line_count}号线路"
|
||||
bofang += f"{namc}${play_url}#"
|
||||
break # 每个 codec 只取第一个可用 CDN
|
||||
|
||||
if bofang:
|
||||
bofang = bofang[:-1]
|
||||
|
||||
xianlu = '哔哩专线'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_content": content,
|
||||
"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
|
||||
# B站直播流必须带 Referer,否则 403
|
||||
result["header"] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
|
||||
'Referer': 'https://live.bilibili.com/',
|
||||
'Origin': 'https://live.bilibili.com'
|
||||
}
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{xurl}/live?keyword={key}&page={str(page)}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', class_="video-list-item")
|
||||
|
||||
for vod in soups:
|
||||
|
||||
names = vod.find('h3', class_="bili-live-card__info--tit")
|
||||
name = names.text.strip().replace('直播中', '')
|
||||
|
||||
href = names.find('a')['href']
|
||||
id = self.extract_middle_text(href, 'bilibili.com/', '?', 0)
|
||||
# 兜底:如果链接没有 ? 参数,用正则直接提取房间号
|
||||
if not id:
|
||||
m = re.search(r'bilibili\.com/(\d+)', href)
|
||||
if m:
|
||||
id = m.group(1)
|
||||
|
||||
pic = vod.find('img')['src']
|
||||
if 'http' not in pic:
|
||||
pic = "https:" + pic
|
||||
|
||||
remarks = vod.find('a', class_="bili-live-card__info--uname")
|
||||
remark = remarks.text.strip()
|
||||
|
||||
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 searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
@@ -0,0 +1,542 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 大马猴影视 - 整合 bubutv 线路解析逻辑,实现直接播放(无需外置解析)
|
||||
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import hashlib
|
||||
from urllib.parse import urlencode, quote
|
||||
from html.parser import HTMLParser
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class _HTMLTextExtractor(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._text = []
|
||||
|
||||
def handle_data(self, data):
|
||||
self._text.append(data)
|
||||
|
||||
def get_text(self):
|
||||
return ''.join(self._text)
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.host = 'https://dmhyy.com'
|
||||
self.classes = [
|
||||
{'type_id': '23', 'type_name': '电影'},
|
||||
{'type_id': '22', 'type_name': '剧集'},
|
||||
{'type_id': '24', 'type_name': '动漫'},
|
||||
{'type_id': '25', 'type_name': '综艺'},
|
||||
]
|
||||
self.web_sign = ''
|
||||
self.x_client = 'YOUR_PUBLIC_CLIENT_ID' # 请替换为真实值
|
||||
self._app_device_id = ''
|
||||
|
||||
def init(self, extend=''):
|
||||
try:
|
||||
if extend:
|
||||
if isinstance(extend, dict):
|
||||
ext = extend
|
||||
else:
|
||||
text = str(extend).strip()
|
||||
ext = json.loads(text) if text.startswith('{') else {'site': text}
|
||||
site = ext.get('site') or ext.get('host') or ''
|
||||
if site:
|
||||
self.host = str(site).split(',')[0].strip().rstrip('/')
|
||||
self.web_sign = ext.get('web-sign') or ext.get('web_sign') or self.web_sign
|
||||
self.x_client = ext.get('x-client') or ext.get('x_client') or self.x_client
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _ensure_ready(self):
|
||||
if not getattr(self, 'host', ''):
|
||||
self.host = 'https://dmhyy.com'
|
||||
self.host = self.host.rstrip('/')
|
||||
|
||||
def getName(self):
|
||||
return '大马猴影视'
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return bool(re.search(r'\.(m3u8|mp4|flv|mkv|avi)(\?|#|$|\s)', str(url or ''), re.I))
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
def _headers(self, referer=''):
|
||||
self._ensure_ready()
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Referer': referer or (self.host + '/'),
|
||||
'x-client': self.x_client,
|
||||
'x-platform': 'web',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
}
|
||||
if getattr(self, 'web_sign', ''):
|
||||
headers['web-sign'] = self.web_sign
|
||||
return headers
|
||||
|
||||
def _app_headers(self):
|
||||
"""生成 app 接口需要的签名头(与 bubutv 一致)"""
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = ''.join([str(random.randint(0, 9)) for _ in range(3)])
|
||||
pkg = 'com.sunshine.tv'
|
||||
ver = '4'
|
||||
finger = 'SF-C3B2B41F6EFFFF9869176CF68F6790E8F07506FC88632C94B4F5F0430D5498CA'
|
||||
sign_str = f"finger={finger}&id={pkg}&nonce={nonce}&sk=SK-thanks&time={timestamp}&v={ver}"
|
||||
sign = hashlib.sha256(sign_str.encode()).hexdigest().upper()
|
||||
|
||||
if not self._app_device_id or len(self._app_device_id) != 16:
|
||||
self._app_device_id = ''.join([random.choice('0123456789abcdef') for _ in range(16)])
|
||||
|
||||
return {
|
||||
'User-Agent': 'okhttp/4.12.0',
|
||||
'Accept': 'application/json',
|
||||
'x-aid': pkg,
|
||||
'x-ave': ver,
|
||||
'x-time': timestamp,
|
||||
'x-nonc': nonce,
|
||||
'x-sign': sign,
|
||||
'x-device-id': self._app_device_id,
|
||||
'x-device-brand': 'vivo',
|
||||
'x-device-model': 'V2309A',
|
||||
'x-update-id': '0245861b-2ebf-5524-389d-f983830651ec'
|
||||
}
|
||||
|
||||
def _api_get(self, path, params=None, referer=''):
|
||||
self._ensure_ready()
|
||||
params = params or {}
|
||||
qs = urlencode(params, doseq=True)
|
||||
url = self.host + path + (('?' + qs) if qs else '')
|
||||
try:
|
||||
r = self.fetch(url, headers=self._headers(referer), timeout=12)
|
||||
text = getattr(r, 'text', '') or getattr(r, 'content', b'')
|
||||
if isinstance(text, bytes):
|
||||
text = text.decode('utf-8', errors='ignore')
|
||||
if not text:
|
||||
return {}
|
||||
return json.loads(text)
|
||||
except Exception as e:
|
||||
print('大马猴接口请求失败:', path, params, e)
|
||||
return {}
|
||||
|
||||
def _clean_text(self, s):
|
||||
s = str(s or '')
|
||||
s = re.sub(r'<[^>]+>', ' ', s)
|
||||
s = s.replace(' ', ' ')
|
||||
return re.sub(r'\s+', ' ', s).strip()
|
||||
|
||||
def _html2text(self, html):
|
||||
try:
|
||||
p = _HTMLTextExtractor()
|
||||
p.feed(str(html or ''))
|
||||
return self._clean_text(p.get_text())
|
||||
except Exception:
|
||||
return self._clean_text(html)
|
||||
|
||||
def _as_list(self, data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
for k in ('data', 'list', 'items', 'records', 'rows', 'vod_list'):
|
||||
v = data.get(k)
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
if isinstance(v, dict):
|
||||
vv = self._as_list(v)
|
||||
if vv:
|
||||
return vv
|
||||
return []
|
||||
|
||||
def _vod_item(self, item):
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
vid = item.get('vod_id') or item.get('id') or item.get('vodId')
|
||||
name = item.get('vod_name') or item.get('name') or item.get('title')
|
||||
if not vid or not name:
|
||||
return None
|
||||
area = item.get('vod_area', '')
|
||||
cls = item.get('vod_class', '')
|
||||
if isinstance(area, list):
|
||||
area = ','.join([str(x) for x in area if x])
|
||||
if isinstance(cls, list):
|
||||
cls = ','.join([str(x) for x in cls if x])
|
||||
return {
|
||||
'vod_id': str(vid),
|
||||
'vod_name': self._clean_text(name),
|
||||
'vod_pic': str(item.get('vod_pic') or item.get('pic') or item.get('cover') or ''),
|
||||
'vod_remarks': str(item.get('vod_remarks') or item.get('remarks') or item.get('vod_douban_score') or item.get('vod_year') or ''),
|
||||
'vod_year': str(item.get('vod_year') or ''),
|
||||
'type_name': self._clean_text(item.get('type_name') or cls or ''),
|
||||
'vod_area': self._clean_text(area),
|
||||
}
|
||||
|
||||
def _vod_list(self, data):
|
||||
arr = self._as_list(data)
|
||||
out = []
|
||||
seen = set()
|
||||
for item in arr:
|
||||
v = self._vod_item(item)
|
||||
if not v:
|
||||
continue
|
||||
if v['vod_id'] in seen:
|
||||
continue
|
||||
seen.add(v['vod_id'])
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
def _category_match(self, item, real_tid):
|
||||
if not isinstance(item, dict):
|
||||
return False
|
||||
real_tid = str(real_tid)
|
||||
item_tid = str(item.get('type_id') or item.get('typeId') or item.get('tid') or '')
|
||||
if item_tid == real_tid:
|
||||
return True
|
||||
name = str(item.get('type_name') or '')
|
||||
class_value = item.get('vod_class') or []
|
||||
if isinstance(class_value, list):
|
||||
cls = ','.join([str(x) for x in class_value if x])
|
||||
else:
|
||||
cls = str(class_value or '')
|
||||
text = name + ',' + cls
|
||||
if real_tid == '23':
|
||||
return ('电影' in text or '动作片' in text or '剧情片' in text or '喜剧片' in text) and '电视剧' not in text
|
||||
if real_tid == '22':
|
||||
return any(k in text for k in ('剧集', '电视剧', '国产剧', '连续剧', '韩剧', '陆剧', '欧美剧', '日剧'))
|
||||
if real_tid == '24':
|
||||
return '动漫' in text or '动画' in text or '国产动漫' in text or '日韩动漫' in text
|
||||
if real_tid == '25':
|
||||
return '综艺' in text or '真人秀' in text
|
||||
return True
|
||||
|
||||
def _filter_items_by_category(self, data, real_tid):
|
||||
arr = self._as_list(data)
|
||||
return [x for x in arr if self._category_match(x, real_tid)]
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {'class': self.classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
j = self._api_get('/api.php/web/filter/vod', {
|
||||
'type_id': '23',
|
||||
'page': '1',
|
||||
'sort': 'hits'
|
||||
}, self.host + '/type/23')
|
||||
return {'list': self._vod_list(j)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
self._ensure_ready()
|
||||
page = str(pg or '1')
|
||||
sort = 'hits'
|
||||
if isinstance(extend, dict):
|
||||
sort = extend.get('sort') or extend.get('by') or sort
|
||||
|
||||
tid_map = {'1': '23', '2': '22', '3': '24', '4': '25'}
|
||||
real_tid = tid_map.get(str(tid), str(tid))
|
||||
j = self._api_get('/api.php/web/filter/vod', {
|
||||
'type_id': real_tid,
|
||||
'page': page,
|
||||
'sort': sort
|
||||
}, self.host + '/type/' + real_tid)
|
||||
|
||||
filtered_items = self._filter_items_by_category(j, real_tid)
|
||||
|
||||
try:
|
||||
cur_page = int(page)
|
||||
except Exception:
|
||||
cur_page = 1
|
||||
if len(filtered_items) < 8:
|
||||
seen_ids = set(str(x.get('vod_id') or x.get('id') or '') for x in filtered_items if isinstance(x, dict))
|
||||
for extra_page in range(cur_page + 1, cur_page + 3):
|
||||
jj = self._api_get('/api.php/web/filter/vod', {
|
||||
'type_id': real_tid,
|
||||
'page': str(extra_page),
|
||||
'sort': sort
|
||||
}, self.host + '/type/' + real_tid)
|
||||
for item in self._filter_items_by_category(jj, real_tid):
|
||||
vid = str(item.get('vod_id') or item.get('id') or '')
|
||||
if vid and vid not in seen_ids:
|
||||
seen_ids.add(vid)
|
||||
filtered_items.append(item)
|
||||
if len(filtered_items) >= 24:
|
||||
break
|
||||
|
||||
videos = self._vod_list(filtered_items)
|
||||
pagecount = 1
|
||||
total = len(videos)
|
||||
limit = 24
|
||||
if isinstance(j, dict):
|
||||
pagecount = int(j.get('pageCount') or j.get('pagecount') or (cur_page + 1 if videos else cur_page))
|
||||
total = int(j.get('total') or total)
|
||||
limit = int(j.get('limit') or limit)
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': cur_page,
|
||||
'pagecount': pagecount,
|
||||
'limit': limit,
|
||||
'total': total
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
self._ensure_ready()
|
||||
wd = str(key or '').strip()
|
||||
page = str(pg or '1')
|
||||
if not wd:
|
||||
return {'list': [], 'page': int(page)}
|
||||
|
||||
paths = [
|
||||
('/api.php/web/search/vod', {'wd': wd, 'page': page}),
|
||||
('/api.php/web/vod/search', {'wd': wd, 'page': page}),
|
||||
('/api.php/web/search', {'wd': wd, 'page': page}),
|
||||
('/api.php/web/filter/vod', {'keyword': wd, 'page': page, 'sort': 'hits'}),
|
||||
]
|
||||
for path, params in paths:
|
||||
j = self._api_get(path, params, self.host + '/search?keyword=' + quote(wd))
|
||||
videos = self._vod_list(j)
|
||||
if videos:
|
||||
return {'list': videos, 'page': int(page)}
|
||||
return {'list': [], 'page': int(page)}
|
||||
|
||||
def _first_detail(self, ids):
|
||||
vid = str(ids[0] if isinstance(ids, list) else ids)
|
||||
j = self._api_get('/api.php/web/vod/get_detail', {'vod_id': vid}, self.host + '/play/' + vid)
|
||||
arr = self._as_list(j)
|
||||
return (arr[0] if arr else {}), j
|
||||
|
||||
def _aggregate_sources(self, vid):
|
||||
paths = [
|
||||
'/api.php/web/internal/search_aggregate',
|
||||
'/api.php/web/search_aggregate',
|
||||
]
|
||||
for path in paths:
|
||||
j = self._api_get(path, {'vod_id': str(vid)}, self.host + '/play/' + str(vid))
|
||||
arr = self._as_list(j)
|
||||
if arr:
|
||||
return arr
|
||||
return []
|
||||
|
||||
# ---------- 线路构建(与 JS 版一致) ----------
|
||||
def detailContent(self, ids):
|
||||
self._ensure_ready()
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
vid = str(ids[0])
|
||||
detail, raw = self._first_detail([vid])
|
||||
|
||||
if not detail:
|
||||
agg = self._aggregate_sources(vid)
|
||||
if agg:
|
||||
detail = agg[0]
|
||||
else:
|
||||
return {'list': []}
|
||||
|
||||
vodplayer = raw.get('vodplayer', []) if isinstance(raw, dict) else []
|
||||
|
||||
shows = []
|
||||
play_urls = []
|
||||
|
||||
# 1. 聚合接口的直链(优先,need_parse=0)
|
||||
agg_sources = self._aggregate_sources(vid)
|
||||
if agg_sources:
|
||||
agg_sources.sort(key=lambda s: (
|
||||
0 if re.search(r'\.(m3u8|mp4|flv)(\?|#|$|\s)', str(s.get('vod_play_url', '')), re.I) else 1,
|
||||
s.get('site_name', '')
|
||||
))
|
||||
for src in agg_sources[:4]:
|
||||
play_url = str(src.get('vod_play_url', '')).strip()
|
||||
if not play_url:
|
||||
continue
|
||||
need_parse = 0 # 聚合直链通常不需要解析
|
||||
site_name = src.get('site_name') or src.get('external_display_name') or '聚合线路'
|
||||
encoded = f"1${site_name}@{need_parse}@{play_url}"
|
||||
shows.append(site_name)
|
||||
play_urls.append(encoded)
|
||||
|
||||
# 2. 详情自带的线路
|
||||
pf = str(detail.get('vod_play_from', '') or '')
|
||||
pu = str(detail.get('vod_play_url', '') or '')
|
||||
if pf and pu:
|
||||
froms = pf.split('$$$')
|
||||
urls = pu.split('$$$')
|
||||
for show_code, urls_str in zip(froms, urls):
|
||||
need_parse = 1
|
||||
is_show = 0
|
||||
show_name = show_code
|
||||
|
||||
for player in vodplayer:
|
||||
if player.get('from') == show_code:
|
||||
is_show = 1
|
||||
need_parse = int(player.get('decode_status', 1))
|
||||
if player.get('show', '').lower() != show_code.lower():
|
||||
show_name = f"{player['show']}\u2005({show_code})"
|
||||
break
|
||||
|
||||
if not is_show:
|
||||
is_show = 1
|
||||
sample_url = urls_str.split('#')[0].split('$')[-1] if urls_str else ''
|
||||
if sample_url.startswith('http') and self.isVideoFormat(sample_url):
|
||||
need_parse = 0
|
||||
|
||||
if is_show:
|
||||
episodes = []
|
||||
for url_item in urls_str.split('#'):
|
||||
if '$' in url_item:
|
||||
ep, raw_url = url_item.split('$', 1)
|
||||
episodes.append(f"{ep}${show_code}@{need_parse}@{raw_url}")
|
||||
if episodes:
|
||||
play_urls.append('#'.join(episodes))
|
||||
shows.append(show_name)
|
||||
|
||||
# 3. 如果没有线路,尝试用聚合接口的未处理项
|
||||
if not shows and agg_sources:
|
||||
for src in agg_sources[:2]:
|
||||
play_url = str(src.get('vod_play_url', '')).strip()
|
||||
if play_url:
|
||||
need_parse = 0
|
||||
name = src.get('site_name', '线路')
|
||||
play_urls.append(f"1${name}@{need_parse}@{play_url}")
|
||||
shows.append(name)
|
||||
|
||||
area = detail.get('vod_area', '')
|
||||
cls = detail.get('vod_class', '')
|
||||
if isinstance(area, list):
|
||||
area = ','.join([str(x) for x in area if x])
|
||||
if isinstance(cls, list):
|
||||
cls = ','.join([str(x) for x in cls if x])
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': self._clean_text(detail.get('vod_name') or ''),
|
||||
'vod_pic': str(detail.get('vod_pic') or ''),
|
||||
'vod_remarks': str(detail.get('vod_remarks') or ''),
|
||||
'type_name': self._clean_text(detail.get('type_name') or cls or ''),
|
||||
'vod_year': str(detail.get('vod_year') or ''),
|
||||
'vod_area': self._clean_text(area),
|
||||
'vod_actor': self._clean_text(detail.get('vod_actor') or ''),
|
||||
'vod_director': self._clean_text(detail.get('vod_director') or ''),
|
||||
'vod_content': self._html2text(detail.get('vod_content') or ''),
|
||||
'vod_play_from': '$$$'.join(shows),
|
||||
'vod_play_url': '$$$'.join(play_urls),
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
# ---------- 播放解析(使用 app 解码接口,实现直接播放) ----------
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
self._ensure_ready()
|
||||
url = str(id or '').strip()
|
||||
if not url:
|
||||
return {'parse': 0, 'url': ''}
|
||||
|
||||
# 处理 @ 标记格式(集数$线路@need_parse@真实地址)
|
||||
if '@' in url and not url.startswith('http'):
|
||||
try:
|
||||
parts = url.split('@', 2)
|
||||
if len(parts) == 3:
|
||||
play_from, need_parse, raw_url = parts
|
||||
if need_parse == '0':
|
||||
if self.isVideoFormat(raw_url) or raw_url.startswith('http'):
|
||||
return {
|
||||
'parse': 0, 'jx': 0,
|
||||
'url': raw_url,
|
||||
'header': self._app_headers()
|
||||
}
|
||||
else:
|
||||
return {'parse': 1, 'jx': 1, 'url': raw_url}
|
||||
# need_parse == '1',尝试解码
|
||||
decoded = self._try_decode(raw_url, play_from)
|
||||
if decoded:
|
||||
return {
|
||||
'parse': 0, 'jx': 0,
|
||||
'url': decoded,
|
||||
'header': self._app_headers()
|
||||
}
|
||||
# 解码失败,若原地址可用则直接播,否则交壳解析
|
||||
if self.isVideoFormat(raw_url) or raw_url.startswith('http'):
|
||||
return {'parse': 0, 'jx': 0, 'url': raw_url,
|
||||
'header': self._app_headers()}
|
||||
else:
|
||||
return {'parse': 1, 'jx': 1, 'url': raw_url}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 旧格式直链直接播放
|
||||
if url.startswith('http') and self.isVideoFormat(url):
|
||||
return {
|
||||
'parse': 0, 'jx': 0,
|
||||
'url': url,
|
||||
'header': self._app_headers()
|
||||
}
|
||||
|
||||
# 尝试旧版解码接口(web 方式)
|
||||
decoded = self._try_decode(url, None)
|
||||
if decoded:
|
||||
return {
|
||||
'parse': 0, 'jx': 0,
|
||||
'url': decoded,
|
||||
'header': self._app_headers()
|
||||
}
|
||||
|
||||
# 腾讯/优酷等大站交给壳
|
||||
if re.search(r'(v\.qq\.com|youku\.com|iqiyi\.com|mgtv\.com|bilibili\.com)', url, re.I):
|
||||
return {'parse': 1, 'jx': 1, 'url': url}
|
||||
|
||||
return {'parse': 1, 'jx': 1, 'url': url}
|
||||
|
||||
def _try_decode(self, raw_url, play_from=None):
|
||||
"""优先使用 app 解码接口,失败再尝试 web 接口"""
|
||||
# ---- 1. app 解码接口(带签名)----
|
||||
if play_from:
|
||||
try:
|
||||
app_headers = self._app_headers()
|
||||
app_url = f"{self.host}/api.php/app/decode/url/?url={quote(raw_url)}&vodFrom={play_from}"
|
||||
r = self.fetch(app_url, headers=app_headers, timeout=10)
|
||||
data = json.loads(r.text) if r.text else {}
|
||||
link = data.get('data', '')
|
||||
if isinstance(link, str) and link.startswith('http'):
|
||||
return link
|
||||
except Exception as e:
|
||||
print(f'App解码失败: {e}')
|
||||
|
||||
# ---- 2. web 解码接口(原有多参数尝试)----
|
||||
decode_tries = [
|
||||
{'url': raw_url},
|
||||
{'play_url': raw_url},
|
||||
{'vod_url': raw_url},
|
||||
]
|
||||
for params in decode_tries:
|
||||
j = self._api_get('/api.php/web/decode/url', params, self.host + '/play')
|
||||
link = self._extract_url(j)
|
||||
if link:
|
||||
return link
|
||||
return None
|
||||
|
||||
def _extract_url(self, resp_data):
|
||||
"""从解码响应中提取直链"""
|
||||
if isinstance(resp_data, str):
|
||||
return resp_data if resp_data.startswith('http') else None
|
||||
if isinstance(resp_data, dict):
|
||||
data = resp_data.get('data')
|
||||
if isinstance(data, str):
|
||||
return data if data.startswith('http') else None
|
||||
if isinstance(data, dict):
|
||||
url = data.get('url') or data.get('play_url') or data.get('playUrl') or ''
|
||||
return url if url.startswith('http') else None
|
||||
url = resp_data.get('url') or resp_data.get('play_url') or ''
|
||||
return url if url.startswith('http') else None
|
||||
return None
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @PyramidStore AutoGen
|
||||
import re
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import hashlib
|
||||
import requests as _req
|
||||
from base.spider import Spider
|
||||
from urllib.parse import quote, urlencode, unquote
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
LANDING_URL = 'https://dyrs.net'
|
||||
HOSTS_API = 'https://dyrshd.net/api/videox/least'
|
||||
|
||||
UA_LIST = [
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36',
|
||||
'Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0',
|
||||
]
|
||||
|
||||
CATEGORIES = [
|
||||
('dianying', '电影'),
|
||||
('dianshiju', '电视剧'),
|
||||
('zongyi', '综艺'),
|
||||
('dongman', '动漫'),
|
||||
('duanju', '短剧'),
|
||||
]
|
||||
|
||||
CLASS_LIST = ['剧情', '喜剧', '动作', '爱情', '惊悚', '犯罪', '院线', '悬疑', '恐怖', '冒险', '奇幻', '科幻', '家庭', '战争', '古装', '历史', '传记', '武侠', '动画', '音乐']
|
||||
AREA_LIST = ['美国', '内地', '中国香港', '日本', '英国', '法国', '韩国', '加拿大', '德国', '中国台湾', '印度', '意大利', '其它地区', '西班牙', '澳大利亚', '泰国', '俄罗斯', '比利时', '丹麦', '墨西哥']
|
||||
YEAR_LIST = ['2026', '2025', '2024', '2023', '2022', '2021', '2020', '2019', '2018', '2017', '2016', '2015', '2014', '2013', '2012', '2011', '2010']
|
||||
|
||||
def init(self, extend=""):
|
||||
self._host = getattr(self, '_host', None) or self._resolve_host()
|
||||
self._vod_name = ''
|
||||
self._last_fetch = 0
|
||||
self._sion_id = ''
|
||||
self._session = _req.Session()
|
||||
self._ua = random.choice(self.UA_LIST)
|
||||
self._session.headers.update({
|
||||
'User-Agent': self._ua,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
})
|
||||
self._session.verify = False
|
||||
# Force fresh session to avoid stale sion_id causing 429
|
||||
self._ensure_session()
|
||||
|
||||
def getName(self):
|
||||
return '电影人生'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def _resolve_host(self):
|
||||
try:
|
||||
r = _req.get(self.HOSTS_API, headers={'User-Agent': random.choice(self.UA_LIST)}, timeout=10, verify=False)
|
||||
data = r.json()
|
||||
urls = data.get('urls', [])
|
||||
for url in urls[:10]:
|
||||
try:
|
||||
host = url.rstrip('/')
|
||||
r2 = _req.get(host + '/dianying.html?page=1', headers={'User-Agent': random.choice(self.UA_LIST)}, timeout=5, verify=False)
|
||||
if r2.status_code == 200 and len(r2.text) > 100000 and re.search(r'data-url="[^"]+"', r2.text):
|
||||
return host
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
return 'https://dyrs3.vip'
|
||||
|
||||
def _switch_host(self):
|
||||
try:
|
||||
r = _req.get(self.HOSTS_API, headers={'User-Agent': random.choice(self.UA_LIST)}, timeout=10, verify=False)
|
||||
data = r.json()
|
||||
urls = data.get('urls', [])
|
||||
for url in urls[:10]:
|
||||
try:
|
||||
host = url.rstrip('/')
|
||||
if host == self._host:
|
||||
continue
|
||||
r2 = _req.get(host + '/', headers={'User-Agent': random.choice(self.UA_LIST)}, timeout=5, verify=False)
|
||||
if r2.status_code == 200:
|
||||
self._host = host
|
||||
self._sion_id = ''
|
||||
self._session.cookies.clear()
|
||||
self._ensure_session()
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _ensure_session(self):
|
||||
if not self._sion_id:
|
||||
try:
|
||||
r = self._session.get(f'{self._host}/', timeout=10)
|
||||
self._sion_id = self._session.cookies.get('sion_id', '')
|
||||
except:
|
||||
pass
|
||||
|
||||
def _solve_pow(self, html):
|
||||
hash_m = re.search(r"var\s+hash\s*=\s*'([^']+)'", html)
|
||||
target_m = re.search(r"var\s+target\s*=\s*'([^']+)'", html)
|
||||
if not hash_m or not target_m:
|
||||
return None
|
||||
h, target = hash_m.group(1), target_m.group(1)
|
||||
for i in range(10000000):
|
||||
if hashlib.sha1((h + str(i)).encode()).hexdigest() == target:
|
||||
return i
|
||||
return None
|
||||
|
||||
def _is_pow_page(self, text):
|
||||
return 'passChallenge' in text and 'var hash' in text and 'var target' in text
|
||||
|
||||
def _try_pow(self, r, url):
|
||||
if self._is_pow_page(r.text):
|
||||
attack_key = self._solve_pow(r.text)
|
||||
if attack_key is not None:
|
||||
sep = '&' if '?' in url else '?'
|
||||
pow_url = f'{url}{sep}attack_key={attack_key}'
|
||||
time.sleep(1)
|
||||
r2 = self._session.get(pow_url, timeout=15)
|
||||
r2.encoding = 'utf-8'
|
||||
if r2.status_code == 200 and len(r2.text) > 1000 and not self._is_pow_page(r2.text):
|
||||
return r2.text
|
||||
return None
|
||||
|
||||
def _fetch(self, url, retries=3):
|
||||
self._ensure_session()
|
||||
if self._sion_id and 'sion_id=' not in url:
|
||||
sep = '&' if '?' in url else '?'
|
||||
url = f'{url}{sep}sion_id={self._sion_id}'
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
elapsed = time.time() - self._last_fetch
|
||||
if elapsed < 2.0:
|
||||
time.sleep(2.0 - elapsed)
|
||||
r = self._session.get(url, timeout=15)
|
||||
self._last_fetch = time.time()
|
||||
r.encoding = 'utf-8'
|
||||
if r.status_code == 200 and len(r.text) > 1000:
|
||||
pow_result = self._try_pow(r, url)
|
||||
if pow_result:
|
||||
return pow_result
|
||||
if not self._is_pow_page(r.text):
|
||||
return r.text
|
||||
if r.status_code in (429, 200) and self._is_pow_page(r.text):
|
||||
pow_result = self._try_pow(r, url)
|
||||
if pow_result:
|
||||
return pow_result
|
||||
if r.status_code == 429:
|
||||
time.sleep(3 * (attempt + 1))
|
||||
self._sion_id = ''
|
||||
self._session.cookies.clear()
|
||||
self._session.headers['User-Agent'] = random.choice(self.UA_LIST)
|
||||
self._ensure_session()
|
||||
if self._sion_id:
|
||||
url = re.sub(r'sion_id=[^&]*', f'sion_id={self._sion_id}', url) if 'sion_id=' in url else f'{url}&sion_id={self._sion_id}'
|
||||
continue
|
||||
except (_req.exceptions.ConnectionError, _req.exceptions.Timeout):
|
||||
if attempt < retries - 1:
|
||||
time.sleep(2)
|
||||
self._switch_host()
|
||||
if self._sion_id:
|
||||
url = re.sub(r'sion_id=[^&]*', f'sion_id={self._sion_id}', url) if 'sion_id=' in url else f'{url}&sion_id={self._sion_id}'
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
if attempt < retries - 1:
|
||||
time.sleep(3)
|
||||
return None
|
||||
|
||||
def _parse_list(self, html):
|
||||
videos = []
|
||||
if not html:
|
||||
return videos
|
||||
urls = re.findall(r'data-url="([^"]+)"', html)
|
||||
titles = re.findall(r'<a\s[^>]*?title="([^"]*)"[^>]*?data-url=', html)
|
||||
pics = re.findall(r'data-src="([^"]+)"', html)
|
||||
for i, data_url in enumerate(urls):
|
||||
title = titles[i] if i < len(titles) else ''
|
||||
pic = pics[i] if i < len(pics) else ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = f'{self._host}{pic}'
|
||||
remark = ''
|
||||
year = ''
|
||||
idx = html.find(f'data-url="{data_url}"')
|
||||
if idx > 0:
|
||||
after = html[idx:idx+1500]
|
||||
# rounded shadow-sm = remark (清晰度/集数)
|
||||
rem_m = re.search(r'rounded shadow-sm[^>]*>\s*([^<]+)', after)
|
||||
if rem_m:
|
||||
remark = rem_m.group(1).strip()
|
||||
# <span>年份</span> = year
|
||||
year_m = re.search(r'<span>\s*(\d{4})\s*</span>', after)
|
||||
if year_m:
|
||||
year = year_m.group(1)
|
||||
videos.append({
|
||||
'vod_id': data_url if data_url.startswith('/') else f'/{data_url}',
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remark,
|
||||
'vod_year': year,
|
||||
})
|
||||
return videos
|
||||
|
||||
def _parse_episodes(self, html, detail_base):
|
||||
sources = {}
|
||||
from urllib.parse import unquote, quote
|
||||
|
||||
# Find source selector links: href="...origin=xxx..." (no p= param, not api/m3u8, not download)
|
||||
source_map = {}
|
||||
for m in re.finditer(r'href="([^"]*origin=[^"]*)"', html):
|
||||
href = m.group(1).replace('&', '&')
|
||||
if 'p=' in href or 'api/m3u8' in href or 'download' in href or 'dianying.html' in href:
|
||||
continue
|
||||
origin_m = re.search(r'origin=([^&"]+)', href)
|
||||
if origin_m:
|
||||
origin_val = unquote(origin_m.group(1))
|
||||
if origin_val and origin_val not in source_map:
|
||||
source_map[origin_val] = href
|
||||
|
||||
# Find episode links: href="...origin=xxx&p=N..."
|
||||
ep_links = re.findall(r'href="([^"]*\?origin=[^"&]*&(?:amp;)?p=\d+[^"]*)"', html)
|
||||
ep_links = list(dict.fromkeys(ep_links))
|
||||
|
||||
# Group episodes by origin from current page
|
||||
for link in ep_links:
|
||||
link = link.replace('&', '&')
|
||||
origin_m = re.search(r'origin=([^&]+)', link)
|
||||
if origin_m:
|
||||
origin = unquote(origin_m.group(1))
|
||||
if origin not in sources:
|
||||
sources[origin] = []
|
||||
sources[origin].append(link)
|
||||
|
||||
# For sources not in current page, visit their selector page
|
||||
for origin_val in source_map:
|
||||
if origin_val not in sources:
|
||||
source_url = f'{self._host}{detail_base}?origin={quote(origin_val)}'
|
||||
try:
|
||||
src_html = self._fetch(source_url, retries=2)
|
||||
if src_html:
|
||||
src_eps = re.findall(r'href="([^"]*\?origin=[^"&]*&(?:amp;)?p=\d+[^"]*)"', src_html)
|
||||
src_eps = list(dict.fromkeys(src_eps))
|
||||
if src_eps:
|
||||
sources[origin_val] = [e.replace('&', '&') for e in src_eps]
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
return sources
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'list': []}
|
||||
try:
|
||||
html = self._fetch(f'{self._host}/')
|
||||
if html:
|
||||
result['class'] = [{'type_id': cid, 'type_name': cn} for cid, cn in self.CATEGORIES]
|
||||
result['list'] = self._parse_list(html)
|
||||
if filter:
|
||||
result['filters'] = {
|
||||
cid: [
|
||||
{'key': 'class', 'name': '类型', 'value': [{'n': '全部', 'v': ''}] + [{'n': c, 'v': c} for c in self.CLASS_LIST]},
|
||||
{'key': 'area', 'name': '地区', 'value': [{'n': '全部', 'v': ''}] + [{'n': a, 'v': a} for a in self.AREA_LIST]},
|
||||
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}] + [{'n': y, 'v': y} for y in self.YEAR_LIST]},
|
||||
]
|
||||
for cid, cn in self.CATEGORIES
|
||||
}
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
html = self._fetch(f'{self._host}/')
|
||||
if html:
|
||||
return {'list': self._parse_list(html)}
|
||||
except:
|
||||
pass
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': int(pg), 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
try:
|
||||
pg = int(pg)
|
||||
params = {'page': pg}
|
||||
if extend:
|
||||
for key in ('area', 'class', 'year'):
|
||||
if extend.get(key):
|
||||
params[key] = extend[key]
|
||||
url = f'{self._host}/{tid}.html?{urlencode(params)}'
|
||||
html = self._fetch(url)
|
||||
if html:
|
||||
items = self._parse_list(html)
|
||||
result['list'] = items
|
||||
result['limit'] = max(len(items), 1)
|
||||
result['total'] = len(items)
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
try:
|
||||
url = ids[0]
|
||||
if not url.startswith('http'):
|
||||
url = f'{self._host}{url}'
|
||||
html = self._fetch(url)
|
||||
if not html:
|
||||
return result
|
||||
|
||||
vod_name = ''
|
||||
m = re.search(r'<title>([^<]+)', html)
|
||||
if m:
|
||||
vod_name = m.group(1).split('-')[0].strip()
|
||||
vod_name = re.sub(r'[\u300a\u300b\u3008\u3009\u300c\u300d\u300e\u300f\uff08\uff09\(\)\[\]\{\}]', '', vod_name)
|
||||
vod_name = re.sub(r'(在线观看|在线播放|免费播放|免费观看|高清播放|高清在线|完整版|全集|电视剧|电影|免费|高清|播放|观看|全集免费|在线|影院)$', '', vod_name)
|
||||
vod_name = re.sub(r'[-\s]+$', '', vod_name).strip()
|
||||
|
||||
vod_pic = ''
|
||||
m = re.search(r'imgurl\s*[=:]\s*[\'"]([^\'"]+)[\'"]', html)
|
||||
if m:
|
||||
vod_pic = m.group(1)
|
||||
if not vod_pic.startswith('http'):
|
||||
vod_pic = f'{self._host}{vod_pic}'
|
||||
|
||||
vod_content = ''
|
||||
m = re.search(r'<meta[^>]*name="description"[^>]*content="([^"]*)"', html)
|
||||
if m:
|
||||
vod_content = m.group(1).strip()
|
||||
|
||||
vod_year = ''
|
||||
m = re.search(r'year\s*[=:]\s*[\'"](\d{4})[\'"]', html)
|
||||
if m:
|
||||
vod_year = m.group(1)
|
||||
|
||||
vod_actor = ''
|
||||
m = re.search(r'"actor"\s*:\s*\[(.*?)\]', html)
|
||||
if m:
|
||||
actors = re.findall(r'"name"\s*:\s*"([^"]+)"', m.group(1))
|
||||
vod_actor = ','.join(actors[:10])
|
||||
|
||||
vod_director = ''
|
||||
m = re.search(r'"director"\s*:\s*\{[^}]*"name"\s*:\s*"([^"]*)"', html)
|
||||
if m:
|
||||
vod_director = m.group(1)
|
||||
|
||||
vod = {
|
||||
'vod_id': ids[0],
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_content': vod_content,
|
||||
'vod_year': vod_year,
|
||||
'vod_actor': vod_actor,
|
||||
'vod_director': vod_director,
|
||||
'vod_play_from': '',
|
||||
'vod_play_url': '',
|
||||
}
|
||||
|
||||
play_sources = {}
|
||||
|
||||
# Method 1: Parse episode links from detail page
|
||||
detail_base = re.search(r'(\/[^?]+\.html)', ids[0])
|
||||
if detail_base:
|
||||
episodes = self._parse_episodes(html, detail_base.group(1))
|
||||
if episodes:
|
||||
play_sources = episodes
|
||||
|
||||
# Method 2: Try xg_video_player_doc.aa (single episode)
|
||||
if not play_sources:
|
||||
aa_m = re.search(r'xg_video_player_doc\s*=\s*\{[^}]*aa:\s*JSON\.parse\([\'"](\{[^}]+\})[\'"]\)', html, re.DOTALL)
|
||||
if aa_m:
|
||||
raw = aa_m.group(1).replace('\\u0022', '"').replace('\\u0026', '&')
|
||||
raw = re.sub(r'\\(.)', r'\1', raw)
|
||||
try:
|
||||
aa = json.loads(raw)
|
||||
origin = aa.get('origin', '默认')
|
||||
play_url = aa.get('url', '')
|
||||
if play_url:
|
||||
if not play_url.startswith('http'):
|
||||
play_url = f'{self._host}{play_url}'
|
||||
play_sources[origin] = [play_url]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Method 3: Try videoid fallback
|
||||
if not play_sources:
|
||||
m = re.search(r'videoid\s*[=:]\s*[\'"]([a-f0-9]+)[\'"]', html)
|
||||
if m:
|
||||
vid = m.group(1)
|
||||
play_sources['超级线路'] = [f'{self._host}/api/m3u8?origin=%E8%B6%85%E7%BA%A7%E7%BA%BF%E8%B7%AF&url={vid}']
|
||||
|
||||
if play_sources:
|
||||
play_from = []
|
||||
play_urls = []
|
||||
for src_name, src_urls in play_sources.items():
|
||||
play_from.append(src_name)
|
||||
eps = []
|
||||
for i, u in enumerate(src_urls, 1):
|
||||
full_url = u if u.startswith('http') else f'{self._host}{u}'
|
||||
eps.append(f'第{i}集${full_url}')
|
||||
play_urls.append('#'.join(eps))
|
||||
vod['vod_play_from'] = '$$$'.join(play_from)
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
|
||||
self._vod_name = vod_name
|
||||
result['list'] = [vod]
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
result = {'list': [], 'page': int(pg), 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
try:
|
||||
html = self._fetch(f'{self._host}/search.html?keyword={quote(key)}')
|
||||
if html:
|
||||
result['list'] = self._parse_list(html)
|
||||
if result['list']:
|
||||
result['pagecount'] = 9999
|
||||
result['total'] = 999999
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
url = id
|
||||
if not url.startswith('http'):
|
||||
url = f'{self._host}{url}'
|
||||
# For episode URLs with origin, fetch the page to get m3u8
|
||||
if 'origin=' in url and 'api/m3u8' not in url:
|
||||
html = self._fetch(url, retries=3)
|
||||
if html:
|
||||
m3u8_url = self._extract_m3u8(html)
|
||||
if m3u8_url:
|
||||
danmaku = self._build_danmaku(url)
|
||||
return {'parse': 0, 'url': m3u8_url, 'header': {'Referer': f'{self._host}/'}, 'danmaku': danmaku}
|
||||
# Retry with fresh session
|
||||
self._sion_id = ''
|
||||
self._session.cookies.clear()
|
||||
self._ensure_session()
|
||||
if self._sion_id:
|
||||
url = re.sub(r'sion_id=[^&]*', f'sion_id={self._sion_id}', url) if 'sion_id=' in url else f'{url}&sion_id={self._sion_id}'
|
||||
html = self._fetch(url, retries=2)
|
||||
if html:
|
||||
m3u8_url = self._extract_m3u8(html)
|
||||
if m3u8_url:
|
||||
danmaku = self._build_danmaku(url)
|
||||
return {'parse': 0, 'url': m3u8_url, 'header': {'Referer': f'{self._host}/'}, 'danmaku': danmaku}
|
||||
# Direct m3u8 URL
|
||||
html = self._fetch(url, retries=2)
|
||||
if html and ('#EXTM3U' in html or 'mpegurl' in html.lower()):
|
||||
danmaku = self._build_danmaku(url)
|
||||
return {'parse': 0, 'url': url, 'header': {'Referer': f'{self._host}/'}, 'danmaku': danmaku}
|
||||
except:
|
||||
pass
|
||||
return {'parse': 1, 'url': id, 'header': {'Referer': f'{self._host}/'}, 'danmaku': ''}
|
||||
|
||||
def _extract_m3u8(self, html):
|
||||
aa_m = re.search(r'xg_video_player_doc\s*=\s*\{[^}]*aa:\s*JSON\.parse\([\'"](\{[^}]+\})[\'"]\)', html, re.DOTALL)
|
||||
if aa_m:
|
||||
raw = aa_m.group(1).replace('\\u0022', '"').replace('\\u0026', '&')
|
||||
raw = re.sub(r'\\(.)', r'\1', raw)
|
||||
try:
|
||||
aa = json.loads(raw)
|
||||
m3u8_url = aa.get('url', '')
|
||||
if m3u8_url:
|
||||
if not m3u8_url.startswith('http'):
|
||||
m3u8_url = f'{self._host}{m3u8_url}'
|
||||
return m3u8_url
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _build_danmaku(self, url):
|
||||
if not self._vod_name:
|
||||
return ''
|
||||
name = self._vod_name
|
||||
name = re.sub(r'[\u300a\u300b\u3008\u3009\u300c\u300d\u300e\u300f\uff08\uff09\(\)\[\]\{\}]', '', name)
|
||||
name = re.sub(r'(在线观看|在线播放|免费播放|免费观看|高清播放|高清在线|完整版|全集|电视剧|电影|免费|高清|完整版|播放|观看|全集免费|在线|影院)$', '', name)
|
||||
name = re.sub(r'[-\s]+$', '', name).strip()
|
||||
if not name:
|
||||
return ''
|
||||
ep_name = ''
|
||||
if 'p=' in url:
|
||||
p_m = re.search(r'p=(\d+)', url)
|
||||
if p_m:
|
||||
ep_name = str(int(p_m.group(1)) + 1)
|
||||
return f'http://127.0.0.1:9978/proxy?do=appdanmu&vodName={quote(name)}&vodIndex={ep_name}'
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, {}, '']
|
||||
@@ -0,0 +1,308 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
host = 'https://www.stonelodgeacademy.com'
|
||||
|
||||
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',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
}
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '达达兔影院'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'list': []}
|
||||
try:
|
||||
html = self._fetch(self.host + '/')
|
||||
if not html:
|
||||
return result
|
||||
for m in re.finditer(r'<nav[^>]*class="[^"]*nav-menu[^"]*"[^>]*>(.*?)</nav>', html, re.DOTALL):
|
||||
for a in re.finditer(r'<a[^>]*href="/(\w+)/?"[^>]*class="[^"]*nav-item[^"]*"[^>]*>([^<]*)</a>', m.group(1)):
|
||||
result['class'].append({'type_id': a.group(1), 'type_name': a.group(2).strip()})
|
||||
result['list'] = self._parse_list(html)
|
||||
except Exception as e:
|
||||
print(f'homeContent error: {e}')
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
html = self._fetch(self.host + '/')
|
||||
if html:
|
||||
return {'list': self._parse_list(html)}
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
try:
|
||||
pg = int(pg)
|
||||
url = f'{self.host}/{tid}/'
|
||||
if pg > 1:
|
||||
url += f'?page={pg}'
|
||||
html = self._fetch(url)
|
||||
if html:
|
||||
result['list'] = self._parse_list(html)
|
||||
result['page'] = str(pg)
|
||||
result['pagecount'] = 9999
|
||||
result['total'] = 999999
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
try:
|
||||
url = ids[0]
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
html = self._fetch(url)
|
||||
if not html:
|
||||
return result
|
||||
vod = {
|
||||
'vod_id': ids[0],
|
||||
'vod_name': '',
|
||||
'vod_pic': '',
|
||||
'vod_content': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_director': '',
|
||||
'vod_actor': '',
|
||||
'vod_play_from': '默认',
|
||||
'vod_play_url': '',
|
||||
}
|
||||
m = re.search(r'<h1[^>]*>([^<]*)</h1>', html)
|
||||
if m:
|
||||
vod['vod_name'] = m.group(1).strip()
|
||||
m = re.search(r'<div[^>]*class="[^"]*detail-poster[^"]*"[^>]*>.*?<img[^>]*src="([^"]*)"', html, re.DOTALL)
|
||||
if m:
|
||||
vod['vod_pic'] = m.group(1)
|
||||
else:
|
||||
m = re.search(r'<meta\s+property="og:image"\s+content="([^"]*)"', html)
|
||||
if m:
|
||||
vod['vod_pic'] = m.group(1)
|
||||
m = re.search(r'<p[^>]*class="[^"]*detail-desc[^"]*"[^>]*>([^<]*)</p>', html)
|
||||
if m:
|
||||
vod['vod_content'] = m.group(1).strip()
|
||||
m = re.search(r'导演[::]\s*([^<\n]*)', html)
|
||||
if m:
|
||||
vod['vod_director'] = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
||||
m = re.search(r'主演[::]\s*([^<\n]*)', html)
|
||||
if m:
|
||||
vod['vod_actor'] = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
||||
m = re.search(r'年份[::]\s*<a[^>]*>([^<]*)</a>', html)
|
||||
if m:
|
||||
vod['vod_year'] = m.group(1).strip()
|
||||
m = re.search(r'地区[::]\s*<a[^>]*>([^<]*)</a>', html)
|
||||
if m:
|
||||
vod['vod_area'] = m.group(1).strip()
|
||||
play_from = []
|
||||
play_url = []
|
||||
for src_m in re.finditer(r'<div[^>]*class="[^"]*play-source[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL):
|
||||
block = src_m.group(1)
|
||||
name_m = re.search(r'<div[^>]*class="[^"]*play-source-name[^"]*"[^>]*>([^<]*)</div>', block)
|
||||
src_name = name_m.group(1).strip() if name_m else '默认'
|
||||
eps = []
|
||||
for ep_m in re.finditer(r'<a[^>]*href="(/[^"]*)"[^>]*class="[^"]*play-item[^"]*"[^>]*>([^<]*)</a>', block):
|
||||
eps.append(f'{ep_m.group(2).strip()}${ep_m.group(1)}')
|
||||
if eps:
|
||||
play_from.append(src_name)
|
||||
play_url.append('#'.join(eps))
|
||||
if play_from:
|
||||
vod['vod_play_from'] = '$$$'.join(play_from)
|
||||
vod['vod_play_url'] = '$$$'.join(play_url)
|
||||
ep_map = {}
|
||||
for src_idx, url_str in enumerate(play_url):
|
||||
for ep in url_str.split('#'):
|
||||
if '$' in ep:
|
||||
ep_name, ep_url = ep.split('$', 1)
|
||||
if ep_url:
|
||||
ep_map[ep_url] = ep_name
|
||||
self._vod_episode_map = ep_map
|
||||
self._vod_name = vod['vod_name']
|
||||
result['list'] = [vod]
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
try:
|
||||
html = self._fetch(f'{self.host}/search/?keyword={quote(key)}')
|
||||
if html:
|
||||
result['list'] = self._parse_list(html)
|
||||
result['pagecount'] = 9999
|
||||
result['total'] = 999999
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
url = id
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
html = self._fetch(url)
|
||||
if html:
|
||||
m = re.search(r'<meta\s+property="og:video"\s+content="([^"]*)"', html)
|
||||
if m:
|
||||
vod_name = getattr(self, '_vod_name', '')
|
||||
ep_map = getattr(self, '_vod_episode_map', {})
|
||||
ep_name = ep_map.get(id, '')
|
||||
danmaku = self._build_danmaku_url(vod_name, ep_name)
|
||||
return {'parse': 0, 'url': m.group(1), 'header': self.headers, 'danmaku': danmaku}
|
||||
m = re.search(r'<source\s+src="([^"]*)"', html)
|
||||
if m:
|
||||
vod_name = getattr(self, '_vod_name', '')
|
||||
ep_map = getattr(self, '_vod_episode_map', {})
|
||||
ep_name = ep_map.get(id, '')
|
||||
danmaku = self._build_danmaku_url(vod_name, ep_name)
|
||||
return {'parse': 0, 'url': m.group(1), 'header': self.headers, 'danmaku': danmaku}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
return {'parse': 1, 'url': id, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, {}, ""]
|
||||
|
||||
def _parse_list(self, html):
|
||||
videos = []
|
||||
if not html:
|
||||
return videos
|
||||
for m in re.finditer(
|
||||
r'<div[^>]*class="[^"]*\bvideo-card\b[^"]*"[^>]*>'
|
||||
r'.*?<a[^>]*href="(/[^"]*)"[^>]*class="[^"]*\bvideo-thumb\b[^"]*"[^>]*>',
|
||||
html, re.DOTALL
|
||||
):
|
||||
block = html[m.start():m.start() + 600]
|
||||
href = m.group(1)
|
||||
pic = ''
|
||||
pic_m = re.search(r'data-src="([^"]*)"', block)
|
||||
if pic_m:
|
||||
pic = pic_m.group(1)
|
||||
else:
|
||||
pic_m = re.search(r'src="([^"]*)"', block)
|
||||
if pic_m:
|
||||
pic = pic_m.group(1)
|
||||
name = ''
|
||||
alt_m = re.search(r'alt="([^"]*)"', block)
|
||||
if alt_m:
|
||||
name = alt_m.group(1)
|
||||
if not name:
|
||||
title_m = re.search(r'<h[23][^>]*class="[^"]*\bvideo-title\b[^"]*"[^>]*>([^<]*)</h[23]>', block)
|
||||
if title_m:
|
||||
name = title_m.group(1).strip()
|
||||
remark = ''
|
||||
ep = re.search(r'<span[^>]*class="[^"]*\bvideo-episode\b[^"]*"[^>]*>([^<]*)</span>', block)
|
||||
if ep:
|
||||
remark = ep.group(1).strip()
|
||||
else:
|
||||
tag = re.search(r'<span[^>]*class="[^"]*\bvideo-tag\b[^"]*"[^>]*>([^<]*)</span>', block)
|
||||
if tag:
|
||||
remark = tag.group(1).strip()
|
||||
year = ''
|
||||
meta_m = re.search(r'<p[^>]*class="[^"]*\bvideo-meta\b[^"]*"[^>]*>([^<]*)</p>', block)
|
||||
if meta_m:
|
||||
ym = re.search(r'(\d{4})', meta_m.group(1))
|
||||
if ym:
|
||||
year = ym.group(1)
|
||||
videos.append({
|
||||
'vod_id': href,
|
||||
'vod_name': name,
|
||||
'vod_pic': pic if pic.startswith('http') else '',
|
||||
'vod_remarks': remark,
|
||||
'vod_year': year,
|
||||
})
|
||||
return videos
|
||||
|
||||
def _fetch(self, url):
|
||||
import time
|
||||
self._rate_limit()
|
||||
for i in range(3):
|
||||
try:
|
||||
r = self.fetch(url, headers=self.headers, timeout=15)
|
||||
if r.status_code == 200 and len(r.text) > 100:
|
||||
return r.text
|
||||
time.sleep(2)
|
||||
except:
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
def _rate_limit(self):
|
||||
import time
|
||||
now = time.time()
|
||||
gap = now - getattr(self, '_last_request_time', 0)
|
||||
if gap < 1.5:
|
||||
time.sleep(1.5 - gap)
|
||||
self._last_request_time = time.time()
|
||||
|
||||
def _build_danmaku_url(self, vod_name='', ep_name=''):
|
||||
idx = self._parse_episode_index(ep_name)
|
||||
params = []
|
||||
if vod_name:
|
||||
params.append(f'vodName={quote(vod_name)}')
|
||||
params.append(f'vodIndex={idx}')
|
||||
q = '&'.join(params)
|
||||
return f'http://127.0.0.1:9978/proxy?do=appdanmu&{q}'
|
||||
|
||||
@staticmethod
|
||||
def _parse_episode_index(name):
|
||||
if not name:
|
||||
return 0
|
||||
s = str(name).strip()
|
||||
m = re.search(r'第\s*((?:\d+)|(?:[一二三四五六七八九十百零]+))\s*[集話话章部期]', s)
|
||||
if m:
|
||||
ns = m.group(1)
|
||||
if ns.isdigit():
|
||||
return int(ns)
|
||||
return Spider._cn_num(ns)
|
||||
m = re.search(r'(?:EP|ep|第)\s*(\d+)', s)
|
||||
if m:
|
||||
n = int(m.group(1))
|
||||
return n if n > 0 else 0
|
||||
m = re.search(r'(\d+)', s)
|
||||
if m:
|
||||
n = int(m.group(1))
|
||||
return n if n > 0 else 0
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _cn_num(s):
|
||||
cm = {'零': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5,
|
||||
'六': 6, '七': 7, '八': 8, '九': 9, '十': 10}
|
||||
t, tmp = 0, 0
|
||||
for ch in s:
|
||||
if ch == '零':
|
||||
continue
|
||||
if ch == '十':
|
||||
t += (tmp or 1) * 10
|
||||
tmp = 0
|
||||
elif ch == '百':
|
||||
t += (tmp or 1) * 100
|
||||
tmp = 0
|
||||
elif ch == '千':
|
||||
t += (tmp or 1) * 1000
|
||||
tmp = 0
|
||||
else:
|
||||
tmp = cm.get(ch, 0)
|
||||
t += tmp
|
||||
return t if t else 0
|
||||
+302
@@ -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()
|
||||
Reference in New Issue
Block a user