Files
tvbox_collect/jaychouqq/yingshi/py/xhamster.py
T
2026-06-29 03:41:14 +00:00

259 lines
8.5 KiB
Python

"""
@header({
searchable: 1,
filterable: 1,
quickSearch: 1,
title: 'xHamster',
lang: 'hipy',
})
"""
# -*- coding: utf-8 -*-
# by @嗷呜 & Gemini
"""
@header({
searchable: 1,
filterable: 1,
quickSearch: 1,
title: 'xHamster',
lang: 'hipy',
})
"""
import json
import sys
import requests
import re
from pyquery import PyQuery as pq
from requests import Session
sys.path.append('..')
from base.spider import Spider
# 忽略 HTTPS 警告
requests.packages.urllib3.disable_warnings()
class Spider(Spider):
# 显式定义 headers,解决之前日志中的 AttributeError
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
def init(self, extend=""):
self.host = self.gethost()
self.headers.update({'referer': f'{self.host}/'})
self.session = Session()
self.session.headers.update(self.headers)
self.session.verify = False
def getName(self):
return "xHamster"
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def localProxy(self, param):
return [200, "video/MP2T", b""]
def destroy(self):
pass
def homeContent(self, filter):
result = {}
# 优化分类 ID
cateManual = {
"4K": "/4k",
"国产": "two_click_/categories/chinese",
"最新": "/newest",
"最佳": "/best",
"频道": "/channels",
"类别": "/categories",
"明星": "/pornstars"
}
classes = []
for k in cateManual:
classes.append({'type_name': k, 'type_id': cateManual[k]})
result['class'] = classes
return result
def homeVideoContent(self):
data = self.getpq("/")
if data:
jsdata = self.getjsdata(data)
if jsdata:
return {'list': self.getlist_from_json(jsdata)}
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
vdata = []
result = {'page': int(pg), 'pagecount': 999, 'limit': 40, 'total': 9999}
path = tid
if 'two_click_' in tid:
path = tid.split('click_')[-1]
# 修复翻页 URL 逻辑
url_path = f'{path}/{pg}' if pg != "1" else path
if '?' in path:
url_path = f'{path}&page={pg}' if pg != "1" else path
data = self.getpq(url_path)
if not data: return result
jsdata = self.getjsdata(data)
if not jsdata:
return result
# 1. 频道页解析
if tid == '/channels':
items = self.search_dict(jsdata, 'channels') or []
for i in items:
vdata.append({
'vod_id': "two_click_" + i.get('channelURL', ''),
'vod_name': i.get('channelName'),
'vod_pic': i.get('siteLogoURL'),
'vod_year': f"Videos: {i.get('videoCount', 0)}",
'vod_tag': 'folder',
'vod_remarks': f"Subs: {i.get('subscriptionModel', {}).get('subscribers', 0)}",
'style': {'ratio': 1.33, 'type': 'rect'}
})
# 2. 类别页解析
elif tid == '/categories':
items = self.search_dict(jsdata, 'assignable') or []
for i in items:
vdata.append({
'vod_id': "two_click_/categories/" + (i.get('id') or i.get('slug', '')),
'vod_name': i.get('name'),
'vod_pic': '',
'vod_tag': 'folder',
'vod_remarks': '分类',
'style': {'ratio': 1.33, 'type': 'rect'}
})
# 3. 明星页解析
elif tid == '/pornstars':
items = self.search_dict(jsdata, 'pornstars') or []
for i in items:
vdata.append({
'vod_id': "two_click_" + i.get('pageURL', ''),
'vod_name': i.get('name'),
'vod_pic': i.get('thumbURL'),
'vod_tag': 'folder',
'vod_remarks': f"Videos: {i.get('videosCount', 0)}",
'style': {'ratio': 1.0, 'type': 'rect'}
})
# 4. 普通视频列表 (4K, 国产, 个人页等)
else:
vdata = self.getlist_from_json(jsdata)
result['list'] = vdata
return result
def detailContent(self, ids):
data = self.getpq(ids[0])
if not data: return {'list': []}
link = data('link[rel="preload"][as="fetch"][crossorigin="true"]').attr('href')
play_url = f"播放源$666_{link}" if link else f"嗅探${ids[0]}"
vod = {
'vod_name': data('meta[property="og:title"]').attr('content') or "Video",
'vod_remarks': data('.rb-new__info').text() or "xHamster",
'vod_play_from': 'xHamster',
'vod_play_url': play_url
}
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
data = self.getpq(f'/search/{key}?page={pg}')
if data:
jsdata = self.getjsdata(data)
if jsdata:
return {'list': self.getlist_from_json(jsdata), 'page': pg}
return {'list': [], 'page': pg}
def playerContent(self, flag, id, vipFlags):
p, url = 1, id
headers = {'User-Agent': self.headers['User-Agent'], 'origin': self.host, 'referer': f'{self.host}/'}
if id.startswith("666_"):
p, url = 0, id[4:]
return {'parse': p, 'url': url, 'header': headers}
def gethost(self):
# 优先选择日本节点域名
fallback = "https://jp.xhamster.com"
try:
r = requests.get('https://xhamster.com', headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=False, timeout=5)
loc = r.headers.get('Location') or r.headers.get('location')
return loc.rstrip('/') if loc else fallback
except:
return fallback
def getlist_from_json(self, jsdata):
vlist = []
# 使用搜索函数查找视频列表键
videos = self.search_dict(jsdata, 'videoThumbProps') or []
for i in videos:
if not i.get('pageURL'): continue
vlist.append({
'vod_id': i.get('pageURL'),
'vod_name': i.get('title'),
'vod_pic': i.get('imageURL') or i.get('thumbURL'),
'vod_year': f"{i.get('views', 0)} views",
'vod_remarks': self.second_to_time(i.get('duration', 0)),
'style': {'ratio': 1.33, 'type': 'rect'}
})
return vlist
def second_to_time(self, sec):
try:
sec = int(sec)
m, s = divmod(sec, 60)
h, m = divmod(m, 60)
return "%d:%02d:%02d" % (h, m, s) if h > 0 else "%02d:%02d" % (m, s)
except: return ""
# --- 核心修复:更鲁棒的 JSON 提取器 ---
def getjsdata(self, data):
try:
# 获取所有 script 标签内容
for script in data("script").items():
vhtml = script.text()
if 'initials=' in vhtml:
# 使用正则精准匹配 initials 赋值的 JSON 部分
match = re.search(r'initials\s*=\s*(\{.*?\});', vhtml, re.DOTALL)
if not match:
match = re.search(r'initials\s*=\s*(\{.*\})', vhtml, re.DOTALL)
if match:
return json.loads(match.group(1))
except: pass
return {}
# --- 核心修复:递归搜索字典中的 Key ---
def search_dict(self, data, key):
if isinstance(data, dict):
if key in data: return data[key]
for v in data.values():
res = self.search_dict(v, key)
if res: return res
elif isinstance(data, list):
for i in data:
res = self.search_dict(i, key)
if res: return res
return None
def getpq(self, path=''):
try:
h = '' if path.startswith('http') else self.host
url = f'{h}{path}'
# 针对 NAS 网络环境优化
response = self.session.get(url, timeout=15)
return pq(response.content)
except: return None