339 lines
13 KiB
Python
339 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# by @嗷呜
|
|
import json
|
|
import sys
|
|
from base64 import b64decode, b64encode
|
|
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=""):
|
|
self.host = self.gethost()
|
|
self.headers['referer'] = f'{self.host}/'
|
|
self.session = Session()
|
|
self.session.headers.update(self.headers)
|
|
pass
|
|
|
|
def getName(self):
|
|
pass
|
|
|
|
def isVideoFormat(self, url):
|
|
pass
|
|
|
|
def manualVideoCheck(self):
|
|
pass
|
|
|
|
def destroy(self):
|
|
pass
|
|
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-full-version': '"133.0.6943.98"',
|
|
'sec-ch-ua-arch': '"x86"',
|
|
'sec-ch-ua-platform': '"Windows"',
|
|
'sec-ch-ua-platform-version': '"19.0.0"',
|
|
'sec-ch-ua-model': '""',
|
|
'sec-ch-ua-full-version-list': '"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="133.0.6943.98", "Chromium";v="133.0.6943.98"',
|
|
'dnt': '1',
|
|
'upgrade-insecure-requests': '1',
|
|
'sec-fetch-site': 'none',
|
|
'sec-fetch-mode': 'navigate',
|
|
'sec-fetch-user': '?1',
|
|
'sec-fetch-dest': 'document',
|
|
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
'priority': 'u=0, i'
|
|
}
|
|
|
|
def homeContent(self, filter):
|
|
result = {}
|
|
cateManual = {
|
|
"4K": "/4k",
|
|
"国产": "two_click_/categories/chinese",
|
|
"最新": "/newest",
|
|
"最佳": "/best",
|
|
"频道": "/channels",
|
|
"类别": "/categories",
|
|
"明星": "/pornstars"
|
|
}
|
|
classes = []
|
|
filters = {}
|
|
for k in cateManual:
|
|
classes.append({
|
|
'type_name': k,
|
|
'type_id': cateManual[k]
|
|
})
|
|
if k !='4K':filters[cateManual[k]]=[{'key':'type','name':'类型','value':[{'n':'4K','v':'/4k'}]}]
|
|
result['class'] = classes
|
|
result['filters'] = filters
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
data = self.getpq()
|
|
return {'list': self.getlist(data(".thumb-list--sidebar .thumb-list__item"))}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
vdata = []
|
|
result = {}
|
|
result['page'] = pg
|
|
result['pagecount'] = 9999
|
|
result['limit'] = 90
|
|
result['total'] = 999999
|
|
if tid in ['/4k', '/newest', '/best'] or 'two_click_' in tid:
|
|
if 'two_click_' in tid: tid = tid.split('click_')[-1]
|
|
data = self.getpq(f'{tid}{extend.get("type","")}/{pg}')
|
|
vdata = self.getlist(data(".thumb-list--sidebar .thumb-list__item"))
|
|
elif tid == '/channels':
|
|
data = self.getpq(f'{tid}/{pg}')
|
|
jsdata = self.getjsdata(data)
|
|
for i in jsdata['channels']:
|
|
vdata.append({
|
|
'vod_id': f"two_click_" + i.get('channelURL'),
|
|
'vod_name': i.get('channelName'),
|
|
'vod_pic': i.get('siteLogoURL'),
|
|
'vod_year': f'videos:{i.get("videoCount")}',
|
|
'vod_tag': 'folder',
|
|
'vod_remarks': f'subscribers:{i["subscriptionModel"].get("subscribers")}',
|
|
'style': {'ratio': 1.33, 'type': 'rect'}
|
|
})
|
|
elif tid == '/categories':
|
|
result['pagecount'] = pg
|
|
data = self.getpq(tid)
|
|
self.cdata = self.getjsdata(data)
|
|
for i in self.cdata['layoutPage']['store']['popular']['assignable']:
|
|
vdata.append({
|
|
'vod_id': "one_click_" + i.get('id'),
|
|
'vod_name': i.get('name'),
|
|
'vod_pic': '',
|
|
'vod_tag': 'folder',
|
|
'style': {'ratio': 1.33, 'type': 'rect'}
|
|
})
|
|
elif tid == '/pornstars':
|
|
data = self.getpq(f'{tid}/{pg}')
|
|
pdata = self.getjsdata(data)
|
|
for i in pdata['pagesPornstarsComponent']['pornstarListProps']['pornstars']:
|
|
vdata.append({
|
|
'vod_id': f"two_click_" + i.get('pageURL'),
|
|
'vod_name': i.get('name'),
|
|
'vod_pic': i.get('imageThumbUrl'),
|
|
'vod_remarks': i.get('translatedCountryName'),
|
|
'vod_tag': 'folder',
|
|
'style': {'ratio': 1.33, 'type': 'rect'}
|
|
})
|
|
elif 'one_click' in tid:
|
|
result['pagecount'] = pg
|
|
tid = tid.split('click_')[-1]
|
|
for i in self.cdata['layoutPage']['store']['popular']['assignable']:
|
|
if i.get('id') == tid:
|
|
for j in i['items']:
|
|
vdata.append({
|
|
'vod_id': f"two_click_" + j.get('url'),
|
|
'vod_name': j.get('name'),
|
|
'vod_pic': j.get('thumb'),
|
|
'vod_tag': 'folder',
|
|
'style': {'ratio': 1.33, 'type': 'rect'}
|
|
})
|
|
result['list'] = vdata
|
|
return result
|
|
|
|
def detailContent(self, ids):
|
|
data = self.getpq(ids[0])
|
|
vn = data('meta[property="og:title"]').attr('content')
|
|
dtext = data('#video-tags-list-container')
|
|
href = dtext('a').attr('href')
|
|
title = dtext('span[class*="body-bold-"]').eq(0).text()
|
|
pdtitle = ''
|
|
if href:
|
|
pdtitle = '[a=cr:' + json.dumps({'id': 'two_click_' + href, 'name': title}) + '/]' + title + '[/a]'
|
|
vod = {
|
|
'vod_name': vn,
|
|
'vod_director': pdtitle,
|
|
'vod_remarks': data('.rb-new__info').text(),
|
|
'vod_play_from': 'Xhamster',
|
|
'vod_play_url': ''
|
|
}
|
|
try:
|
|
plist = []
|
|
html_content = data.outerHtml()
|
|
|
|
# 尝试从页面中提取视频URL
|
|
import re
|
|
hls_urls = set()
|
|
|
|
# 只匹配m3u8格式链接
|
|
hls_patterns = [
|
|
r'setVideoHLS\(["\']([^"\']+\.m3u8)["\']\)',
|
|
r'"hlsUrl"\s*:\s*["\']([^"\']+\.m3u8)["\']',
|
|
r'hls\s*:\s*[{\s]*url\s*:\s*["\']([^"\']+\.m3u8)["\']',
|
|
r'["\'](https?:\/\/[^"\']+\.m3u8)["\']'
|
|
]
|
|
|
|
# 尝试所有HLS模式
|
|
for pattern in hls_patterns:
|
|
matches = re.findall(pattern, html_content)
|
|
for match in matches:
|
|
hls_urls.add(match)
|
|
|
|
# 如果找到HLS链接,尝试从中提取所有画质
|
|
if hls_urls:
|
|
for hls_url in hls_urls:
|
|
# 解析m3u8文件获取所有画质
|
|
hls_qualities = self._parse_hls_qualities(hls_url)
|
|
|
|
# 如果成功解析出多个画质
|
|
if hls_qualities:
|
|
for quality, url in hls_qualities.items():
|
|
encoded = self.e64(f'{0}@@@@{url}')
|
|
plist.append(f"{quality}${encoded}")
|
|
else:
|
|
# 如果无法解析画质,添加原始HLS链接
|
|
encoded = self.e64(f'{0}@@@@{hls_url}')
|
|
plist.append(f"HLS${encoded}")
|
|
|
|
except Exception as e:
|
|
plist = [f"{vn}${self.e64(f'{1}@@@@{ids[0]}')}"]
|
|
print(f"获取视频信息失败: {str(e)}")
|
|
|
|
if plist:
|
|
# 按质量排序
|
|
def custom_sort_key(url):
|
|
quality = url.split('$')[0]
|
|
number = ''.join(filter(str.isdigit, quality))
|
|
number = int(number) if number else 0
|
|
return -number, quality
|
|
|
|
plist.sort(key=custom_sort_key)
|
|
vod['vod_play_url'] = '#'.join(plist)
|
|
|
|
return {'list': [vod]}
|
|
|
|
def _parse_hls_qualities(self, m3u8_url):
|
|
"""解析m3u8文件,提取所有画质选项"""
|
|
try:
|
|
# 发送请求获取m3u8内容
|
|
response = self.session.get(m3u8_url, headers=self.headers, timeout=5)
|
|
response.encoding = 'utf-8'
|
|
m3u8_content = response.text
|
|
|
|
# 解析m3u8内容中的画质信息
|
|
qualities = {}
|
|
# 查找EXT-X-STREAM-INF标签,这通常包含不同画质的信息
|
|
import re
|
|
stream_inf_pattern = r'#EXT-X-STREAM-INF:.*?RESOLUTION=([\d]+x[\d]+).*?\n([^\n]+)'
|
|
matches = re.findall(stream_inf_pattern, m3u8_content)
|
|
|
|
for resolution, path in matches:
|
|
# 从分辨率中提取高度作为画质标识(如1080p, 720p等)
|
|
height = resolution.split('x')[1]
|
|
quality = f"{height}p"
|
|
|
|
# 构建完整的URL
|
|
if path.startswith('http'):
|
|
full_url = path
|
|
else:
|
|
# 处理相对路径
|
|
from urllib.parse import urlparse, urljoin
|
|
base_url = urlparse(m3u8_url).scheme + '://' + urlparse(m3u8_url).netloc
|
|
full_url = urljoin(m3u8_url, path)
|
|
|
|
qualities[quality] = full_url
|
|
|
|
# 如果找到画质信息,按清晰度从高到低排序
|
|
if qualities:
|
|
sorted_qualities = {}
|
|
for quality in sorted(qualities.keys(), key=lambda x: int(''.join(filter(str.isdigit, x))), reverse=True):
|
|
sorted_qualities[quality] = qualities[quality]
|
|
return sorted_qualities
|
|
|
|
return None
|
|
except Exception as e:
|
|
print(f"解析HLS画质失败: {str(e)}")
|
|
return None
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
data = self.getpq(f'/search/{key}?page={pg}')
|
|
return {'list': self.getlist(data(".thumb-list--sidebar .thumb-list__item")), 'page': pg}
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
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',
|
|
'origin': self.host,
|
|
'sec-fetch-site': 'cross-site',
|
|
'sec-fetch-mode': 'cors',
|
|
'sec-fetch-dest': 'empty',
|
|
'referer': f'{self.host}/',
|
|
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
'priority': 'u=1, i',
|
|
}
|
|
ids = self.d64(id).split('@@@@')
|
|
return {'parse': int(ids[0]), 'url': ids[1], 'header': headers}
|
|
|
|
def localProxy(self, param):
|
|
pass
|
|
|
|
def gethost(self):
|
|
try:
|
|
response = self.fetch('https://xhamster.com', headers=self.headers, allow_redirects=False)
|
|
return response.headers['Location']
|
|
except Exception as e:
|
|
print(f"获取主页失败: {str(e)}")
|
|
return "https://zn.xhamster.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('.role-pop').attr('href'),
|
|
'vod_name': i('.video-thumb-info a').text(),
|
|
'vod_pic': i('.role-pop img').attr('src'),
|
|
'vod_year': i('.video-thumb-info .video-thumb-views').text().split(' ')[0],
|
|
'vod_remarks': i('.role-pop div[data-role="video-duration"]').text(),
|
|
'style': {'ratio': 1.33, 'type': 'rect'}
|
|
})
|
|
return vlist
|
|
|
|
def getpq(self, path=''):
|
|
h = '' if path.startswith('http') else self.host
|
|
response = self.session.get(f'{h}{path}').text
|
|
try:
|
|
return pq(response)
|
|
except Exception as e:
|
|
print(f"{str(e)}")
|
|
return pq(response.encode('utf-8'))
|
|
|
|
def getjsdata(self, data):
|
|
vhtml = data("script[id='initials-script']").text()
|
|
jst = json.loads(vhtml.split('initials=')[-1][:-1])
|
|
return jst
|
|
|