上传文件至「api」

This commit is contained in:
2026-08-13 18:03:00 +02:00
parent 818ef47618
commit 4f894c5cc8
4 changed files with 1299 additions and 0 deletions
+306
View File
@@ -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('&amp;', '&'))
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
View File
@@ -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
View File
@@ -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
+310
View File
@@ -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 };
}