上传文件至「py」

This commit is contained in:
2026-08-14 11:24:46 +02:00
parent 4c008aa4a3
commit 23a6e84111
5 changed files with 1997 additions and 0 deletions
+329
View File
@@ -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
+542
View File
@@ -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('&nbsp;', ' ')
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
View File
@@ -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('&amp;', '&')
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('&amp;', '&')
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('&amp;', '&') 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, {}, '']
+308
View File
@@ -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
View File
@@ -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()