上传文件至「api」
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import re
|
||||
import sys
|
||||
from html import unescape
|
||||
from urllib.parse import urljoin, quote
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
host = 'https://www.xb6v.org'
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36',
|
||||
'Referer': 'https://www.xb6v.org/',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
}
|
||||
classes = [
|
||||
{'type_name': '首页', 'type_id': '/'},
|
||||
{'type_name': '喜剧片', 'type_id': '/xijupian/'},
|
||||
{'type_name': '动作片', 'type_id': '/dongzuopian/'},
|
||||
{'type_name': '爱情片', 'type_id': '/aiqingpian/'},
|
||||
{'type_name': '科幻片', 'type_id': '/kehuanpian/'},
|
||||
{'type_name': '恐怖片', 'type_id': '/kongbupian/'},
|
||||
{'type_name': '剧情片', 'type_id': '/juqingpian/'},
|
||||
{'type_name': '战争片', 'type_id': '/zhanzhengpian/'},
|
||||
{'type_name': '纪录片', 'type_id': '/jilupian/'},
|
||||
{'type_name': '动画片', 'type_id': '/donghuapian/'},
|
||||
{'type_name': '电视剧', 'type_id': '/dianshiju/'},
|
||||
{'type_name': '综艺', 'type_id': '/ZongYi/'},
|
||||
]
|
||||
|
||||
def getName(self):
|
||||
return '6v影视'
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {'class': self.classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list': self._parse_list(self._html(self.host + '/'))}
|
||||
|
||||
# 兼容部分 Python Spider 壳的命名
|
||||
def homeVod(self):
|
||||
return self.homeVideoContent()
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page = int(pg or 1)
|
||||
path = tid or '/'
|
||||
if page > 1:
|
||||
if path.endswith('/'):
|
||||
path = path + 'index_%d.html' % page
|
||||
else:
|
||||
path = path.rstrip('/') + '/index_%d.html' % page
|
||||
html = self._html(self._abs(path))
|
||||
videos = self._parse_list(html)
|
||||
return {'list': videos, 'page': page, 'pagecount': page + 1 if videos else page, 'limit': 18, 'total': 999999 if videos else 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = ids[0] if isinstance(ids, list) else ids
|
||||
url = self._abs(url)
|
||||
html = self._html(url)
|
||||
title = self._first([r'<title>\s*([^<]+?)(?:-|_|\|).*?</title>', r'<h1[^>]*>(.*?)</h1>'], html, '未知影片')
|
||||
pic = self._first([r'<div[^>]+class=["\'][^"\']*thumbnail[^"\']*["\'][\s\S]*?<img[^>]+(?:src|data-original|data-src)=["\']([^"\']+)["\']', r'<img[^>]+(?:src|data-original|data-src)=["\']([^"\']+\.(?:jpg|jpeg|png|webp)[^"\']*)["\']'], html, '')
|
||||
desc = self._first([r'◎简\s*介\s*([\s\S]*?)(?:◎|<h3|</article>|$)', r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']'], html, '暂无简介')
|
||||
lines = self._parse_detail_lines(html)
|
||||
play_from = '$$$'.join([x['name'] for x in lines]) or '详情页'
|
||||
play_url = '$$$'.join(['#'.join(['%s$%s' % (ep['title'], ep['url']) for ep in x['episodes']]) for x in lines]) or ('打开详情页$' + url)
|
||||
vod = {
|
||||
'vod_id': url,
|
||||
'vod_name': self._clean(title),
|
||||
'vod_pic': self._abs(pic),
|
||||
'type_name': '6v影视',
|
||||
'vod_year': self._year(title + ' ' + html),
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': self._clean(desc),
|
||||
'vod_play_from': play_from,
|
||||
'vod_play_url': play_url,
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = self.host + '/e/search/11index.php'
|
||||
body = 'keyboard=%s&show=title&tempid=1&tbname=article&mid=1&dopost=search&submit=' % quote(key or '')
|
||||
html = self._html(url, method='post', data=body, extra_headers={'Content-Type': 'application/x-www-form-urlencoded'})
|
||||
return {'list': self._parse_list(html)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = self._abs(id)
|
||||
low = url.lower()
|
||||
if low.startswith('magnet:') or any(x in low for x in ['pan.quark.cn', 'pan.baidu.com', 'xunlei.com', 'aliyundrive.com', 'alipan.com', 'cloud.189.cn', 'yun.139.com', '123pan']):
|
||||
return {'parse': 0, 'jx': 0, 'url': url, 'header': self.headers}
|
||||
real = self._resolve_play_url(url)
|
||||
return {'parse': 0, 'jx': 0, 'url': real or url, 'header': {'User-Agent': self.headers['User-Agent'], 'Referer': self.host + '/'}}
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
def _html(self, url, method='get', data=None, extra_headers=None):
|
||||
headers = dict(self.headers)
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
try:
|
||||
if method.lower() == 'post':
|
||||
res = self.fetch(url, headers=headers, data=data, method='post', timeout=20)
|
||||
else:
|
||||
res = self.fetch(url, headers=headers, timeout=20)
|
||||
return self._res_text(res)
|
||||
except Exception:
|
||||
# 某些壳不支持 method 参数,POST 搜索失败时返回空;分类/首页不受影响
|
||||
try:
|
||||
if method.lower() == 'post':
|
||||
res = self.fetch(url, headers=headers, postData=data, timeout=20)
|
||||
return self._res_text(res)
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
def _res_text(self, res):
|
||||
if res is None:
|
||||
return ''
|
||||
if isinstance(res, str):
|
||||
return res
|
||||
if isinstance(res, bytes):
|
||||
return self._decode(res)
|
||||
if isinstance(res, dict):
|
||||
val = res.get('content') or res.get('body') or res.get('data') or res.get('text') or ''
|
||||
if isinstance(val, bytes):
|
||||
return self._decode(val)
|
||||
return str(val or '')
|
||||
if hasattr(res, 'content'):
|
||||
val = getattr(res, 'content')
|
||||
if isinstance(val, bytes):
|
||||
return self._decode(val)
|
||||
return str(val or '')
|
||||
if hasattr(res, 'text'):
|
||||
return str(getattr(res, 'text') or '')
|
||||
return str(res)
|
||||
|
||||
def _decode(self, data):
|
||||
for enc in ('utf-8', 'gbk', 'gb18030'):
|
||||
try:
|
||||
txt = data.decode(enc)
|
||||
if '锟斤拷' not in txt and '\ufffd' not in txt:
|
||||
return txt
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return data.decode('utf-8', errors='ignore')
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def _abs(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
if str(url).startswith('magnet:'):
|
||||
return url
|
||||
return urljoin(self.host + '/', str(url).replace('&', '&'))
|
||||
|
||||
def _clean(self, text):
|
||||
text = re.sub(r'<script[\s\S]*?</script>|<style[\s\S]*?</style>', '', str(text or ''), flags=re.I)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
text = unescape(text)
|
||||
text = re.sub(r'"?>\s*', ' ', text)
|
||||
text = re.sub(r'[\ue000-\uf8ff]', '', text)
|
||||
return re.sub(r'\s+', ' ', text).strip()
|
||||
|
||||
def _first(self, patterns, html, default=''):
|
||||
for pat in patterns:
|
||||
m = re.search(pat, html or '', flags=re.I)
|
||||
if m:
|
||||
return self._clean(m.group(1))
|
||||
return default
|
||||
|
||||
def _year(self, text):
|
||||
m = re.search(r'\b((?:19|20)\d{2})\b', text or '')
|
||||
return m.group(1) if m else ''
|
||||
|
||||
def _parse_list(self, html):
|
||||
html = html or ''
|
||||
videos = []
|
||||
seen = set()
|
||||
# 6v 实测列表:<li class="post box row fixed-hight"> ... thumbnail ... h2 ... </li>
|
||||
blocks = re.findall(r'<li[^>]+class=["\'][^"\']*\bpost\b[^"\']*["\'][^>]*>[\s\S]*?</li>', html, flags=re.I)
|
||||
for block in blocks:
|
||||
href = self._first_raw([r'<h2>[\s\S]*?<a[^>]+href=["\']([^"\']+\.html)["\']', r'<a[^>]+href=["\']([^"\']+\.html)["\'][^>]*class=["\'][^"\']*zoom'], block)
|
||||
title = self._first_raw([r'<h2>[\s\S]*?<a[^>]*>([\s\S]*?)</a>', r'<h2>[\s\S]*?<a[^>]*title=["\']([^"\']+)["\']', r'<a[^>]*title=["\']([^"\']+)["\']'], block)
|
||||
pic = self._first_raw([r'<img[^>]+(?:src|data-original|data-src)=["\']([^"\']+)["\']'], block)
|
||||
item = self._item(href, title, pic)
|
||||
if item and item['vod_id'] not in seen:
|
||||
seen.add(item['vod_id'])
|
||||
videos.append(item)
|
||||
if videos:
|
||||
return videos
|
||||
# 兜底:全页 anchor,只收真正详情页,避免导航链接
|
||||
for href, inner in re.findall(r'<a\b[^>]*href=["\']([^"\']+\.html)["\'][^>]*>([\s\S]*?)</a>', html, flags=re.I):
|
||||
if '/e/' in href or href in ('/', '/index.html'):
|
||||
continue
|
||||
if not re.search(r'/[A-Za-z0-9_-]+/\d+\.html|/\d+\.html', href):
|
||||
continue
|
||||
title = self._clean(inner)
|
||||
pic = self._first_raw([r'<img[^>]+(?:src|data-original|data-src)=["\']([^"\']+)["\']'], inner)
|
||||
item = self._item(href, title, pic)
|
||||
if item and item['vod_id'] not in seen:
|
||||
seen.add(item['vod_id'])
|
||||
videos.append(item)
|
||||
return videos
|
||||
|
||||
def _first_raw(self, patterns, text):
|
||||
for pat in patterns:
|
||||
m = re.search(pat, text or '', flags=re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
def _item(self, href, title, pic=''):
|
||||
title = self._clean(title)
|
||||
if not href or len(title) < 2:
|
||||
return None
|
||||
url = self._abs(href)
|
||||
return {'vod_id': url, 'vod_name': title, 'vod_pic': self._abs(pic), 'vod_remarks': self._year(title) or '点击查看'}
|
||||
|
||||
def _parse_detail_lines(self, html):
|
||||
lines = []
|
||||
h3s = list(re.finditer(r'<h3[^>]*>([^<]*播放地址[^<]*)</h3>', html or '', flags=re.I))
|
||||
for i, h3 in enumerate(h3s):
|
||||
section = html[h3.start():(h3s[i + 1].start() if i + 1 < len(h3s) else len(html))]
|
||||
eps = self._parse_eps(section, play=True)
|
||||
if eps:
|
||||
lines.append({'name': self._clean(h3.group(1)) or '在线播放', 'episodes': eps})
|
||||
if not lines:
|
||||
eps = self._parse_eps(html, play=True)
|
||||
if eps:
|
||||
lines.append({'name': '在线播放', 'episodes': eps})
|
||||
idx = (html or '').find('【下载地址】')
|
||||
if idx >= 0:
|
||||
sec = html[idx:]
|
||||
cut_points = [x for x in [sec.find('<h3', 6), sec.find('<div class="widget', 6)] if x > 0]
|
||||
if cut_points:
|
||||
sec = sec[:min(cut_points)]
|
||||
eps = self._parse_eps(sec, play=False)
|
||||
if eps:
|
||||
lines.append({'name': '下载地址', 'episodes': eps})
|
||||
return lines
|
||||
|
||||
def _parse_eps(self, html, play=True):
|
||||
eps, seen = [], set()
|
||||
if play:
|
||||
pat = r'<a\s+(?:[^>]*?\s+)?href\s*=\s*["\']([^"\']*/e/DownSys/play/[^"\']+)["\'][^>]*>(.*?)</a>'
|
||||
else:
|
||||
pat = r'<a\s+(?:[^>]*?\s+)?href\s*=\s*["\']([^"\']+)["\'][^>]*>(.*?)</a>'
|
||||
for href, title in re.findall(pat, html or '', flags=re.I):
|
||||
title = self._clean(title)
|
||||
if href in seen or not title:
|
||||
continue
|
||||
if not play and ('#respond' in href or 'category' in href):
|
||||
continue
|
||||
seen.add(href)
|
||||
if not play and (title == '链接' or len(title) < 2):
|
||||
low = href.lower()
|
||||
title = '夸克网盘' if 'quark' in low else ('迅雷网盘' if 'xunlei' in low else ('磁力链接' if low.startswith('magnet:') else '下载'))
|
||||
eps.append({'title': title, 'url': self._abs(href)})
|
||||
return eps
|
||||
|
||||
def _resolve_play_url(self, play_url):
|
||||
html = self._html(play_url)
|
||||
media = self._find_media(html, play_url)
|
||||
if media:
|
||||
return media
|
||||
iframe = re.search(r'<iframe[^>]+src\s*=\s*["\']([^"\']+)["\']', html or '', flags=re.I)
|
||||
if iframe:
|
||||
iframe_url = urljoin(play_url, iframe.group(1))
|
||||
media = self._find_media(self._html(iframe_url), iframe_url)
|
||||
if media:
|
||||
return media
|
||||
return None
|
||||
|
||||
def _find_media(self, html, base_url):
|
||||
patterns = [
|
||||
r'https?://[^\s"\'<>]+\.(?:m3u8|mp4)[^\s"\'<>]*',
|
||||
r'const\s+url\s*=\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']',
|
||||
r'url\s*[:=]\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']',
|
||||
r'["\']url["\']\s*:\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']',
|
||||
]
|
||||
for pat in patterns:
|
||||
m = re.search(pat, html or '', flags=re.I)
|
||||
if m:
|
||||
val = m.group(1) if m.lastindex else m.group(0)
|
||||
return urljoin(base_url, val.replace('\\/', '/'))
|
||||
return None
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习及爬虫技术交流。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
"""
|
||||
{
|
||||
"key": "xxx",
|
||||
"name": "xxx",
|
||||
"type": 3,
|
||||
"api": "./ApptoV5无加密.py",
|
||||
"ext": "http://domain.com"
|
||||
}
|
||||
"""
|
||||
|
||||
import re,sys,uuid
|
||||
from base.spider import Spider
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
host,config,local_uuid,parsing_config = '','','',[]
|
||||
headers = {
|
||||
'User-Agent': "Dart/2.19 (dart:io)",
|
||||
'Accept-Encoding': "gzip",
|
||||
'appto-local-uuid': local_uuid
|
||||
}
|
||||
|
||||
def init(self, extend=''):
|
||||
try:
|
||||
host = extend.strip()
|
||||
if not host.startswith('http'):
|
||||
return {}
|
||||
if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host):
|
||||
host_=self.fetch(host).json()
|
||||
self.host = host_['domain']
|
||||
else:
|
||||
self.host = host
|
||||
self.local_uuid = str(uuid.uuid4())
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/config/get?p=android&__platform=android', headers=self.headers).json()
|
||||
config = response['data']
|
||||
self.config = config
|
||||
parsing_conf = config['get_parsing']['lists']
|
||||
parsing_config = {}
|
||||
for i in parsing_conf:
|
||||
if len(i['config']) != 0:
|
||||
label = []
|
||||
for j in i['config']:
|
||||
if j['type'] == 'json':
|
||||
label.append(j['label'])
|
||||
parsing_config.update({i['key']:label})
|
||||
self.parsing_config = parsing_config
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f'初始化异常:{e}')
|
||||
return {}
|
||||
|
||||
def detailContent(self, ids):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/getVod?id={ids[0]}",headers=self.headers).json()
|
||||
data3 = response['data']
|
||||
videos = []
|
||||
vod_play_url = ''
|
||||
vod_play_from = ''
|
||||
for i in data3['vod_play_list']:
|
||||
play_url = ''
|
||||
for j in i['urls']:
|
||||
play_url += f"{j['name']}${i['player_info']['from']}@{j['url']}#"
|
||||
vod_play_from += i['player_info']['show'] + '$$$'
|
||||
vod_play_url += play_url.rstrip('#') + '$$$'
|
||||
vod_play_url = vod_play_url.rstrip('$$$')
|
||||
vod_play_from = vod_play_from.rstrip('$$$')
|
||||
videos.append({
|
||||
'vod_id': data3.get('vod_id'),
|
||||
'vod_name': data3.get('vod_name'),
|
||||
'vod_content': data3.get('vod_content'),
|
||||
'vod_remarks': data3.get('vod_remarks'),
|
||||
'vod_director': data3.get('vod_director'),
|
||||
'vod_actor': data3.get('vod_actor'),
|
||||
'vod_year': data3.get('vod_year'),
|
||||
'vod_area': data3.get('vod_area'),
|
||||
'vod_play_from': vod_play_from,
|
||||
'vod_play_url': vod_play_url
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = f"{self.host}/apptov5/v1/search/lists?wd={key}&page={pg}&type=&__platform=android"
|
||||
response = self.fetch(url, headers=self.headers).json()
|
||||
data = response['data']['data']
|
||||
for i in data:
|
||||
if i.get('vod_pic').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data, 'page': pg, 'total': response['data']['total']}
|
||||
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
default_ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
parsing_config = self.parsing_config
|
||||
parts = id.split('@')
|
||||
if len(parts) != 2:
|
||||
return {'parse': 0, 'url': id, 'header': {'User-Agent': default_ua}}
|
||||
playfrom, rawurl = parts
|
||||
label_list = parsing_config.get(playfrom)
|
||||
if not label_list:
|
||||
return {'parse': 0, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
result = {'parse': 1, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
for label in label_list:
|
||||
payload = {
|
||||
'play_url': rawurl,
|
||||
'label': label,
|
||||
'key': playfrom
|
||||
}
|
||||
try:
|
||||
response = self.post(
|
||||
f"{self.host}/apptov5/v1/parsing/proxy?__platform=android",
|
||||
data=payload,
|
||||
headers=self.headers
|
||||
).json()
|
||||
except Exception as e:
|
||||
print(f"请求异常: {e}")
|
||||
continue
|
||||
if not isinstance(response, dict):
|
||||
continue
|
||||
if response.get('code') == 422:
|
||||
continue
|
||||
data = response.get('data')
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
url = data.get('url')
|
||||
if not url:
|
||||
continue
|
||||
ua = data.get('UA') or data.get('UserAgent') or default_ua
|
||||
result = {
|
||||
'parse': 0,
|
||||
'url': url,
|
||||
'header': {'User-Agent': ua}
|
||||
}
|
||||
break
|
||||
return result
|
||||
|
||||
def homeContent(self, filter):
|
||||
config = self.config
|
||||
if not config:
|
||||
return {}
|
||||
home_cate = config['get_home_cate']
|
||||
classes = []
|
||||
for i in home_cate:
|
||||
if isinstance(i.get('extend', []),dict):
|
||||
classes.append({'type_id': i['cate'], 'type_name': i['title']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/home/data?id=1&mold=1&__platform=android',headers=self.headers).json()
|
||||
data = response['data']
|
||||
vod_list = []
|
||||
for i in data['sections']:
|
||||
for j in i['items']:
|
||||
vod_pic = j.get('vod_pic')
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'http://', 1)
|
||||
vod_list.append({
|
||||
"vod_id": j.get('vod_id'),
|
||||
"vod_name": j.get('vod_name'),
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": j.get('vod_remarks')
|
||||
})
|
||||
return {'list': vod_list}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/lists?area={extend.get('area','')}&lang={extend.get('lang','')}&year={extend.get('year','')}&order={extend.get('sort','time')}&type_id={tid}&type_name=&page={pg}&pageSize=21&__platform=android", headers=self.headers).json()
|
||||
data = response['data']
|
||||
data2 = data['data']
|
||||
for i in data['data']:
|
||||
if i.get('vod_pic','').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data2, 'page': pg, 'total': data['total']}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
# coding = utf-8
|
||||
#!/usr/bin/python
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import urllib.parse
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
from base.spider import Spider
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.name = "瓜子"
|
||||
self.hosts = [
|
||||
'https://apinew.uozvr.com',
|
||||
'https://api.w32z7vtd.com',
|
||||
'https://api.6a7nnf7.com',
|
||||
'https://api.umygrx3.com',
|
||||
'https://api.rmedphk.com'
|
||||
]
|
||||
self.host_index = 0
|
||||
self.host = self.hosts[self.host_index]
|
||||
|
||||
# AES 固定密钥(与Java版一致)
|
||||
self.AES_KEY = 'OITxa5OqAYjhswxx'
|
||||
self.AES_IV = 'rCMNwZASNBKZ8mXV'
|
||||
|
||||
# RSA 公钥/私钥
|
||||
self.RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUM5+/y8sPsWkd1/RQS64X259EUwxFXFE5HlA65MqrxnPs0JqoSRojSDy5QhwvROlaD6TwRQHKMY2OAZ6SnQeUJsChTEFIR9qUkwrs3/MVUMxjsv6JS6Oe/juclyJGTgVmDhB55EafXsD0SQYVj/QXXsxR6ewR5E2kL52yAAD4yQIDAQAB"
|
||||
self.RSA_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGAe6hKrWLi1zQmjTT1
|
||||
ozbE4QdFeJGNxubxld6GrFGximxfMsMB6BpJhpcTouAqywAFppiKetUBBbXwYsYU
|
||||
1wNr648XVmPmCMCy4rY8vdliFnbMUj086DU6Z+/oXBdWU3/b1G0DN3E9wULRSwcK
|
||||
ZT3wj/cCI1vsCm3gj2R5SqkA9Y0CAwEAAQKBgAJH+4CxV0/zBVcLiBCHvSANm0l7
|
||||
HetybTh/j2p0Y1sTXro4ALwAaCTUeqdBjWiLSo9lNwDHFyq8zX90+gNxa7c5EqcW
|
||||
V9FmlVXr8VhfBzcZo1nXeNdXFT7tQ2yah/odtdcx+vRMSGJd1t/5k5bDd9wAvYdI
|
||||
DblMAg+wiKKZ5KcdAkEA1cCakEN4NexkF5tHPRrR6XOY/XHfkqXxEhMqmNbB9U34
|
||||
saTJnLWIHC8IXys6Qmzz30TtzCjuOqKRRy+FMM4TdwJBAJQZFPjsGC+RqcG5UvVM
|
||||
iMPhnwe/bXEehShK86yJK/g/UiKrO87h3aEu5gcJqBygTq3BBBoH2md3pr/W+hUM
|
||||
WBsCQQChfhTIrdDinKi6lRxrdBnn0Ohjg2cwuqK5zzU9p/N+S9x7Ck8wUI53DKm8
|
||||
jUJE8WAG7WLj/oCOWEh+ic6NIwTdAkEAj0X8nhx6AXsgCYRql1klbqtVmL8+95KZ
|
||||
K7PnLWG/IfjQUy3pPGoSaZ7fdquG8bq8oyf5+dzjE/oTXcByS+6XRQJAP/5ciy1b
|
||||
L3NhUhsaOVy55MHXnPjdcTX0FaLi+ybXZIfIQ2P4rb19mVq1feMbCXhz+L1rG8oa
|
||||
t5lYKfpe8k83ZA==
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
self.DEVICE_OLD_KEY = "aLFBMWpxBrIDAD1Si/KVvm41"
|
||||
|
||||
# 设备信息(随机生成)
|
||||
self.deviceId = str(864150060000000 + random.randint(0, 9999))
|
||||
self.deviceKey = ''.join(random.choices('0123456789ABCDEF', k=40)) # 20字节hex大写
|
||||
self.token = ""
|
||||
self.token_id = ""
|
||||
self.registered = False
|
||||
|
||||
self.header = {
|
||||
'User-Agent': 'Lavf/57.83.100',
|
||||
'code': 'GZ0369',
|
||||
'deviceId': self.deviceId,
|
||||
'lang': 'zh_cn',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Version': '2604028',
|
||||
'PackageName': 'com.ae06aebdbb.y286327f5a.ofe849883320260517',
|
||||
'Ver': '3.0.3.2',
|
||||
'api-ver': '3.0.3.2',
|
||||
'Referer': self.host
|
||||
}
|
||||
|
||||
self.cache = {}
|
||||
self.cache_timeout = 300
|
||||
|
||||
# 初始化token
|
||||
self.init_token()
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
# ---------- 设备注册与认证 ----------
|
||||
def init_token(self):
|
||||
"""初始化token:注册设备 -> 刷新"""
|
||||
print("===== 初始化设备认证 =====")
|
||||
try:
|
||||
if not self.registered:
|
||||
self.sign_up()
|
||||
# 刷新获取最终token
|
||||
self.refresh_token()
|
||||
except Exception as e:
|
||||
print(f"初始化token失败: {e}")
|
||||
# 兜底使用原有硬编码(几乎没用)
|
||||
self.token = '024212ef0975c5306a1434e113a46463.bc77313e11a248558a6ca244ca980944ec3421fa480c50e0229ad91f1cb15aea582603202cd71796885c9e5163e500f1b72f737059aff1ddb8beea47c5a331d6760540345b7f88b2302a0e6e09589f9dcf3ff9175d8c905f990203f5fc04748008ea7a366571cbf5b09509a873dcfba3cf1d5590385f5f7ef6e01d1850974aa220eb5178c89e61c24411af9b9a19435e.06fde789ece48d9b33c5dc857e04e9b5838f08264d928b87237d3476c4484b46'
|
||||
|
||||
def sign_up(self):
|
||||
"""注册设备"""
|
||||
print("注册新设备...")
|
||||
params = {
|
||||
"new_key": self.deviceKey,
|
||||
"old_key": self.DEVICE_OLD_KEY,
|
||||
"phone_type": 1,
|
||||
"code": ""
|
||||
}
|
||||
result = self._auth_request('/App/Authentication/Device/signUp', params)
|
||||
self._apply_auth(result)
|
||||
self.registered = True
|
||||
|
||||
def sign_in(self):
|
||||
"""登录设备"""
|
||||
print("设备登录...")
|
||||
params = {
|
||||
"new_key": self.deviceKey,
|
||||
"old_key": self.DEVICE_OLD_KEY
|
||||
}
|
||||
result = self._auth_request('/App/Authentication/Device/signIn', params)
|
||||
self._apply_auth(result)
|
||||
|
||||
def _apply_auth(self, result):
|
||||
"""从认证响应中提取token"""
|
||||
new_token = result.get('token', '')
|
||||
if not new_token:
|
||||
raise Exception("认证失败,无token返回: {}".format(result))
|
||||
self.token = new_token
|
||||
new_token_id = result.get('app_user_id', '')
|
||||
if new_token_id:
|
||||
self.token_id = new_token_id
|
||||
print(f"获取token成功, token前缀: {self.token[:30]}...")
|
||||
|
||||
def refresh_token(self):
|
||||
"""刷新token"""
|
||||
print("刷新token...")
|
||||
result = self._auth_request('/App/Authentication/Authenticator/refresh', {})
|
||||
self._apply_auth(result)
|
||||
|
||||
def _auth_request(self, path, params):
|
||||
"""认证类请求(不需要ensure_token)"""
|
||||
return self._send_encrypted_request(params, path, is_auth=True)
|
||||
|
||||
# ---------- 业务请求核心(修复加密与签名) ----------
|
||||
def ensure_token(self):
|
||||
"""确保token有效,如未就绪则重新获取"""
|
||||
if not self.token or not self.token_id:
|
||||
if self.registered:
|
||||
self.sign_in()
|
||||
else:
|
||||
self.sign_up()
|
||||
self.refresh_token()
|
||||
|
||||
def _send_encrypted_request(self, data, path, is_auth=False):
|
||||
"""
|
||||
发送加密请求,返回解密后的字典
|
||||
:param data: 业务参数字典
|
||||
:param path: 请求路径
|
||||
:param is_auth: 是否为认证类请求(signUp/signIn/refresh),此时不使用ensure_token
|
||||
"""
|
||||
try:
|
||||
if not is_auth:
|
||||
self.ensure_token()
|
||||
|
||||
# 1. 将参数转为JSON并AES加密
|
||||
json_params = json.dumps(data)
|
||||
encrypted = self.aes_encrypt(json_params, self.AES_KEY, self.AES_IV)
|
||||
request_key = encrypted.upper() # Java中是bytesToHex(encrypted).toUpperCase()
|
||||
|
||||
# 2. 生成keys (RSA加密 iv/key JSON)
|
||||
key_json = json.dumps({"iv": self.AES_IV, "key": self.AES_KEY})
|
||||
keys = self.rsa_encrypt(key_json, self.RSA_PUBLIC_KEY)
|
||||
|
||||
# 3. 生成签名
|
||||
t = str(int(time.time()))
|
||||
sign_str = f"token_id=,token={self.token},phone_type=1,request_key={request_key},app_id=1,time={t},keys={keys}*&zvdvdvddbfikkkumtmdwqppp?|4Y!s!2br"
|
||||
signature = self.get_md5(sign_str) # 已改为大写
|
||||
|
||||
# 4. 构建请求体
|
||||
body = {
|
||||
'token': self.token,
|
||||
'token_id': '',
|
||||
'phone_type': '1',
|
||||
'time': t,
|
||||
'phone_model': 'xiaomi-25031', # 与Java版保持一致
|
||||
'keys': keys,
|
||||
'request_key': request_key,
|
||||
'signature': signature,
|
||||
'app_id': '1',
|
||||
'ad_version': '1'
|
||||
}
|
||||
|
||||
# 5. 发送请求
|
||||
url = f"{self.host}{path}"
|
||||
response = self.post(url, headers=self.header, data=body, timeout=10)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"HTTP {response.status_code}")
|
||||
|
||||
resp_json = response.json()
|
||||
# 检查业务code(若不为200可能token过期)
|
||||
if 'code' in resp_json and resp_json['code'] != 200:
|
||||
print(f"业务错误码: {resp_json['code']}, 信息: {resp_json}")
|
||||
# 如果不是认证请求,尝试重新获取token后重试一次(这里简单处理,外层get_data已有重试)
|
||||
raise Exception("业务错误")
|
||||
|
||||
data_section = resp_json.get('data')
|
||||
if not data_section:
|
||||
raise Exception("响应缺少data字段")
|
||||
|
||||
encrypted_response = data_section.get('response_key', '')
|
||||
encrypted_keys = data_section.get('keys', '')
|
||||
|
||||
# 6. 解密响应
|
||||
decrypted_keys_json = self.rsa_decrypt(encrypted_keys, self.RSA_PRIVATE_KEY)
|
||||
key_info = json.loads(decrypted_keys_json)
|
||||
resp_key = key_info['key']
|
||||
resp_iv = key_info['iv']
|
||||
decrypted_data = self.aes_decrypt(encrypted_response, resp_key, resp_iv)
|
||||
return json.loads(decrypted_data)
|
||||
|
||||
except Exception as e:
|
||||
print(f"请求失败 [{path}]: {e}")
|
||||
return None
|
||||
|
||||
def get_data(self, data, path, use_cache=True):
|
||||
"""带重试和域名轮询的数据获取(保持原框架)"""
|
||||
try:
|
||||
cache_key = f"{path}_{hash(str(data))}" if use_cache else None
|
||||
if use_cache and cache_key in self.cache:
|
||||
cached_data, timestamp = self.cache[cache_key]
|
||||
if time.time() - timestamp < self.cache_timeout:
|
||||
return cached_data
|
||||
|
||||
for attempt in range(3):
|
||||
tried = 0
|
||||
while tried < len(self.hosts):
|
||||
self.host = self.hosts[self.host_index]
|
||||
self.header['Referer'] = self.host
|
||||
result = self._send_encrypted_request(data, path)
|
||||
if result is not None:
|
||||
print(f"请求成功: {path}, 域名: {self.host}")
|
||||
if use_cache and cache_key:
|
||||
self.cache[cache_key] = (result, time.time())
|
||||
return result
|
||||
|
||||
# 切换到下一个域名
|
||||
self.host_index = (self.host_index + 1) % len(self.hosts)
|
||||
tried += 1
|
||||
|
||||
# 所有域名失败,尝试重新认证并重试
|
||||
if attempt < 2:
|
||||
print("所有域名失败,尝试重新认证...")
|
||||
try:
|
||||
self.ensure_token()
|
||||
except:
|
||||
pass
|
||||
self.host_index = 0
|
||||
else:
|
||||
break
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"get_data异常: {e}")
|
||||
return None
|
||||
|
||||
# ---------- 加解密工具 ----------
|
||||
def aes_encrypt(self, text, key, iv):
|
||||
try:
|
||||
key_bytes = key.encode('utf-8')
|
||||
iv_bytes = iv.encode('utf-8')
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||||
encrypted = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size))
|
||||
return encrypted.hex().upper()
|
||||
except Exception as e:
|
||||
print(f"AES加密失败: {e}")
|
||||
return ""
|
||||
|
||||
def aes_decrypt(self, text, key, iv):
|
||||
try:
|
||||
key_bytes = key.encode('utf-8')
|
||||
iv_bytes = iv.encode('utf-8')
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||||
encrypted_bytes = bytes.fromhex(text)
|
||||
decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
|
||||
return decrypted.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"AES解密失败: {e}")
|
||||
return ""
|
||||
|
||||
def rsa_encrypt(self, text, public_key_str):
|
||||
"""RSA公钥加密(PKCS1v1.5)"""
|
||||
try:
|
||||
key = RSA.import_key("-----BEGIN PUBLIC KEY-----\n" + public_key_str + "\n-----END PUBLIC KEY-----")
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted = cipher.encrypt(text.encode('utf-8'))
|
||||
return base64.b64encode(encrypted).decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"RSA加密失败: {e}")
|
||||
return ""
|
||||
|
||||
def rsa_decrypt(self, encrypted_data, private_key_str):
|
||||
"""RSA私钥解密"""
|
||||
try:
|
||||
encrypted_bytes = base64.b64decode(encrypted_data)
|
||||
rsa_key = RSA.import_key(private_key_str)
|
||||
cipher = PKCS1_v1_5.new(rsa_key)
|
||||
decrypted = cipher.decrypt(encrypted_bytes, None)
|
||||
return decrypted.decode('utf-8') if decrypted else ""
|
||||
except Exception as e:
|
||||
print(f"RSA解密失败: {e}")
|
||||
return ""
|
||||
|
||||
def get_md5(self, text):
|
||||
return hashlib.md5(text.encode()).hexdigest().upper() # 与Java一致大写
|
||||
|
||||
# ---------- 业务方法(不变) ----------
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = [
|
||||
{"type_name": "电影", "type_id": "1"},
|
||||
{"type_name": "电视剧", "type_id": "2"},
|
||||
{"type_name": "动漫", "type_id": "4"},
|
||||
{"type_name": "综艺", "type_id": "3"},
|
||||
{"type_name": "短剧", "type_id": "64"}
|
||||
]
|
||||
result['class'] = classes
|
||||
filters = {}
|
||||
for cate in classes:
|
||||
tid = cate['type_id']
|
||||
filters[tid] = [
|
||||
{"key": "area", "name": "地区", "value": [
|
||||
{"n": "全部", "v": "0"}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "法国", "v": "法国"},
|
||||
{"n": "泰国", "v": "泰国"}, {"n": "印度", "v": "印度"}, {"n": "其他", "v": "其他"}
|
||||
]},
|
||||
{"key": "year", "name": "年份", "value": [
|
||||
{"n": "全部", "v": "0"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"},
|
||||
{"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"}, {"n": "2015", "v": "2015"},
|
||||
{"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"},
|
||||
{"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"},
|
||||
{"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"},
|
||||
{"n": "2005", "v": "2005"}, {"n": "更早", "v": "2004"}
|
||||
]},
|
||||
{"key": "sort", "name": "排序", "value": [
|
||||
{"n": "最新", "v": "d_id"}, {"n": "最热", "v": "d_hits"}, {"n": "推荐", "v": "d_score"}
|
||||
]}
|
||||
]
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
videos = []
|
||||
try:
|
||||
body = {
|
||||
"area": extend.get('area', '0'),
|
||||
"year": extend.get('year', '0'),
|
||||
"pageSize": "30",
|
||||
"sort": extend.get('sort', 'd_id'),
|
||||
"page": str(pg),
|
||||
"tid": tid
|
||||
}
|
||||
cache_key = f"category_{tid}_{pg}_{hash(str(body))}"
|
||||
data = self.get_cached_data(cache_key, body, '/App/IndexList/indexList')
|
||||
if data and 'list' in data:
|
||||
for item in data['list']:
|
||||
vod_continu = item.get('vod_continu', 0)
|
||||
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集'
|
||||
video = {
|
||||
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
|
||||
"vod_name": item.get('vod_name', ''),
|
||||
"vod_pic": item.get('vod_pic', ''),
|
||||
"vod_remarks": remarks
|
||||
}
|
||||
videos.append(video)
|
||||
except Exception as e:
|
||||
print(f"获取分类内容失败: {e}")
|
||||
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
vod_id = ids[0].split('/')[0]
|
||||
t = str(int(time.time()))
|
||||
body1 = {"token_id": self.token_id, "vod_id": vod_id, "mobile_time": t, "token": self.token}
|
||||
qdata = self.get_data(body1, '/App/IndexPlay/playInfo')
|
||||
body2 = {"vurl_cloud_id": "2", "vod_d_id": vod_id}
|
||||
jdata = self.get_data(body2, '/App/Resource/Vurl/show')
|
||||
if not qdata or 'vodInfo' not in qdata:
|
||||
return {'list': []}
|
||||
vod = qdata['vodInfo']
|
||||
video_detail = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": vod.get('vod_name', ''),
|
||||
"vod_pic": vod.get('vod_pic', ''),
|
||||
"vod_year": vod.get('vod_year', ''),
|
||||
"vod_area": vod.get('vod_area', ''),
|
||||
"vod_actor": vod.get('vod_actor', ''),
|
||||
"vod_director": vod.get('vod_director', ''),
|
||||
"vod_content": vod.get('vod_use_content', '').strip(),
|
||||
"vod_play_from": "瓜子影视"
|
||||
}
|
||||
play_list = []
|
||||
if jdata and 'list' in jdata:
|
||||
for index, item in enumerate(jdata['list']):
|
||||
if 'play' in item:
|
||||
n, p = [], []
|
||||
for key, value in item['play'].items():
|
||||
if 'param' in value and value['param']:
|
||||
n.append(key)
|
||||
p.append(value['param'])
|
||||
if p:
|
||||
play_name = str(index + 1) if len(jdata['list']) != 1 else vod.get('vod_name', '')
|
||||
play_url = f"{p[-1]}||{'@'.join(n)}"
|
||||
play_list.append(f"{play_name}${play_url}")
|
||||
video_detail["vod_play_url"] = "#".join(play_list)
|
||||
return {'list': [video_detail]}
|
||||
except Exception as e:
|
||||
print(f"获取详情失败: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
videos = []
|
||||
try:
|
||||
body = {"keywords": key, "order_val": "1", "page": str(pg)}
|
||||
data = self.get_data(body, '/App/Index/findMoreVod', use_cache=False)
|
||||
if data and 'list' in data:
|
||||
for item in data['list']:
|
||||
vod_continu = item.get('vod_continu', 0)
|
||||
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集'
|
||||
videos.append({
|
||||
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
|
||||
"vod_name": item.get('vod_name', ''),
|
||||
"vod_pic": item.get('vod_pic', ''),
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"搜索失败: {e}")
|
||||
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
parts = id.split('||')
|
||||
if len(parts) < 2:
|
||||
return {"parse": 0, "playUrl": "", "url": ""}
|
||||
param_str = parts[0]
|
||||
resolutions = parts[1].split('@') if len(parts) > 1 else []
|
||||
params = {}
|
||||
for pair in param_str.split('&'):
|
||||
if '=' in pair:
|
||||
key, value = pair.split('=', 1)
|
||||
params[key] = value
|
||||
if resolutions:
|
||||
resolutions.sort(key=lambda x: int(x) if x.isdigit() else 0, reverse=True)
|
||||
params['resolution'] = resolutions[0]
|
||||
data = self.get_data(params, '/App/Resource/VurlDetail/showOne', use_cache=False)
|
||||
if data and 'url' in data:
|
||||
return {"parse": 0, "playUrl": "", "url": data['url'],
|
||||
"header": json.dumps({"User-Agent": "Lavf/57.83.100", "Referer": "http://WJiZxLXA2.com/"}), 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'}
|
||||
return {"parse": 0, "playUrl": "", "url": ""}
|
||||
except Exception as e:
|
||||
print(f"播放解析失败: {e}")
|
||||
return {"parse": 0, "playUrl": "", "url": ""}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts']
|
||||
return any(url.lower().endswith(fmt) for fmt in video_formats)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, params):
|
||||
return None
|
||||
|
||||
def get_cached_data(self, cache_key, data, path):
|
||||
current_time = time.time()
|
||||
if cache_key in self.cache:
|
||||
cached_data, timestamp = self.cache[cache_key]
|
||||
if current_time - timestamp < self.cache_timeout:
|
||||
return cached_data
|
||||
result = self.get_data(data, path)
|
||||
if result:
|
||||
self.cache[cache_key] = (result, current_time)
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '聚合儿歌[儿]',
|
||||
lang: 'cat'
|
||||
})
|
||||
*/
|
||||
|
||||
let siteName = '聚合儿歌', siteKey = '', siteType = 0;
|
||||
|
||||
const platformList = [
|
||||
{ name: '贝乐虎', id: 'beilehu' },
|
||||
{ name: '兔小贝', id: 'tuxiaobei' }
|
||||
];
|
||||
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
};
|
||||
|
||||
const rule = {
|
||||
beilehu: {
|
||||
host: 'https://vd.ubestkid.com',
|
||||
api: '/api/v1/bv/video'
|
||||
},
|
||||
tuxiaobei: {
|
||||
host: 'https://www.tuxiaobei.com',
|
||||
listApi: '/list/mip-data',
|
||||
playUrl: '/play/',
|
||||
searchApi: '/search/'
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = {
|
||||
beilehu: [{
|
||||
key: "area", name: "分类",
|
||||
value: [
|
||||
{ "n": "最新上架", "v": "65" }, { "n": "人气热播", "v": "113" }, { "n": "经典童谣", "v": "56" },
|
||||
{ "n": "开心贝乐虎", "v": "137" }, { "n": "律动儿歌", "v": "53" }, { "n": "经典儿歌", "v": "59" },
|
||||
{ "n": "超级汽车1", "v": "101" }, { "n": "超级汽车第二季", "v": "119" }, { "n": "超级汽车第三季", "v": "136" },
|
||||
{ "n": "三字经", "v": "95" }, { "n": "幼儿手势舞", "v": "133" }, { "n": "哄睡儿歌", "v": "117" },
|
||||
{ "n": "英文儿歌", "v": "70" }, { "n": "节日与节气", "v": "116" }, { "n": "恐龙世界", "v": "97" },
|
||||
{ "n": "动画片儿歌", "v": "55" }, { "n": "流行歌曲", "v": "57" }, { "n": "贝乐虎入园记", "v": "118" },
|
||||
{ "n": "贝乐虎大百科", "v": "106" }, { "n": "经典古诗", "v": "62" }, { "n": "经典故事", "v": "63" },
|
||||
{ "n": "萌虎学功夫", "v": "128" }, { "n": "绘本故事", "v": "100" }, { "n": "开心贝乐虎英文版", "v": "121" },
|
||||
{ "n": "嗨贝乐虎情商动画", "v": "96" }, { "n": "动物音乐派对", "v": "108" }, { "n": "动物音乐派对英文版", "v": "126" },
|
||||
{ "n": "奇妙的身体", "v": "105" }, { "n": "奇妙的身体英文版", "v": "124" }, { "n": "认知卡片", "v": "64" },
|
||||
{ "n": "趣味简笔画", "v": "109" }, { "n": "数字儿歌", "v": "78" }, { "n": "识字体验版", "v": "120" },
|
||||
{ "n": "启蒙系列体验版", "v": "127" }
|
||||
]
|
||||
}],
|
||||
tuxiaobei: [{
|
||||
key: "area", name: "分类",
|
||||
value: [
|
||||
{ "n": "全部", "v": "" }, { "n": "儿歌", "v": "2" }, { "n": "故事", "v": "3" },
|
||||
{ "n": "公益", "v": "27" }, { "n": "十万个为什么", "v": "9" }, { "n": "安全教育", "v": "28" },
|
||||
{ "n": "动物奇缘", "v": "29" }, { "n": "弟子规", "v": "7" }, { "n": "古诗", "v": "5" },
|
||||
{ "n": "三字经", "v": "6" }, { "n": "千字文", "v": "8" }, { "n": "数学", "v": "11" },
|
||||
{ "n": "英语", "v": "25" }, { "n": "折纸", "v": "24" }
|
||||
]
|
||||
}]
|
||||
};
|
||||
|
||||
const ruleFilterDef = {
|
||||
beilehu: { area: '56' },
|
||||
tuxiaobei: { area: '2' }
|
||||
};
|
||||
|
||||
function init(cfg) {
|
||||
siteName = cfg.skey?.split('_')[1] || cfg.skey || '聚合儿歌';
|
||||
siteKey = cfg.skey;
|
||||
siteType = cfg.stype;
|
||||
}
|
||||
|
||||
function safeJSONParse(str, defaultValue = {}) {
|
||||
if (!str || typeof str === 'object') return str || defaultValue;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(url, options = {}) {
|
||||
const reqHeaders = { ...headers, ...options.headers };
|
||||
let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' :
|
||||
reqHeaders['Content-Type']?.includes('form') ? 'form' : '';
|
||||
|
||||
try {
|
||||
const response = await req(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: reqHeaders,
|
||||
data: options.data,
|
||||
postType: postType,
|
||||
timeout: options.timeout || 15000
|
||||
});
|
||||
return response?.content || response?.data || response;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatList() {
|
||||
return platformList;
|
||||
}
|
||||
|
||||
async function getBeilehuList(typeId, page) {
|
||||
let videos = [];
|
||||
try {
|
||||
const postData = {
|
||||
age: 1,
|
||||
appver: "6.1.9",
|
||||
egvip_status: 0,
|
||||
svip_status: 0,
|
||||
vps: 60,
|
||||
subcateId: parseInt(typeId),
|
||||
p: page
|
||||
};
|
||||
|
||||
const html = await request(rule.beilehu.host + rule.beilehu.api, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
data: postData
|
||||
});
|
||||
|
||||
const json = safeJSONParse(html);
|
||||
const items = json.result?.items || [];
|
||||
|
||||
videos = items.map(item => ({
|
||||
vod_id: `beilehu@${item.url}`,
|
||||
vod_name: item.title || '未知视频',
|
||||
vod_pic: item.image || '',
|
||||
vod_remarks: `贝乐虎 | 播放:${item.viewcount || 0}`,
|
||||
vod_content: item.description || ''
|
||||
}));
|
||||
} catch (e) {}
|
||||
return videos;
|
||||
}
|
||||
|
||||
async function getTuxiaobeiList(typeId, page) {
|
||||
let videos = [];
|
||||
try {
|
||||
const url = `${rule.tuxiaobei.host}${rule.tuxiaobei.listApi}?typeId=${typeId}&page=${page}&callback=`;
|
||||
const html = await request(url, { headers });
|
||||
|
||||
const match = html.match(/\((.*?)\);/);
|
||||
if (!match) return videos;
|
||||
|
||||
const data = safeJSONParse(match[1]).data;
|
||||
const items = data.items || [];
|
||||
|
||||
videos = items.map(item => ({
|
||||
vod_id: `tuxiaobei@${item.video_id}`,
|
||||
vod_name: item.name || '未知视频',
|
||||
vod_pic: item.image || '',
|
||||
vod_remarks: `兔小贝 | ${item.root_category_name || ''} ${item.duration_string || ''}`,
|
||||
vod_content: item.description || ''
|
||||
}));
|
||||
} catch (e) {}
|
||||
return videos;
|
||||
}
|
||||
|
||||
async function getBeilehuDetail(url) {
|
||||
return {
|
||||
vod_id: url,
|
||||
vod_name: '贝乐虎视频',
|
||||
vod_remarks: '贝乐虎',
|
||||
vod_play_from: '贝乐虎',
|
||||
vod_play_url: `点击播放$${url}`
|
||||
};
|
||||
}
|
||||
|
||||
async function getTuxiaobeiDetail(id) {
|
||||
return {
|
||||
vod_id: id,
|
||||
vod_name: '兔小贝视频',
|
||||
vod_remarks: '兔小贝',
|
||||
vod_play_from: '兔小贝',
|
||||
vod_play_url: `点击播放$${rule.tuxiaobei.host}${rule.tuxiaobei.playUrl}${id}`
|
||||
};
|
||||
}
|
||||
|
||||
async function home(filter) {
|
||||
const platForms = getPlatList();
|
||||
const classes = platForms.map(item => ({ type_name: item.name, type_id: item.id }));
|
||||
const filters = {};
|
||||
platForms.forEach(item => { if (filterOptions[item.id]) filters[item.id] = filterOptions[item.id]; });
|
||||
return JSON.stringify({ class: classes, filters: filters });
|
||||
}
|
||||
|
||||
async function homeVod() {
|
||||
try {
|
||||
const platForms = getPlatList();
|
||||
const randomPlat = platForms[Math.floor(Math.random() * platForms.length)];
|
||||
const randomArea = ruleFilterDef[randomPlat.id]?.area || '';
|
||||
const categoryResult = await category(randomPlat.id, 1, { area: randomArea }, {});
|
||||
const categoryList = safeJSONParse(categoryResult).list || [];
|
||||
return JSON.stringify({ list: categoryList.slice(0, 12) });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ list: [] });
|
||||
}
|
||||
}
|
||||
|
||||
async function category(tid, pg, filter, extend) {
|
||||
const page = pg || 1;
|
||||
extend = extend || {};
|
||||
|
||||
const platformItem = platformList.find(p => p.id === tid);
|
||||
if (!platformItem) {
|
||||
return JSON.stringify({ list: [], page, pagecount: 1, limit: 0, total: 0 });
|
||||
}
|
||||
|
||||
const searchKeyword = extend?.custom;
|
||||
if (searchKeyword) {
|
||||
return await cfs(tid, searchKeyword, pg);
|
||||
}
|
||||
|
||||
const area = filter?.area || extend?.area || ruleFilterDef[tid]?.area || '';
|
||||
const videos = [];
|
||||
|
||||
try {
|
||||
switch (tid) {
|
||||
case 'beilehu':
|
||||
videos.push(...await getBeilehuList(area, page));
|
||||
break;
|
||||
case 'tuxiaobei':
|
||||
videos.push(...await getTuxiaobeiList(area, page));
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return JSON.stringify({
|
||||
list: videos,
|
||||
page: page,
|
||||
pagecount: page + 1,
|
||||
limit: videos.length,
|
||||
total: videos.length * (page + 1)
|
||||
});
|
||||
}
|
||||
|
||||
async function detail(id) {
|
||||
try {
|
||||
const parts = id.split('@');
|
||||
const platform = parts[0];
|
||||
const did = parts.slice(1).join('@');
|
||||
|
||||
let vod = {};
|
||||
if (platform === 'beilehu') {
|
||||
vod = await getBeilehuDetail(did);
|
||||
} else if (platform === 'tuxiaobei') {
|
||||
vod = await getTuxiaobeiDetail(did);
|
||||
}
|
||||
return JSON.stringify({ list: [vod] });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ list: [] });
|
||||
}
|
||||
}
|
||||
|
||||
async function play(flag, id, flags) {
|
||||
try {
|
||||
if (flag.includes('贝乐虎')) {
|
||||
return JSON.stringify({ parse: 0, url: id, header: headers });
|
||||
}
|
||||
|
||||
if (flag.includes('兔小贝')) {
|
||||
try {
|
||||
const html = await request(id, { headers });
|
||||
let videoUrl = '';
|
||||
|
||||
const srcMatch = html.match(/video-src=["']([^"']+)["']/);
|
||||
if (srcMatch) videoUrl = srcMatch[1];
|
||||
|
||||
if (!videoUrl) {
|
||||
const sourceMatch = html.match(/<source[^>]*src=["']([^"']+)["']/);
|
||||
if (sourceMatch) videoUrl = sourceMatch[1];
|
||||
}
|
||||
|
||||
if (!videoUrl) {
|
||||
const m3u8Match = html.match(/https?:\/\/[^"']+\.m3u8[^"']*/);
|
||||
if (m3u8Match) videoUrl = m3u8Match[0];
|
||||
}
|
||||
|
||||
if (!videoUrl) {
|
||||
return JSON.stringify({ parse: 0, url: id, msg: '未找到播放地址' });
|
||||
}
|
||||
|
||||
return JSON.stringify({ parse: 0, url: videoUrl, header: headers });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ parse: 0, url: id, msg: `播放失败: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ parse: 0, url: id });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ parse: 0, url: id, msg: `播放失败: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
async function cfs(siteId, wd, pg) {
|
||||
return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 });
|
||||
}
|
||||
|
||||
async function search(wd, quick, pg) {
|
||||
return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 });
|
||||
}
|
||||
|
||||
export function __jsEvalReturn() {
|
||||
return { init, home, homeVod, category, detail, play, search };
|
||||
}
|
||||
Reference in New Issue
Block a user