Sync all projects
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
'''
|
||||
内置代理配置:真心jar为例
|
||||
{
|
||||
"key": "Phb",
|
||||
"name": "Phb",
|
||||
"type": 3,
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"api": "./py/Phb.py",
|
||||
"ext": {
|
||||
"http": "http://127.0.0.1:1072",
|
||||
"https": "http://127.0.0.1:1072"
|
||||
}
|
||||
},
|
||||
注:http(s)代理都是http
|
||||
'''
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
self.host = self.gethost()
|
||||
self.headers.update({'referer': f'{self.host}/', 'origin': self.host})
|
||||
self.session = Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"视频": "/video",
|
||||
"片单": "/playlists",
|
||||
"频道": "/channels",
|
||||
"分类": "/categories",
|
||||
"明星": "/pornstars"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq('/recommended')
|
||||
vhtml = data("#recommendedListings .pcVideoListItem .phimage")
|
||||
return {'list': self.getlist(vhtml)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid == '/video' or '_this_video' in tid:
|
||||
pagestr = f'&' if '?' in tid else f'?'
|
||||
tid = tid.split('_this_video')[0]
|
||||
data = self.getpq(f'{tid}{pagestr}page={pg}')
|
||||
vdata = self.getlist(data('#videoCategory .pcVideoListItem'))
|
||||
elif tid == '/playlists':
|
||||
data = self.getpq(f'{tid}?page={pg}')
|
||||
vhtml = data('#playListSection li')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'playlists_click_' + i('.thumbnail-info-wrapper .display-block a').attr('href'),
|
||||
'vod_name': i('.thumbnail-info-wrapper .display-block a').attr('title'),
|
||||
'vod_pic': self.proxy(i('.largeThumb').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.playlist-videos .number').text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/channels':
|
||||
data = self.getpq(f'{tid}?o=rk&page={pg}')
|
||||
vhtml = data('#filterChannelsSection li .description')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'director_click_' + i('.avatar a').attr('href'),
|
||||
'vod_name': i('.avatar img').attr('alt'),
|
||||
'vod_pic': self.proxy(i('.avatar img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.descriptionContainer ul li').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/categories' and pg == '1':
|
||||
result['pagecount'] = 1
|
||||
data = self.getpq(f'{tid}')
|
||||
vhtml = data('.categoriesListSection li .relativeWrapper')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': i('a').attr('href') + '_this_video',
|
||||
'vod_name': i('a').attr('alt'),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/pornstars':
|
||||
data = self.getpq(f'{tid}?o=t&page={pg}')
|
||||
vhtml = data('#popularPornstars .performerCard .wrap')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'pornstars_click_' + i('a').attr('href'),
|
||||
'vod_name': i('.performerCardName').text(),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_year': i('.performerVideosViewsCount span').eq(0).text(),
|
||||
'vod_remarks': i('.performerVideosViewsCount span').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif 'playlists_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
if pg == '1':
|
||||
hdata = self.getpq(tid)
|
||||
self.token = hdata('#searchInput').attr('data-token')
|
||||
vdata = self.getlist(hdata('#videoPlaylist .pcVideoListItem .phimage'))
|
||||
else:
|
||||
tid = tid.split('playlist/')[-1]
|
||||
data = self.getpq(f'/playlist/viewChunked?id={tid}&token={self.token}&page={pg}')
|
||||
vdata = self.getlist(data('.pcVideoListItem .phimage'))
|
||||
elif 'director_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#showAllChanelVideos .pcVideoListItem .phimage'))
|
||||
elif 'pornstars_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#mostRecentVideosSection .pcVideoListItem .phimage'))
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = f"{self.host}{ids[0]}"
|
||||
data = self.getpq(ids[0])
|
||||
vn = data('meta[property="og:title"]').attr('content')
|
||||
dtext = data('.userInfo .usernameWrap a')
|
||||
pdtitle = '[a=cr:' + json.dumps(
|
||||
{'id': 'director_click_' + dtext.attr('href'), 'name': dtext.text()}) + '/]' + dtext.text() + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director': pdtitle,
|
||||
'vod_remarks': (data('.userInfo').text() + ' / ' + data('.ratingInfo').text()).replace('\n', ' / '),
|
||||
'vod_play_from': '老僧酿酒',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
js_content = data("#player script").eq(0).text()
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{url}')}"]
|
||||
try:
|
||||
pattern = r'"mediaDefinitions":\s*(\[.*?\]),\s*"isVertical"'
|
||||
match = re.search(pattern, js_content, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
udata = json.loads(json_str)
|
||||
plist = [
|
||||
f"{media['height']}${self.e64(f'{0}@@@@{url}')}"
|
||||
for media in udata[:-1]
|
||||
if (url := media.get('videoUrl'))
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"提取mediaDefinitions失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(f'/video/search?search={key}&page={pg}')
|
||||
return {'list': self.getlist(data('#videoSearchResult .pcVideoListItem .phimage'))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = self.d64(id).split('@@@@')
|
||||
if '.m3u8' in ids[1]: ids[1] = self.proxy(ids[1], 'm3u8')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
url = self.d64(param.get('url'))
|
||||
if param.get('type') == 'm3u8':
|
||||
return self.m3Proxy(url)
|
||||
else:
|
||||
return self.tsProxy(url)
|
||||
|
||||
def m3Proxy(self, url):
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain = last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def tsProxy(self, url):
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers['Content-Type'], data.content]
|
||||
|
||||
def gethost(self):
|
||||
try:
|
||||
response = requests.get('https://www.pornhub.com', headers=self.headers, proxies=self.proxies,
|
||||
allow_redirects=False)
|
||||
return response.headers['Location'][:-1]
|
||||
except Exception as e:
|
||||
print(f"获取主页失败: {str(e)}")
|
||||
return "https://www.pornhub.com"
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
vlist.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': self.proxy(i('img').attr('src')),
|
||||
'vod_remarks': i('.bgShadeEffect').text() or i('.duration').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path):
|
||||
try:
|
||||
response = self.session.get(f'{self.host}{path}').text
|
||||
return pq(response.encode('utf-8'))
|
||||
except Exception as e:
|
||||
print(f"请求失败: , {str(e)}")
|
||||
return None
|
||||
|
||||
def proxy(self, data, type='img'):
|
||||
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:return data
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys,json,time,base64,random,string,hashlib
|
||||
from urllib.parse import urlencode,quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES,PKCS1_v1_5
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util.Padding import pad,unpad
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base_url = 'https://api-h5.uvod.tv'; self.web_url = 'https://www.uvod.tv'; self.token = ''; self._iv = b"abcdefghijklmnop"
|
||||
self._client_private = """-----BEGIN PRIVATE KEY-----
|
||||
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJ4FBai1Y6my4+fc
|
||||
8AD5tyYzxgN8Q7M/PuFv+8i1Xje8ElXYVwzvYd1y/cNxwgW4RX0tDy9ya562V33x
|
||||
6SyNr29DU6XytOeOlOkxt3gd5169K4iFaJ0l0wA4koMTcCAYVxC9B4+zzS5djYmF
|
||||
MuRGfYgKYNH99vfY7BZjdAY68ty5AgMBAAECgYB1rbvHJj5wVF7Rf4Hk2BMDCi9+
|
||||
zP4F8SW88Y6KrDbcPt1QvOonIea56jb9ZCxf4hkt3W6foRBwg86oZo2FtoZcpCJ+
|
||||
rFqUM2/wyV4CuzlL0+rNNSq7bga7d7UVld4hQYOCffSMifyF5rCFNH1py/4Dvswm
|
||||
pi5qljf+dPLSlxXl2QJBAMzPJ/QPAwcf5K5nngQtbZCD3nqDFpRixXH4aUAIZcDz
|
||||
S1RNsHrT61mEwZ/thQC2BUJTQNpGOfgh5Ecd1MnURwsCQQDFhAFfmvK7svkygoKX
|
||||
t55ARNZy9nmme0StMOfdb4Q2UdJjfw8+zQNtKFOM7VhB7ijHcfFuGsE7UeXBe20n
|
||||
g/XLAkEAv9SoT2hgJaQxxUk4MCF8pgddstJlq8Z3uTA7JMa4x+kZfXTm/6TOo6I8
|
||||
2VbXZLsYYe8op0lvsoHMFvBSBljV0QJBAKhxyoYRa98dZB5qZRskciaXTlge0WJk
|
||||
kA4vvh3/o757izRlQMgrKTfng1GVfIZFqKtnBiIDWTXQw2N9cnqXtH8CQAx+CD5t
|
||||
l1iT0cMdjvlMg2two3SnpOjpo7gALgumIDHAmsVWhocLtcrnJI032VQSUkNnLq9z
|
||||
EIfmHDz0TPTNHBQ=
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
self._client_public = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeBQWotWOpsuPn3PAA+bcmM8YD
|
||||
fEOzPz7hb/vItV43vBJV2FcM72Hdcv3DccIFuEV9LQ8vcmuetld98eksja9vQ1Ol
|
||||
8rTnjpTpMbd4HedevSuIhWidJdMAOJKDE3AgGFcQvQePs80uXY2JhTLkRn2ICmDR
|
||||
/fb32OwWY3QGOvLcuQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
self._server_public = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeBQWotWOpsuPn3PAA+bcmM8YD
|
||||
fEOzPz7hb/vItV43vBJV2FcM72Hdcv3DccIFuEV9LQ8vcmuetld98eksja9vQ1Ol
|
||||
8rTnjpTpMbd4HedevSuIhWidJdMAOJKDE3AgGFcQvQePs80uXY2JhTLkRn2ICmDR
|
||||
/fb32OwWY3QGOvLcuQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
def getName(self): return "UVOD"
|
||||
|
||||
def init(self, extend=""):
|
||||
try: cfg = json.loads(extend) if isinstance(extend, str) and extend.strip().startswith('{') else extend if isinstance(extend, dict) else {}
|
||||
except Exception: cfg = {}
|
||||
self.base_url = cfg.get('base_url', self.base_url); self.token = cfg.get('token', self.token)
|
||||
return self.homeContent(False)
|
||||
|
||||
def isVideoFormat(self, url): return any(x in url.lower() for x in ['.m3u8', '.mp4']) if url else False
|
||||
def manualVideoCheck(self): return False
|
||||
def destroy(self): pass
|
||||
|
||||
def _random_key(self, n=32):
|
||||
chars = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(n))
|
||||
|
||||
def _encrypt(self, plain_text: str) -> str:
|
||||
aes_key = self._random_key(32).encode('utf-8')
|
||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv=self._iv)
|
||||
ct_b64 = base64.b64encode(cipher.encrypt(pad(plain_text.encode('utf-8'), AES.block_size))).decode('utf-8')
|
||||
rsa_pub = RSA.import_key(self._server_public); rsa_cipher = PKCS1_v1_5.new(rsa_pub)
|
||||
rsa_b64 = base64.b64encode(rsa_cipher.encrypt(aes_key)).decode('utf-8')
|
||||
return f"{ct_b64}.{rsa_b64}"
|
||||
|
||||
def _decrypt(self, enc_text: str) -> str:
|
||||
try:
|
||||
parts = enc_text.split('.'); ct_b64, rsa_b64 = parts
|
||||
rsa_priv = RSA.import_key(self._client_private)
|
||||
aes_key = PKCS1_v1_5.new(rsa_priv).decrypt(base64.b64decode(rsa_b64), None)
|
||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv=self._iv)
|
||||
pt = unpad(cipher.decrypt(base64.b64decode(ct_b64)), AES.block_size)
|
||||
return pt.decode('utf-8', 'ignore')
|
||||
except Exception: return enc_text
|
||||
|
||||
def _build_headers(self, path: str, payload: dict):
|
||||
ts = str(int(time.time() * 1000)); token = self.token or ''
|
||||
if path == '/video/latest':
|
||||
parent_id = payload.get('parent_category_id', 101); text = f"-parent_category_id={parent_id}-{ts}"
|
||||
elif path == '/video/list':
|
||||
keyword = payload.get('keyword')
|
||||
if keyword: keyword = quote(str(keyword), safe='').lower(); text = f"-keyword={keyword}&need_fragment=1&page=1&pagesize=42&sort_type=asc-{ts}"
|
||||
else: page = payload.get('page', 1); pagesize = payload.get('pagesize', 42); parent_id = payload.get('parent_category_id', ''); text = f"-page={page}&pagesize={pagesize}&parent_category_id={parent_id}&sort_type=asc-{ts}"
|
||||
elif path == '/video/info': text = f"-id={payload.get('id', '')}-{ts}"
|
||||
elif path == '/video/source': quality = payload.get('quality', ''); fragment_id = payload.get('video_fragment_id', ''); video_id = payload.get('video_id', ''); text = f"-quality={quality}&video_fragment_id={fragment_id}&video_id={video_id}-{ts}"
|
||||
else: filtered = {k: v for k, v in (payload or {}).items() if v not in (0, '0', '', False, None)}; query = urlencode(sorted(filtered.items()), doseq=True).lower(); text = f"{token}-{query}-{ts}"
|
||||
sig = hashlib.md5(text.encode('utf-8')).hexdigest()
|
||||
return {'Content-Type': 'application/json', 'X-TOKEN': token, 'X-TIMESTAMP': ts, 'X-SIGNATURE': sig, 'Origin': self.web_url, 'Referer': self.web_url + '/', 'Accept': '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'}
|
||||
|
||||
def _post_api(self, path: str, payload: dict):
|
||||
url = self.base_url.rstrip('/') + path
|
||||
try:
|
||||
body = self._encrypt(json.dumps(payload, ensure_ascii=False)); headers = self._build_headers(path, payload)
|
||||
rsp = self.post(url, data=body, headers=headers, timeout=15)
|
||||
if rsp.status_code != 200 or not rsp.text: return None
|
||||
txt = rsp.text.strip(); obj = None
|
||||
try: dec = self._decrypt(txt); obj = json.loads(dec)
|
||||
except:
|
||||
try: obj = json.loads(txt)
|
||||
except: pass
|
||||
if isinstance(obj, dict) and obj.get('error') == 0: return obj.get('data')
|
||||
return None
|
||||
except Exception: return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self._post_api('/video/category', {}); lst = (data.get('list') or data.get('category') or []) if isinstance(data, dict) else (data or []); classes = []
|
||||
for it in lst:
|
||||
cid = it.get('id') or it.get('category_id') or it.get('value'); name = it.get('name') or it.get('label') or it.get('title')
|
||||
if cid and name: classes.append({'type_name': str(name), 'type_id': str(cid)})
|
||||
if not classes: classes = [{'type_name': '电影', 'type_id': '100'}, {'type_name': '电视剧', 'type_id': '101'}, {'type_name': '综艺', 'type_id': '102'}, {'type_name': '动漫', 'type_id': '103'}, {'type_name': '体育', 'type_id': '104'}, {'type_name': '纪录片', 'type_id': '105'}, {'type_name': '粤台专区', 'type_id': '106'},
|
||||
{'type_name': '儿童', 'type_id': '107'},
|
||||
{'type_name': '七哥', 'type_id': '108'}]
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self._post_api('/video/latest', {'parent_category_id': 101})
|
||||
if isinstance(data, dict): lst = data.get('video_latest_list') or data.get('list') or data.get('rows') or data.get('items') or []
|
||||
elif isinstance(data, list): lst = data
|
||||
else: lst = []
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or k.get('remarks') or ''})
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page = int(pg) if str(pg).isdigit() else 1
|
||||
payload = {'parent_category_id': str(tid), 'category_id': None, 'language': None, 'year': None, 'region': None, 'state': None, 'keyword': '', 'paid': None, 'page': page, 'pagesize': 42, 'sort_field': '', 'sort_type': 'asc'}
|
||||
if isinstance(extend, dict):
|
||||
for k in ['category_id', 'year', 'region', 'state', 'keyword']:
|
||||
if extend.get(k): payload[k] = extend[k]
|
||||
data = self._post_api('/video/list', payload)
|
||||
if isinstance(data, dict): lst = data.get('video_list') or data.get('list') or data.get('rows') or data.get('items') or []; total = data.get('total', 999999)
|
||||
elif isinstance(data, list): lst = data; total = 999999
|
||||
else: lst, total = [], 0
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or ''})
|
||||
return {'list': videos, 'page': page, 'pagecount': 9999, 'limit': 24, 'total': total}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]; data = self._post_api('/video/info', {'id': vid}) or {}; video_info = data.get('video', {}) if isinstance(data, dict) else {}; fragments = data.get('video_fragment_list', []) if isinstance(data, dict) else []; play_urls = []
|
||||
if fragments:
|
||||
for fragment in fragments:
|
||||
name = fragment.get('symbol', '播放'); fragment_id = fragment.get('id', ''); qualities = fragment.get('qualities', [])
|
||||
if fragment_id and qualities:
|
||||
|
||||
max_quality = max(qualities) if qualities else 4
|
||||
play_urls.append(f"{name}${vid}|{fragment_id}|[{max_quality}]")
|
||||
if not play_urls: play_urls.append(f"播放${vid}")
|
||||
vod = {'vod_id': str(vid), 'vod_name': video_info.get('title') or video_info.get('name') or '', 'vod_pic': video_info.get('poster') or video_info.get('cover') or video_info.get('pic') or '', 'vod_year': video_info.get('year') or '', 'vod_remarks': video_info.get('duration') or '', 'vod_content': video_info.get('description') or video_info.get('desc') or '', 'vod_play_from': '优汁🍑源', 'vod_play_url': '#'.join(play_urls) + '$$$'}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = int(pg) if str(pg).isdigit() else 1
|
||||
payload = {'parent_category_id': None, 'category_id': None, 'language': None, 'year': None, 'region': None, 'state': None, 'keyword': key, 'paid': None, 'page': page, 'pagesize': 42, 'sort_field': '', 'sort_type': 'asc', 'need_fragment': 1}
|
||||
data = self._post_api('/video/list', payload)
|
||||
if isinstance(data, dict): lst = data.get('video_list') or data.get('list') or data.get('rows') or data.get('items') or []
|
||||
elif isinstance(data, list): lst = data
|
||||
else: lst = []
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or ''})
|
||||
return {'list': videos}
|
||||
|
||||
def _extract_first_media(self, obj):
|
||||
if not obj: return None
|
||||
if isinstance(obj, str): s = obj.strip(); return s if self.isVideoFormat(s) else None
|
||||
if isinstance(obj, (dict, list)):
|
||||
for v in (obj.values() if isinstance(obj, dict) else obj):
|
||||
r = self._extract_first_media(v)
|
||||
if r: return r
|
||||
return None
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parts = id.split('|'); video_id = parts[0]
|
||||
if len(parts) >= 3:
|
||||
fragment_id = parts[1]; qualities_str = parts[2].strip('[]').replace(' ', ''); qualities = [q.strip() for q in qualities_str.split(',') if q.strip()]; quality = qualities[0] if qualities else '4'
|
||||
payload = {'video_id': video_id, 'video_fragment_id': int(fragment_id) if str(fragment_id).isdigit() else fragment_id, 'quality': int(quality) if str(quality).isdigit() else quality, 'seek': None}
|
||||
else: payload = {'video_id': video_id, 'video_fragment_id': 1, 'quality': 4, 'seek': None}
|
||||
data = self._post_api('/video/source', payload) or {}
|
||||
url = (data.get('video', {}).get('url', '') or data.get('url') or data.get('playUrl') or data.get('play_url') or self._extract_first_media(data) or '')
|
||||
if not url: return {'parse': 1, 'url': id}
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', 'Referer': self.web_url + '/', 'Origin': self.web_url}
|
||||
return {'parse': 0, 'url': url, 'header': headers}
|
||||
|
||||
def localProxy(self, param): return None
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from urllib.parse import quote
|
||||
from Crypto.Hash import MD5
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.cookies.update(self.cookie)
|
||||
self.get_ctoken()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.youku.com'
|
||||
|
||||
shost='https://search.youku.com'
|
||||
|
||||
h5host='https://acs.youku.com'
|
||||
|
||||
ihost='https://v.youku.com'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)',
|
||||
'Referer': f'{host}/'
|
||||
}
|
||||
|
||||
cookie={
|
||||
"__ysuid": "17416134165380iB",
|
||||
"__aysid": "1741613416541WbD",
|
||||
"xlly_s": "1",
|
||||
"isI18n": "false",
|
||||
"cna": "bNdVIKmmsHgCAXW9W6yrQ1/s",
|
||||
"__ayft": "1741672162330",
|
||||
"__arpvid": "1741672162331FBKgrn-1741672162342",
|
||||
"__ayscnt": "1",
|
||||
"__aypstp": "1",
|
||||
"__ayspstp": "3",
|
||||
"tfstk": "gZbiib4JpG-6DqW-B98_2rwPuFrd1fTXQt3vHEp4YpJIBA3OgrWcwOi90RTOo9XVQ5tAM5NcK_CP6Ep97K2ce1XDc59v3KXAgGFLyzC11ET2n8U8yoyib67M3xL25e8gS8pbyzC1_ET4e8URWTsSnHv2uh8VTeJBgEuN3d-ELQAWuKWV36PHGpJ2uEWVTxvicLX1ewyUXYSekxMf-CxMEqpnoqVvshvP_pABOwvXjL5wKqeulm52np_zpkfCDGW9Ot4uKFIRwZtP7vP9_gfAr3KEpDWXSIfWRay-DHIc_Z-hAzkD1i5Ooi5LZ0O5YO_1mUc476YMI3R6xzucUnRlNe_zemKdm172xMwr2L7CTgIkbvndhFAVh3_YFV9Ng__52U4SQKIdZZjc4diE4EUxlFrfKmiXbBOHeP72v7sAahuTtWm78hRB1yV3tmg9bBOEhWVnq5KwOBL5."
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
categories = ["电视剧", "电影", "综艺", "动漫", "少儿", "纪录片", "文化", "亲子", "教育", "搞笑", "生活",
|
||||
"体育", "音乐", "游戏"]
|
||||
classes = [{'type_name': category, 'type_id': category} for category in categories]
|
||||
filters = {}
|
||||
self.typeid = {}
|
||||
with ThreadPoolExecutor(max_workers=len(categories)) as executor:
|
||||
tasks = {
|
||||
executor.submit(self.cf, {'type': category}, True): category
|
||||
for category in categories
|
||||
}
|
||||
|
||||
for future in as_completed(tasks):
|
||||
try:
|
||||
category = tasks[future]
|
||||
session, ft = future.result()
|
||||
filters[category] = ft
|
||||
self.typeid[category] = session
|
||||
except Exception as e:
|
||||
print(f"处理分类 {tasks[future]} 时出错: {str(e)}")
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
vlist = []
|
||||
params={"ms_codes":"2019061000","params":"{\"debug\":0,\"gray\":0,\"pageNo\":1,\"utdid\":\"ZYmGMAAAACkDAMU8hbiMmYdd\",\"userId\":\"\",\"bizKey\":\"YOUKU_WEB\",\"appPackageKey\":\"com.youku.YouKu\",\"showNodeList\":0,\"reqSubNode\":0,\"nodeKey\":\"WEBHOME\",\"bizContext\":\"{\\\"spmA\\\":\\\"a2hja\\\"}\"}","system_info":"{\"device\":\"pcweb\",\"os\":\"pcweb\",\"ver\":\"1.0.0.0\",\"userAgent\":\"Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)\",\"guid\":\"1590141704165YXe\",\"appPackageKey\":\"com.youku.pcweb\",\"young\":0,\"brand\":\"\",\"network\":\"\",\"ouid\":\"\",\"idfa\":\"\",\"scale\":\"\",\"operator\":\"\",\"resolution\":\"\",\"pid\":\"\",\"childGender\":0,\"zx\":0}"}
|
||||
data=self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.home.query/1.0/',params)
|
||||
okey=list(data['data'].keys())[0]
|
||||
for i in data['data'][okey]['data']['nodes'][0]['nodes'][-1]['nodes'][0]['nodes']:
|
||||
if i.get('nodes') and i['nodes'][0].get('data'):
|
||||
i=i['nodes'][0]['data']
|
||||
if i.get('assignId'):
|
||||
vlist.append({
|
||||
'vod_id': i['assignId'],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('vImg') or i.get('img'),
|
||||
'vod_year': i.get('mark',{}).get('data',{}).get('text'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
return {'list': vlist}
|
||||
except Exception as e:
|
||||
print(f"处理主页视频数据时出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
vlist = []
|
||||
result['page'] = pg
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
pagecount = 9999
|
||||
params = {'type': tid}
|
||||
id = self.typeid[tid]
|
||||
params.update(extend)
|
||||
if pg == '1':
|
||||
id=self.cf(params)
|
||||
data=self.session.get(f'{self.host}/category/data?session={id}¶ms={quote(json.dumps(params))}&pageNo={pg}').json()
|
||||
try:
|
||||
data=data['data']['filterData']
|
||||
for i in data['listData']:
|
||||
if i.get('videoLink') and 's=' in i['videoLink']:
|
||||
vlist.append({
|
||||
'vod_id': i.get('videoLink').split('s=')[-1],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('img'),
|
||||
'vod_year': i.get('rightTagText'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
self.typeid[tid]=quote(json.dumps(data['session']))
|
||||
except:
|
||||
pagecount=pg
|
||||
result['list'] = vlist
|
||||
result['pagecount'] = pagecount
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
data=self.session.get(f'{self.ihost}/v_getvideo_info/?showId={ids[0]}').json()
|
||||
v=data['data']
|
||||
vod = {
|
||||
'type_name': v.get('showVideotype'),
|
||||
'vod_year': v.get('lastUpdate'),
|
||||
'vod_remarks': v.get('rc_title'),
|
||||
'vod_actor': v.get('_personNameStr'),
|
||||
'vod_content': v.get('showdesc'),
|
||||
'vod_play_from': '优酷',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
params={"biz":"new_detail_web2","videoId":v.get('vid'),"scene":"web_page","componentVersion":"3","ip":data.get('ip'),"debug":0,"utdid":"ZYmGMAAAACkDAMU8hbiMmYdd","userId":0,"platform":"pc","nextSession":"","gray":0,"source":"pcNoPrev","showId":ids[0]}
|
||||
sdata,index=self.getinfo(params)
|
||||
pdata=sdata['nodes']
|
||||
if index > len(pdata):
|
||||
batch_size = len(pdata)
|
||||
total_batches = ((index + batch_size - 1) // batch_size) - 1
|
||||
ssj = json.loads(sdata['data']['session'])
|
||||
with ThreadPoolExecutor(max_workers=total_batches) as executor:
|
||||
futures = []
|
||||
for batch in range(total_batches):
|
||||
start = batch_size + 1 + (batch * batch_size)
|
||||
end = start + batch_size - 1
|
||||
next_session = ssj.copy()
|
||||
next_session.update({
|
||||
"itemStartStage": start,
|
||||
"itemEndStage": min(end, index)
|
||||
})
|
||||
current_params = params.copy()
|
||||
current_params['nextSession'] = json.dumps(next_session)
|
||||
futures.append((start, executor.submit(self.getvinfo, current_params)))
|
||||
futures.sort(key=lambda x: x[0])
|
||||
|
||||
for _, future in futures:
|
||||
try:
|
||||
result = future.result()
|
||||
pdata.extend(result['nodes'])
|
||||
except Exception as e:
|
||||
print(f"Error fetching data: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join([f"{i['data'].get('title')}${i['data']['action'].get('value')}" for i in pdata])
|
||||
return {'list': [vod]}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {'list': [{'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'呜呜呜${self.host}'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.session.get(f'{self.shost}/api/search?pg={pg}&keyword={key}').json()
|
||||
vlist = []
|
||||
for i in data['pageComponentList']:
|
||||
if i.get('commonData') and (i['commonData'].get('showId') or i['commonData'].get('realShowId')):
|
||||
i=i['commonData']
|
||||
vlist.append({
|
||||
'vod_id': i.get('showId') or i.get('realShowId'),
|
||||
'vod_name': i['titleDTO'].get('displayName'),
|
||||
'vod_pic': i['posterDTO'].get('vThumbUrl'),
|
||||
'vod_year': i.get('feature'),
|
||||
'vod_remarks': i.get('updateNotice')
|
||||
})
|
||||
return {'list': vlist, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'jx':1,'parse': 1, 'url': f"{self.ihost}/video?vid={id}", 'header': ''}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def cf(self,params,b=False):
|
||||
response = self.session.get(f'{self.host}/category/data?params={quote(json.dumps(params))}&optionRefresh=1&pageNo=1').json()
|
||||
data=response['data']['filterData']
|
||||
session=quote(json.dumps(data['session']))
|
||||
if b:
|
||||
return session,self.get_filter_data(data['filter']['filterData'][1:])
|
||||
return session
|
||||
|
||||
def process_key(self, key):
|
||||
if '_' not in key:
|
||||
return key
|
||||
parts = key.split('_')
|
||||
result = parts[0]
|
||||
for part in parts[1:]:
|
||||
if part:
|
||||
result += part[0].upper() + part[1:]
|
||||
return result
|
||||
|
||||
def get_filter_data(self, data):
|
||||
result = []
|
||||
try:
|
||||
for item in data:
|
||||
if not item.get('subFilter'):
|
||||
continue
|
||||
first_sub = item['subFilter'][0]
|
||||
if not first_sub.get('filterType'):
|
||||
continue
|
||||
filter_item = {
|
||||
'key': self.process_key(first_sub['filterType']),
|
||||
'name': first_sub['title'],
|
||||
'value': []
|
||||
}
|
||||
for sub in item['subFilter']:
|
||||
if 'value' in sub:
|
||||
filter_item['value'].append({
|
||||
'n': sub['title'],
|
||||
'v': sub['value']
|
||||
})
|
||||
if filter_item['value']:
|
||||
result.append(filter_item)
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理筛选数据时出错: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
def get_ctoken(self):
|
||||
data=self.session.get(f'{self.h5host}/h5/mtop.ykrec.recommendservice.recommend/1.0/?jsv=2.6.1&appKey=24679788')
|
||||
|
||||
def md5(self,t,text):
|
||||
h = MD5.new()
|
||||
token=self.session.cookies.get('_m_h5_tk').split('_')[0]
|
||||
data=f"{token}&{t}&24679788&{text}"
|
||||
h.update(data.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getdata(self, url, params, recursion_count=0, max_recursion=3):
|
||||
data = json.dumps(params)
|
||||
t = int(time.time() * 1000)
|
||||
jsdata = {
|
||||
'appKey': '24679788',
|
||||
't': t,
|
||||
'sign': self.md5(t, data),
|
||||
'data': data
|
||||
}
|
||||
response = self.session.get(url, params=jsdata)
|
||||
if '令牌过期' in response.text:
|
||||
if recursion_count >= max_recursion:
|
||||
raise Exception("达到最大递归次数,无法继续请求")
|
||||
self.get_ctoken()
|
||||
return self.getdata(url, params, recursion_count + 1, max_recursion)
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
def getvinfo(self,params):
|
||||
body = {
|
||||
"ms_codes": "2019030100",
|
||||
"params": json.dumps(params),
|
||||
"system_info": "{\"os\":\"iku\",\"device\":\"iku\",\"ver\":\"9.2.9\",\"appPackageKey\":\"com.youku.iku\",\"appPackageId\":\"pcweb\"}"
|
||||
}
|
||||
data = self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.gateway.new.execute/1.0/', body)
|
||||
okey = list(data['data'].keys())[0]
|
||||
i = data['data'][okey]['data']
|
||||
return i
|
||||
|
||||
def getinfo(self,params):
|
||||
i = self.getvinfo(params)
|
||||
jdata=i['nodes'][0]['nodes'][3]
|
||||
info=i['data']['extra']['episodeTotal']
|
||||
if i['data']['extra']['showCategory'] in ['电影','游戏']:
|
||||
jdata = i['nodes'][0]['nodes'][4]
|
||||
return jdata,info
|
||||
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from base64 import b64decode, b64encode
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import SHA256, MD5
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Util.Padding import unpad
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host, self.appKey, self.rsakey = self.userinfo()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.fetch(f"{self.host}/api.php/zjv6.vod/types", headers=self.getheader()).json()
|
||||
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序", }
|
||||
filters = {}
|
||||
classes = []
|
||||
json_data = data['data']['list']
|
||||
for item in json_data:
|
||||
has_non_empty_field = False
|
||||
jsontype_extend = item["type_extend"]
|
||||
jsontype_extend['by'] = '按更新,按播放,按评分,按收藏'
|
||||
classes.append({"type_name": item["type_name"], "type_id": item["type_id"]})
|
||||
for key in dy:
|
||||
if key in jsontype_extend and jsontype_extend[key].strip() != "":
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["type_id"])] = []
|
||||
for dkey in jsontype_extend:
|
||||
if dkey in dy and jsontype_extend[dkey].strip() != "":
|
||||
values = jsontype_extend[dkey].split(",")
|
||||
sl = {'按更新': 'time', '按播放': 'hits', '按评分': 'score', '按收藏': 'store_num'}
|
||||
value_array = [
|
||||
{"n": value.strip(), "v": sl[value.strip()] if dkey == "by" else value.strip()}
|
||||
for value in values
|
||||
if value.strip() != ""
|
||||
]
|
||||
filters[str(item["type_id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
result = {"class": classes, "filters": filters}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.fetch(f"{self.host}/api.php/zjv6.vod/vodPhbAll", headers=self.getheader()).json()
|
||||
return {'list': data['data']['list'][0]['vod_list']}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {
|
||||
"type": tid,
|
||||
"class": extend.get('class', ''),
|
||||
"lang": extend.get('lang', ''),
|
||||
"area": extend.get('area', ''),
|
||||
"year": extend.get('year', ''),
|
||||
"by": extend.get('by', ''),
|
||||
"page": pg,
|
||||
"limit": "12"
|
||||
}
|
||||
data = self.fetch(f"{self.host}/api.php/zjv6.vod", headers=self.getheader(), params=params).json()
|
||||
result = {}
|
||||
result['list'] = data['data']['list']
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.fetch(f"{self.host}/api.php/zjv6.vod/detail?vod_id={ids[0]}&rel_limit=10",
|
||||
headers=self.getheader()).json()
|
||||
vod = data['data']
|
||||
v, np = {'vod_play_from': [], 'vod_play_url': []}, {}
|
||||
for i in vod['vod_play_list']:
|
||||
n = i['player_info']['show']
|
||||
np[n] = []
|
||||
for j in i['urls']:
|
||||
j['parse'] = i['player_info']['parse2']
|
||||
nm = j.pop('name')
|
||||
np[n].append(f"{nm}${self.e64(json.dumps(j))}")
|
||||
for key, value in np.items():
|
||||
v['vod_play_from'].append(key)
|
||||
v['vod_play_url'].append('#'.join(value))
|
||||
v['vod_play_from'] = '$$$'.join(v['vod_play_from'])
|
||||
v['vod_play_url'] = '$$$'.join(v['vod_play_url'])
|
||||
vod.update(v)
|
||||
vod.pop('vod_play_list', None)
|
||||
vod.pop('type', None)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(f"{self.host}/api.php/zjv6.vod?page={pg}&limit=20&wd={key}", headers=self.getheader()).json()
|
||||
return {'list': data['data']['list'], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = json.loads(self.d64(id))
|
||||
target_url = ids['url']
|
||||
try:
|
||||
parse_str = ids.get('parse', '')
|
||||
if parse_str:
|
||||
parse_urls = parse_str.split(',')
|
||||
result_url = self.try_all_parses(parse_urls, target_url)
|
||||
if result_url:
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': result_url,
|
||||
'header': {'User-Agent': 'dart:io'}
|
||||
}
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': target_url,
|
||||
'header': {'User-Agent': 'dart:io'}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': target_url,
|
||||
'header': {'User-Agent': 'dart:io'}
|
||||
}
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def userinfo(self):
|
||||
t = str(int(time.time() * 1000))
|
||||
uid = self.generate_uid()
|
||||
sign = self.md5(f"appKey=3bbf7348cf314874883a18d6b6fcf67a&uid={uid}&time={t}")
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36',
|
||||
'Connection': 'Keep-Alive',
|
||||
'appKey': '3bbf7348cf314874883a18d6b6fcf67a',
|
||||
'uid': uid,
|
||||
'time': t,
|
||||
'sign': sign,
|
||||
}
|
||||
|
||||
params = {
|
||||
'access_token': '74d5879931b9774be10dee3d8c51008e',
|
||||
}
|
||||
|
||||
response = self.fetch('https://gitee.com/api/v5/repos/aycapp/openapi/contents/wawaconf.txt', params=params,
|
||||
headers=headers).json()
|
||||
data = json.loads(self.decrypt(response['content']))
|
||||
return data['baseUrl'], data['appKey'], data['appSecret']
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def generate_uid(self):
|
||||
return uuid.uuid4().hex
|
||||
|
||||
def getheader(self):
|
||||
t = str(int(time.time() * 1000))
|
||||
uid = self.generate_uid()
|
||||
sign = self.sign_message(f"appKey={self.appKey}&time={t}&uid={uid}")
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/4.9.3',
|
||||
'Connection': 'Keep-Alive',
|
||||
'uid': uid,
|
||||
'time': t,
|
||||
'appKey': self.appKey,
|
||||
'sign': sign,
|
||||
}
|
||||
return headers
|
||||
|
||||
def decrypt(self, encrypted_data):
|
||||
key = b64decode('Crm4FXWkk5JItpYirFDpqg==')
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
encrypted = bytes.fromhex(self.d64(encrypted_data))
|
||||
decrypted = cipher.decrypt(encrypted)
|
||||
unpadded = unpad(decrypted, AES.block_size)
|
||||
return unpadded.decode('utf-8')
|
||||
|
||||
def sign_message(self, message):
|
||||
private_key_str = f"-----BEGIN PRIVATE KEY-----\n{self.rsakey}\n-----END PRIVATE KEY-----"
|
||||
private_key = RSA.import_key(private_key_str)
|
||||
message_hash = SHA256.new(message.encode('utf-8'))
|
||||
signature = pkcs1_15.new(private_key).sign(message_hash)
|
||||
signature_b64 = b64encode(signature).decode('utf-8')
|
||||
return signature_b64
|
||||
|
||||
def fetch_url(self, parse_url, target_url):
|
||||
try:
|
||||
response = self.fetch(f"{parse_url.replace('..', '.')}{target_url}",
|
||||
headers={"user-agent": "okhttp/4.1.0/luob.app"}, timeout=5)
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
result_url = data.get('url') or data.get('data', {}).get('url')
|
||||
if result_url:
|
||||
return result_url
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def try_all_parses(self, parse_urls, target_url):
|
||||
with ThreadPoolExecutor(max_workers=(len(parse_urls))) as executor:
|
||||
future_to_url = {
|
||||
executor.submit(self.fetch_url, parse_url.strip(), target_url): parse_url
|
||||
for parse_url in parse_urls if parse_url.strip()
|
||||
}
|
||||
|
||||
for future in as_completed(future_to_url):
|
||||
try:
|
||||
result = future.result()
|
||||
if result:
|
||||
return result
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
@@ -0,0 +1,413 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import re
|
||||
import requests
|
||||
from lxml import etree
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "香蕉视频"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://618013.xyz"
|
||||
self.api_host = "https://h5.xxoo168.org"
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': self.host
|
||||
}
|
||||
self.log(f"香蕉视频爬虫初始化完成,主站: {self.host}")
|
||||
|
||||
def html(self, content):
|
||||
"""将HTML内容转换为可查询的对象"""
|
||||
try:
|
||||
return etree.HTML(content)
|
||||
except:
|
||||
self.log("HTML解析失败")
|
||||
return None
|
||||
|
||||
def regStr(self, pattern, string, index=1):
|
||||
"""正则表达式提取字符串"""
|
||||
try:
|
||||
match = re.search(pattern, string, re.IGNORECASE)
|
||||
if match and len(match.groups()) >= index:
|
||||
return match.group(index)
|
||||
except:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类"""
|
||||
result = {}
|
||||
# 只保留指定的分类
|
||||
classes = [
|
||||
{'type_id': '618013.xyz_1', 'type_name': '全部视频'},
|
||||
{'type_id': '618013.xyz_13', 'type_name': '香蕉精品'},
|
||||
{'type_id': '618013.xyz_22', 'type_name': '制服诱惑'},
|
||||
{'type_id': '618013.xyz_6', 'type_name': '国产视频'},
|
||||
{'type_id': '618013.xyz_8', 'type_name': '清纯少女'},
|
||||
{'type_id': '618013.xyz_9', 'type_name': '辣妹大奶'},
|
||||
{'type_id': '618013.xyz_10', 'type_name': '女同专属'},
|
||||
{'type_id': '618013.xyz_11', 'type_name': '素人出演'},
|
||||
{'type_id': '618013.xyz_12', 'type_name': '角色扮演'},
|
||||
{'type_id': '618013.xyz_20', 'type_name': '人妻熟女'},
|
||||
{'type_id': '618013.xyz_23', 'type_name': '日韩剧情'},
|
||||
{'type_id': '618013.xyz_21', 'type_name': '经典伦理'},
|
||||
{'type_id': '618013.xyz_7', 'type_name': '成人动漫'},
|
||||
{'type_id': '618013.xyz_14', 'type_name': '精品二区'},
|
||||
{'type_id': '618013.xyz_40', 'type_name': '精品三区'},
|
||||
{'type_id': '618013.xyz_53', 'type_name': '动漫中字'},
|
||||
{'type_id': '618013.xyz_52', 'type_name': '日本无码'},
|
||||
{'type_id': '618013.xyz_33', 'type_name': '中文字幕'},
|
||||
{'type_id': '618013.xyz_44', 'type_name': '国产传媒'},
|
||||
{'type_id': '618013.xyz_32', 'type_name': '国产自拍'}
|
||||
]
|
||||
result['class'] = classes
|
||||
try:
|
||||
rsp = self.fetch(self.host, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=20)
|
||||
result['list'] = videos
|
||||
except Exception as e:
|
||||
self.log(f"首页获取出错: {str(e)}")
|
||||
result['list'] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""分类定义 - 兼容性方法"""
|
||||
return {
|
||||
'class': [
|
||||
{'type_id': '618013.xyz_1', 'type_name': '全部视频'},
|
||||
{'type_id': '618013.xyz_13', 'type_name': '香蕉精品'},
|
||||
{'type_id': '618013.xyz_22', 'type_name': '制服诱惑'},
|
||||
{'type_id': '618013.xyz_6', 'type_name': '国产视频'},
|
||||
{'type_id': '618013.xyz_8', 'type_name': '清纯少女'},
|
||||
{'type_id': '618013.xyz_9', 'type_name': '辣妹大奶'},
|
||||
{'type_id': '618013.xyz_10', 'type_name': '女同专属'},
|
||||
{'type_id': '618013.xyz_11', 'type_name': '素人出演'},
|
||||
{'type_id': '618013.xyz_12', 'type_name': '角色扮演'},
|
||||
{'type_id': '618013.xyz_20', 'type_name': '人妻熟女'},
|
||||
{'type_id': '618013.xyz_23', 'type_name': '日韩剧情'},
|
||||
{'type_id': '618013.xyz_21', 'type_name': '经典伦理'},
|
||||
{'type_id': '618013.xyz_7', 'type_name': '成人动漫'},
|
||||
{'type_id': '618013.xyz_14', 'type_name': '精品二区'},
|
||||
{'type_id': '618013.xyz_40', 'type_name': '精品三区'},
|
||||
{'type_id': '618013.xyz_53', 'type_name': '动漫中字'},
|
||||
{'type_id': '618013.xyz_52', 'type_name': '日本无码'},
|
||||
{'type_id': '618013.xyz_33', 'type_name': '中文字幕'},
|
||||
{'type_id': '618013.xyz_44', 'type_name': '国产传媒'},
|
||||
{'type_id': '618013.xyz_32', 'type_name': '国产自拍'}
|
||||
]
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容 - 修改为使用固定页数设置"""
|
||||
try:
|
||||
domain, type_id = tid.split('_')
|
||||
url = f"https://{domain}/index.php/vod/type/id/{type_id}.html"
|
||||
if pg and pg != '1':
|
||||
url = url.replace('.html', f'/page/{pg}.html')
|
||||
self.log(f"访问分类URL: {url}")
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=20)
|
||||
|
||||
# 使用固定页数设置,而不是尝试从页面解析
|
||||
pagecount = 999
|
||||
total = 19980
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': pagecount,
|
||||
'limit': 20,
|
||||
'total': total
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"分类内容获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索功能"""
|
||||
try:
|
||||
search_url = f"{self.host}/index.php/vod/search.html?wd={urllib.parse.quote(key)}&page={pg}"
|
||||
self.log(f"搜索URL: {search_url}")
|
||||
rsp = self.fetch(search_url, headers=self.headers)
|
||||
if not rsp or rsp.status_code != 200:
|
||||
return {'list': []}
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc)
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
self.log(f"搜索出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页面"""
|
||||
try:
|
||||
vid = ids[0]
|
||||
if '_' in vid:
|
||||
domain, video_id = vid.split('_')
|
||||
detail_url = f"https://{domain}/index.php/vod/detail/id/{video_id}.html"
|
||||
else:
|
||||
detail_url = f"{self.host}/index.php/vod/detail/id/{vid}.html"
|
||||
self.log(f"访问详情URL: {detail_url}")
|
||||
rsp = self.fetch(detail_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
video_info = self._get_detail(doc, vid)
|
||||
return {'list': [video_info]} if video_info else {'list': []}
|
||||
except Exception as e:
|
||||
self.log(f"详情获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放链接 - 直接使用API获取视频地址"""
|
||||
try:
|
||||
self.log(f"获取播放链接: flag={flag}, id={id}")
|
||||
|
||||
# 提取视频ID
|
||||
if '_' in id:
|
||||
_, video_id = id.split('_')
|
||||
else:
|
||||
video_id = id
|
||||
|
||||
self.log(f"视频ID: {video_id}")
|
||||
|
||||
# 直接调用API获取视频地址
|
||||
api_url = f"{self.api_host}/api/v2/vod/reqplay/{video_id}"
|
||||
self.log(f"请求API获取视频地址: {api_url}")
|
||||
|
||||
api_headers = self.headers.copy()
|
||||
api_headers.update({
|
||||
'Referer': f"{self.host}/",
|
||||
'Origin': self.host,
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
})
|
||||
|
||||
api_response = self.fetch(api_url, headers=api_headers)
|
||||
if api_response and api_response.status_code == 200:
|
||||
data = api_response.json()
|
||||
self.log(f"API响应: {data}")
|
||||
|
||||
if data.get('retcode') == 3:
|
||||
video_url = data.get('data', {}).get('httpurl_preview', '')
|
||||
else:
|
||||
video_url = data.get('data', {}).get('httpurl', '')
|
||||
|
||||
if video_url:
|
||||
# 移除可能的参数
|
||||
video_url = video_url.replace('?300', '')
|
||||
self.log(f"从API获取到视频地址: {video_url}")
|
||||
return {'parse': 0, 'playUrl': '', 'url': video_url}
|
||||
else:
|
||||
self.log("API响应中没有找到视频地址")
|
||||
else:
|
||||
self.log(f"API请求失败,状态码: {api_response.status_code if api_response else '无响应'}")
|
||||
|
||||
# 如果API请求失败,回退到原来的方法
|
||||
if '_' in id:
|
||||
domain, play_id = id.split('_')
|
||||
play_url = f"https://{domain}/html/kkyd.html?m={play_id}"
|
||||
else:
|
||||
play_url = f"{self.host}/html/kkyd.html?m={id}"
|
||||
|
||||
self.log(f"回退到播放页面: {play_url}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_url}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"播放链接获取出错: {str(e)}")
|
||||
# 出错时也返回播放页面URL
|
||||
if '_' in id:
|
||||
domain, play_id = id.split('_')
|
||||
play_url = f"https://{domain}/html/kkyd.html?m={play_id}"
|
||||
else:
|
||||
play_url = f"{self.host}/html/kkyd.html?m={id}"
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_url}
|
||||
|
||||
# ========== 辅助方法 ==========
|
||||
|
||||
def _get_videos(self, doc, limit=None):
|
||||
"""获取影片列表 - 根据实际网站结构"""
|
||||
try:
|
||||
videos = []
|
||||
elements = doc.xpath('//a[@class="vodbox"]')
|
||||
self.log(f"找到 {len(elements)} 个vodbox元素")
|
||||
for elem in elements:
|
||||
video = self._extract_video(elem)
|
||||
if video:
|
||||
videos.append(video)
|
||||
return videos[:limit] if limit and videos else videos
|
||||
except Exception as e:
|
||||
self.log(f"获取影片列表出错: {str(e)}")
|
||||
return []
|
||||
|
||||
def _extract_video(self, element):
|
||||
"""提取影片信息 - 修复标题乱码问题,正确读取km-script标签文本"""
|
||||
try:
|
||||
# 1. 提取影片链接(获取vod_id的来源)
|
||||
link = element.xpath('./@href')[0] # 获取a标签的href属性
|
||||
if link.startswith('/'):
|
||||
link = self.host + link # 补全相对路径为完整URL
|
||||
|
||||
# 2. 提取vod_id(从URL的m参数获取,而非hash,更准确)
|
||||
vod_id = self.regStr(r'm=(\d+)', link) # 匹配 ?m=123 中的数字
|
||||
if not vod_id:
|
||||
vod_id = str(hash(link) % 1000000) # 兜底:hash生成唯一ID
|
||||
|
||||
# 3. 提取标题(关键修复:读取<p class="km-script">内的文本并解密)
|
||||
title_elem = element.xpath('./p[@class="km-script"]/text()') # 定位km-script标签
|
||||
if not title_elem:
|
||||
# 尝试其他可能的标题选择器
|
||||
title_elem = element.xpath('.//p[contains(@class, "script")]/text()')
|
||||
if not title_elem:
|
||||
title_elem = element.xpath('.//p/text()')
|
||||
if not title_elem:
|
||||
title_elem = element.xpath('.//h3/text()')
|
||||
if not title_elem:
|
||||
title_elem = element.xpath('.//h4/text()')
|
||||
if not title_elem:
|
||||
self.log(f"未找到标题元素,跳过该视频")
|
||||
return None
|
||||
|
||||
title_encrypted = title_elem[0].strip() # 获取加密的标题文本
|
||||
|
||||
# 4. 解密标题 - 使用网站的解密算法
|
||||
title = self._decrypt_title(title_encrypted)
|
||||
|
||||
# 5. 提取封面图(逻辑不变,兼容data-original和src)
|
||||
pic_elem = element.xpath('.//img/@data-original') # 优先懒加载地址
|
||||
if not pic_elem:
|
||||
pic_elem = element.xpath('.//img/@src') # 兜底:直接src地址
|
||||
pic = pic_elem[0] if pic_elem else ''
|
||||
|
||||
# 6. 补全图片URL(处理相对路径或无协议的情况)
|
||||
if pic:
|
||||
if pic.startswith('//'):
|
||||
pic = 'https:' + pic # 补全https协议
|
||||
elif pic.startswith('/'):
|
||||
pic = self.host + pic # 补全主域名
|
||||
|
||||
# 7. 返回正确的视频信息
|
||||
return {
|
||||
'vod_id': f"618013.xyz_{vod_id}",
|
||||
'vod_name': title, # 此时title已为正确文本
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': '',
|
||||
'vod_year': ''
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"提取影片信息出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _decrypt_title(self, encrypted_text):
|
||||
"""解密标题 - 使用网站的解密算法"""
|
||||
try:
|
||||
# 网站使用的解密算法:每个字符与128进行异或操作
|
||||
decrypted_chars = []
|
||||
for char in encrypted_text:
|
||||
# 将字符转换为Unicode码点
|
||||
code_point = ord(char)
|
||||
# 与128进行异或操作
|
||||
decrypted_code = code_point ^ 128
|
||||
# 转换回字符
|
||||
decrypted_char = chr(decrypted_code)
|
||||
decrypted_chars.append(decrypted_char)
|
||||
|
||||
# 拼接解密后的字符
|
||||
decrypted_text = ''.join(decrypted_chars)
|
||||
return decrypted_text
|
||||
except Exception as e:
|
||||
self.log(f"标题解密失败: {str(e)}")
|
||||
return encrypted_text # 如果解密失败,返回原文本
|
||||
|
||||
def _get_detail(self, doc, vid):
|
||||
"""获取详情信息 (优化版) - 修复播放源提取问题"""
|
||||
try:
|
||||
title = self._get_text(doc, ['//h1/text()', '//title/text()'])
|
||||
pic = self._get_text(doc, ['//div[@class="dyimg"]//img/@src', '//img[@class="poster"]/@src'])
|
||||
if pic and pic.startswith('/'):
|
||||
pic = self.host + pic
|
||||
desc = self._get_text(doc, ['//div[@class="yp_context"]/text()', '//div[@class="introduction"]//text()'])
|
||||
actor = self._get_text(doc, ['//span[contains(text(),"主演")]/following-sibling::*/text()'])
|
||||
director = self._get_text(doc, ['//span[contains(text(),"导演")]/following-sibling::*/text()'])
|
||||
|
||||
play_from = []
|
||||
play_urls = []
|
||||
|
||||
# 尝试查找播放源
|
||||
play_links = doc.xpath('//a[contains(@href, "m=")]')
|
||||
if play_links:
|
||||
episodes = []
|
||||
for link in play_links:
|
||||
ep_title = link.xpath('./text()')
|
||||
ep_href = link.xpath('./@href')[0]
|
||||
if ep_title:
|
||||
ep_title = ep_title[0].strip()
|
||||
play_id = self.regStr(r'm=(\d+)', ep_href)
|
||||
if play_id:
|
||||
episodes.append(f"{ep_title}${play_id}")
|
||||
|
||||
if episodes:
|
||||
play_from.append("默认播放源")
|
||||
play_urls.append('#'.join(episodes))
|
||||
|
||||
if not play_from:
|
||||
self.log("未找到播放源元素,无法定位播放源列表")
|
||||
# 即使没有播放源,也返回基本信息
|
||||
return {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '默认播放源',
|
||||
'vod_play_url': f"第1集${vid}"
|
||||
}
|
||||
|
||||
return {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_urls)
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"获取详情出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_text(self, doc, selectors):
|
||||
"""通用文本提取"""
|
||||
for selector in selectors:
|
||||
texts = doc.xpath(selector)
|
||||
for text in texts:
|
||||
if text and text.strip():
|
||||
return text.strip()
|
||||
return ''
|
||||
@@ -16,6 +16,12 @@
|
||||
"type":3,
|
||||
"api":"https://file.icve.com.cn/file_doc/249/899/3E7E0C8A023B624CEC6BDCC200F06F02.js",
|
||||
"ext":"https://cdn.waimaimingtang.com/file/images/bwc/20251023002200-507c3e8aae.js"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{"key":"TX",
|
||||
"name":"🐬腾讯视频",
|
||||
@@ -82,6 +88,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
@@ -578,6 +590,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/黑料不打烊.py"
|
||||
},
|
||||
{
|
||||
"key": "xj",
|
||||
"name": "🐬香蕉视频.py(关梯)|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/香蕉视频.py"
|
||||
},
|
||||
{
|
||||
"key": "dj",
|
||||
"name": "🐬妲己高清视频.py(关梯)|🔞",
|
||||
@@ -620,6 +638,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Xvideos.py"
|
||||
},
|
||||
{
|
||||
"key": "Pornhub",
|
||||
"name": "🐬Pornhub.py|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pornhub.py"
|
||||
},
|
||||
{
|
||||
"key": "fullhd",
|
||||
"name": "🐬FullHD.py|🔞",
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/芒果TV.py"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{
|
||||
"key": "ppx",
|
||||
"name": "🐬皮皮虾.py",
|
||||
"type": 3,
|
||||
@@ -23,6 +29,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/枫叶影院.py"
|
||||
},
|
||||
{
|
||||
"key": "ww",
|
||||
"name": "🐬哇哇APP.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/哇哇APP.py"
|
||||
},
|
||||
{
|
||||
"key": "rb",
|
||||
"name": "🐬热播APP.py",
|
||||
@@ -41,6 +53,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
@@ -251,6 +269,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/黑料不打烊.py"
|
||||
},
|
||||
{
|
||||
"key": "xj",
|
||||
"name": "🐬香蕉视频.py(关梯)|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/香蕉视频.py"
|
||||
},
|
||||
{
|
||||
"key": "dj",
|
||||
"name": "🐬妲己高清视频.py(关梯)|🔞",
|
||||
@@ -293,6 +317,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Xvideos.py"
|
||||
},
|
||||
{
|
||||
"key": "Pornhub",
|
||||
"name": "🐬Pornhub.py|🔞",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pornhub.py"
|
||||
},
|
||||
{
|
||||
"key": "fullhd",
|
||||
"name": "🐬FullHD.py|🔞",
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
"type":3,
|
||||
"api":"https://file.icve.com.cn/file_doc/249/899/3E7E0C8A023B624CEC6BDCC200F06F02.js",
|
||||
"ext":"https://cdn.waimaimingtang.com/file/images/bwc/20251023002200-507c3e8aae.js"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{"key":"TX",
|
||||
"name":"🐬腾讯视频",
|
||||
@@ -82,6 +88,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
"type":3,
|
||||
"api":"https://file.icve.com.cn/file_doc/249/899/3E7E0C8A023B624CEC6BDCC200F06F02.js",
|
||||
"ext":"https://cdn.waimaimingtang.com/file/images/bwc/20251023002200-507c3e8aae.js"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{"key":"TX",
|
||||
"name":"🐬腾讯视频[追剧]",
|
||||
@@ -113,6 +119,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py[追剧]",
|
||||
@@ -662,6 +674,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/黑料不打烊.py"
|
||||
},
|
||||
{
|
||||
"key": "xj",
|
||||
"name": "🐬香蕉视频.py(关梯)|🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/香蕉视频.py"
|
||||
},
|
||||
{
|
||||
"key": "dj",
|
||||
"name": "🐬妲己高清视频.py(关梯)|🔞[成人]",
|
||||
@@ -704,6 +722,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Xvideos.py"
|
||||
},
|
||||
{
|
||||
"key": "Pornhub",
|
||||
"name": "🐬Pornhub.py|🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pornhub.py"
|
||||
},
|
||||
{
|
||||
"key": "fullhd",
|
||||
"name": "🐬FullHD.py|🔞[成人]",
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
"name": "🐬芒果TV.py海豚影视交流群 TG:@hshsjk9[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/芒果TV.py"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{
|
||||
"key": "kf",
|
||||
@@ -65,6 +71,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py[追剧]",
|
||||
@@ -263,6 +275,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/黑料不打烊.py"
|
||||
},
|
||||
{
|
||||
"key": "xj",
|
||||
"name": "🐬香蕉视频.py(关梯)|🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/香蕉视频.py"
|
||||
},
|
||||
{
|
||||
"key": "dj",
|
||||
"name": "🐬妲己高清视频.py(关梯)|🔞[成人]",
|
||||
@@ -305,6 +323,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Xvideos.py"
|
||||
},
|
||||
{
|
||||
"key": "Pornhub",
|
||||
"name": "🐬Pornhub.py|🔞[成人]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pornhub.py"
|
||||
},
|
||||
{
|
||||
"key": "fullhd",
|
||||
"name": "🐬FullHD.py|🔞[成人]",
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
"type":3,
|
||||
"api":"https://file.icve.com.cn/file_doc/249/899/3E7E0C8A023B624CEC6BDCC200F06F02.js",
|
||||
"ext":"https://cdn.waimaimingtang.com/file/images/bwc/20251023002200-507c3e8aae.js"
|
||||
},
|
||||
{
|
||||
"key": "YK",
|
||||
"name": "🐬优酷视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优酷视频.py"
|
||||
},
|
||||
{"key":"TX",
|
||||
"name":"🐬腾讯视频",
|
||||
@@ -106,6 +112,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "UVOD",
|
||||
"name": "🐬优视频.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/优视频.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
|
||||
Reference in New Issue
Block a user