feat(adult): 添加 javxx 和 91RB 成人内容源

This commit is contained in:
Wang.Luo 2025-10-12 14:55:25 +08:00
parent 9043a859b9
commit cddd37858a
3 changed files with 431 additions and 0 deletions

View File

@ -1338,6 +1338,24 @@
"ratio": 1.5 "ratio": 1.5
} }
}, },
{
"key": "javxx",
"name": "javxx",
"type": 3,
"api": "./py/adult/javxx.py",
"searchable": 1,
"quickSearch": 1,
"filterable": 1
},
{
"key": "91RB",
"name": "91RB",
"type": 3,
"api": "./py/adult/91RB.py",
"searchable": 1,
"quickSearch": 1,
"filterable": 1
},
{ {
"key": "cam4", "key": "cam4",
"name": "cam4直播", "name": "cam4直播",

84
py/adult/91RB.py Normal file
View File

@ -0,0 +1,84 @@
#author Kyle
import re, sys, time, urllib.parse
sys.path.append('..')
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def __init__(self):
super().__init__(); self.base = 'https://www.91rb.com'; self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': self.base + '/'}
def getName(self): return '91热爆'
def init(self, extend=""): self.extend = extend or ''; return {'class': 'movie'}
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4)(\?|$)', url))
def manualVideoCheck(self): return False
def destroy(self): pass
def homeContent(self, filter): return {'class': [{'type_name': '最新上传', 'type_id': 'latest-updates'}, {'type_name': '热门视频', 'type_id': 'most-popular'}, {'type_name': '收藏最多', 'type_id': 'most-favourited'}, {'type_name': '日本AV', 'type_id': 'tags/av2/'}, {'type_name': 'jav', 'type_id': 'tags/jav/'}, {'type_name': '韩国', 'type_id': 'tags/20c3f16d021b069ce3af1da50b15bd83/'}]}
def homeVideoContent(self):
try: return self._listPage(self._buildListUrl('latest-updates', '1'))
except Exception as e: self.log(f'homeVideoContent error: {e}'); return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try: return self._listPage(self._buildListUrl(tid, pg), page=pg)
except Exception as e: self.log(f'categoryContent error: {e}'); return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 48, 'total': 0}
def detailContent(self, ids):
vid = self._ensure_id(ids[0]); detail_url = f"{self.base}/videos/{vid}/"; name = f'视频 {vid}'; pic = ''
try:
r = self.fetch(detail_url, headers=self.headers, timeout=10, allow_redirects=True)
if r and hasattr(r, 'text'):
doc = self.html(r.text)
if doc:
name = ''.join(doc.xpath('//h1//text()')).strip() or name
ogs = doc.xpath('//meta[@property="og:image"]/@content'); tws = doc.xpath('//meta[@name="twitter:image"]/@content')
pic = ogs[0].strip() if ogs else (tws[0].strip() if tws else '')
if pic: pic = self._abs_url(pic)
except Exception as e: self.log(f'detailContent fetch error: {e}')
if not pic: pic = self._cover_fallback(vid)
vod = {'vod_id': str(vid), 'vod_name': name, 'vod_pic': pic, 'type_name': '', 'vod_year': '', 'vod_remarks': '', 'vod_content': '', 'vod_play_from': '91RB', 'vod_play_url': f'正片${vid}'}
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
key_enc = urllib.parse.quote(key); url = f"{self.base}/search/{key_enc}/"
if pg != '1': url = url.rstrip('/') + f'/{pg}/'
try: return self._listPage(url, page=pg)
except Exception as e: self.log(f'searchContent error: {e}'); return {'list': [], 'page': pg, 'pagecount': 1, 'total': 0}
def playerContent(self, flag, id, vipFlags):
vid = self._ensure_id(id); group = int(vid) - (int(vid) % 1000)
m3u8 = f"https://91rbnet.gslb-al.com/hls/contents/videos/{group}/{vid}/{vid}.mp4/index.m3u8"
try:
r = self.fetch(m3u8, headers=self.headers, timeout=5, allow_redirects=True, verify=True, stream=True)
if r.status_code >= 400: self.log(f'm3u8 head status={r.status_code}, fallback to direct anyway')
except Exception as e: self.log(f'playerContent HEAD error: {e}')
return {'parse': 0, 'playUrl': '', 'url': m3u8, 'header': self.headers}
def localProxy(self, param): return None
def _buildListUrl(self, tid, pg):
path = tid.strip('/') or 'latest-updates'; page_suffix = f"/{pg}/" if str(pg) != '1' else '/'
if path.startswith('categories') or path in ['latest-updates', 'most-popular', 'most-favourited']: return f"{self.base}/{path}{page_suffix}"
return f"{self.base}/{path}{page_suffix}"
def _abs_url(self, url):
if not url: return url
u = url.strip()
return 'https:' + u if u.startswith('//') else (self.base + u if u.startswith('/') else u)
def _parse_srcset_first(self, srcset):
if not srcset: return ''
return srcset.split(',')[0].strip().split(' ')[0]
def _cover_fallback(self, vid):
try: iv = int(vid); group = iv - (iv % 1000); return f'https://rimg.iomycdn.com/videos_screenshots/{group}/{iv}/preview.jpg'
except Exception: return ''
def _listPage(self, url, page='1'):
doc = self.html(self.fetch(url, headers=self.headers, timeout=10).text)
if doc is None: return {'list': [], 'page': page, 'pagecount': 1, 'total': 0}
nodes, videos, seen = doc.xpath('//main//a[contains(@href, "/videos/")]'), [], set()
for a in nodes:
href = a.get('href') or ''; m = re.search(r'/videos/(\d+)/', href)
if not m or '/login' in href: continue
vid = m.group(1);
if vid in seen: continue
seen.add(vid); title = ''; img = a.xpath('.//img')
if img:
im = img[0]; title = (im.get('alt') or '').strip()
pic = (im.get('src') or im.get('data-src') or im.get('data-original') or '').strip()
if not pic: pic = self._parse_srcset_first(im.get('data-srcset') or im.get('srcset') or '')
pic = self._abs_url(pic)
else: title = (a.text or '').strip(); pic = ''
title = title or f'视频 {vid}'
if not pic or pic.startswith('data:'): pic = self._cover_fallback(vid)
videos.append({'vod_id': vid, 'vod_name': title, 'vod_pic': pic, 'vod_remarks': ''})
return {'list': videos, 'page': str(page), 'pagecount': 9999, 'limit': 48, 'total': 0}
def _ensure_id(self, s):
m = re.search(r'(\d+)', str(s)); return m.group(1) if m else str(s)

329
py/adult/javxx.py Normal file
View File

@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
# by @嗷呜
import gzip
import html
import json
import re
import sys
import base64
from base64 import b64decode
from urllib.parse import unquote, urlparse
import requests
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend='{}'):
config = json.loads(extend)
self.proxies = config.get('proxy', {})
self.plp = config.get('plp', '')
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
host = 'https://javxx.com'
contr = 'cn'
conh = f'{host}/{contr}'
headers = {
'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',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
'referer': f'{conh}/',
'sec-ch-ua': '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
}
gcate = 'H4sIAAAAAAAAA6tWejan4dm0DUpWCkp5qeVKOkrPm9e+nL4CxM/ILwHygfIv9k8E8YtSk1PzwELTFzxf0AgSKs0DChXnF6WmwIWfbW55OWcTqqRuTmpiNljN8427n3asBsmmp+YVpRaDtO2Z8nTiDJBQYnIJUKgYLPq0Y9uTvXOeTm0DSeQCdReBRJ9vBmqfDhIqTi3KhGhf0P587T6QUElierFSLQCk4MAf0gAAAA=='
flts = 'H4sIAAAAAAAAA23QwYrCMBAG4FeRnH0CX0WKBDJiMRpoY0WkIOtFXLQU1IoEFFHWw4qHPazgii/TRPctNKK1Ro/zz8cM/PkmKkMD5TLIZQ5HWVTFFUiNHqY1PeebyNOxAxSwCwWCOWitMxmEcttW0VKJKfKzN4kJAfLk1O9OdmemKzF+B8f2+j9aPVacEdwoeDbU3TuJd93LgdPXx1F8PmAdoEwNqTaBDFemrLAqL72hSnReqcuvDkgCRUsGkfqenw59AxaxxxybP9uRuFjkW5reai7alIOTKjoJzKoxpUnDvWG8bcnlj/obyHCcKi95JxeTeN9LEcu3zoYr9GndAQAA'
actft = 'H4sIAAAAAAAAA22UTUsbURSG/0qYtQMxZvIhIvidxI/oVpEy6GiCmpFkEhEpVBcqikYprV2kG6GkhYK2XRbxzziT+C88c2/OnLnnunznec47zJ3LWTsydpxDYzRhVJzqdsUzhoyavecoD1r2bjN8snZktEIwPJI0h0fSoRqL/vW33p9/xsehyLLgcZ4sETUrDcNp6pJRt2A4TV0yapYFwxZ1yahbMGxRl4yalYHhDHXJqFswnKEuGTUrC8NZ6pJRt2A4S10yalYOhnPUJaNuwXCOumTUrDwM56lLRrTWQ29wNzaa+7GLIRO/FRPYM9F7+hV8f6D3TCKZ5GQKyRQn00imOZlBMsPJLJJZTuaQzHFSQFLgpIikyEkJSYmTeSTznCwgWeBkEckiJ0tIljgpIylzsoxkmZMVJCucrCJZRRL/9/a2E/v3MvF/H14cLBlLpJL+32OqTyXNVHTJRFCxZaaiYREUDMuFVo0IKrZM2jEiKBjWCS0XEVRsmbRVRFAwLBBaJyIoGHZCPpoeT2TkZ8fPruHW4xt1EPnpCTyo8buf/ZsreseG26x5CPvd09f72+DL4+tZmxTP3bQPP7SqzkEDxZf/F8Hdj373pNe5JPHAcXZ2mRk8tP3bn9zcc2te5R016JzrasMTnrMZiZ1Pfvsu+H3ff75m4pbdcutVT3W/dsAND279DSxD8pmOBgAA'
def homeContent(self, filter):
data = self.getpq(requests.get(f"{self.conh}", headers=self.headers, proxies=self.proxies).text)
result = {}
cate = self.ungzip(self.gcate)
classes = []
filters = {}
for k, j in cate.items():
classes.append({
'type_name': k,
'type_id': j
})
if j == 'actresses':
fts = self.ungzip(self.actft)
else:
fts = self.ungzip(self.flts)
filters[j] = fts
result['class'] = classes
result['filters'] = filters
result['list'] = self.getvl(data('.vid-items .item'))
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
videos = []
if tid in ['genres', 'makers', 'series', 'tags']:
gggg = tid if tid == 'series' else tid[:-1]
pagecount = 1
data = self.getpq(requests.get(f"{self.conh}/{tid}", headers=self.headers, proxies=self.proxies).text)
for i in data(f'.term-items.{gggg} .item').items():
videos.append({
'vod_id': i('a').attr('href'),
'vod_name': i('h2').text(),
'vod_remarks': i('.meta').text(),
'vod_tag': 'folder',
'style': {"type": "rect", "ratio": 2}
})
elif tid == 'actresses':
params = {
'height': extend.get('height'),
"cup": extend.get('cup'),
"sort": extend.get('sort'),
'age': extend.get('age'),
"page": pg
}
c_params = {k: v for k, v in params.items() if v}
data = self.getpq(
requests.get(f"{self.conh}/{tid}", headers=self.headers, params=c_params, proxies=self.proxies).text)
pagecount = self.getpgc(data('ul.pagination li').eq(-1))
for i in data('.chanel-items .item').items():
i = i('.main')
videos.append({
'vod_id': i('.info a').attr('href'),
'vod_name': i('.info h2').text(),
'vod_pic': i('.avatar img').attr('src'),
'vod_year': i('.meta div div').eq(-1).text(),
'vod_remarks': i('.meta div div').eq(0).text(),
'vod_tag': 'folder',
'style': {"type": "oval", "ratio": 0.75}
})
else:
tid = tid.split('_click')[0].replace(f"/{self.contr}/", "")
params = {
"filter": extend.get('filter'),
"sort": extend.get('sort'),
"page": pg
}
c_params = {k: v for k, v in params.items() if v}
data = self.getpq(
requests.get(f"{self.conh}/{tid}", params=c_params, headers=self.headers, proxies=self.proxies).text)
videos = self.getvl(data('.vid-items .item'))
pagecount = self.getpgc(data('ul.pagination li').eq(-1))
result = {}
result['list'] = videos
result['page'] = pg
result['pagecount'] = pagecount
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
data = self.getpq(requests.get(f"{self.host}{ids[0]}", headers=self.headers, proxies=self.proxies).text)
dv = data('#video-details')
pnpn = {
'老僧酿酒、名妓读经': f"{data('#video-info h1').text()}${data('#video-files div').attr('data-url')}",
'书生玩剑': '#'.join(
[f"{i('.info .title span').eq(-1).text()}$_gggb_{i('.info .title').attr('href')}" for i in
data('.main .vid-items .item').items()]),
'将军作文': '#'.join([f"{i('.info .title span').eq(-1).text()}$_gggb_{i('.info .title').attr('href')}" for i in
data('.vid-items.side .item').items()])
}
n, p = [], []
for k, v in pnpn.items():
if v:
n.append(k)
p.append(v)
vod = {
'vod_content': dv('.content').text(),
'vod_play_from': '$$$'.join(n),
'vod_play_url': '$$$'.join(p)
}
a, b, c, d = [], [], [], []
for i in dv('.meta div').items():
if re.search(r'发布日期', i('label').text()):
vod['vod_year'] = i('span').text()
elif re.search(r'演员', i('label').text()):
a.extend(['[a=cr:' + json.dumps(
{'id': f"{j.attr('href')}_click", 'name': j.text()}) + '/]' + j.text() + '[/a]' for j in
i('a').items()])
elif re.search(r'制作商|系列', i('label').text()):
b.extend(['[a=cr:' + json.dumps(
{'id': f"{j.attr('href')}_click", 'name': j.text()}) + '/]' + j.text() + '[/a]' for j in
i('a').items()])
elif re.search(r'标签', i('label').text()):
c.extend(['[a=cr:' + json.dumps(
{'id': f"{j.attr('href')}_click", 'name': j.text()}) + '/]' + j.text() + '[/a]' for j in
i('a').items()])
elif re.search(r'类别', i('label').text()):
d.extend(['[a=cr:' + json.dumps(
{'id': f"{j.attr('href')}_click", 'name': j.text()}) + '/]' + j.text() + '[/a]' for j in
i('a').items()])
vod.update({'vod_actor': ' '.join(a), 'vod_director': ' '.join(b), 'vod_remarks': ' '.join(c),
'vod_content': ' '.join(d) + '\n' + vod['vod_content']})
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
params = {'keyword': key, 'page': pg}
data = self.getpq(
requests.get(f"{self.conh}/search", headers=self.headers, params=params, proxies=self.proxies).text)
return {'list': self.getvl(data('.vid-items .item')), 'page': pg}
def playerContent(self, flag, id, vipFlags):
# 处理跳转标识,获取初始加密地址
if id.startswith('_gggb_'):
data = self.getpq(
requests.get(f"{self.host}{id.replace('_gggb_', '')}", headers=self.headers).text)
id = data('#video-files div').attr('data-url')
# 解密初始URL
url = self.de_url(id)
parsed_url = urlparse(url)
durl = parsed_url.scheme + "://" + parsed_url.netloc
video_id = parsed_url.path.split('/')[-1]
# 生成加密的token
tkid = self.encrypt_video_id(video_id)
data_url = f"{durl}/stream?token={tkid}"
# 请求视频流数据
response = requests.get(data_url, timeout=10)
data = response.json()
# 解密媒体数据
media = data["result"]["media"]
decrypted_media = self.decrypt_media(media)
decrypted_data = json.loads(decrypted_media)
playeurl = decrypted_data["stream"]
# 构建请求头并返回结果
headers = {
'user-agent': self.headers['user-agent'],
'origin': durl,
'referer': f"{durl}/"
}
return {'parse': 0, 'url': playeurl, 'header': headers}
def encrypt_video_id(self, video_id, key=None):
"""使用指定密钥对视频ID进行XOR加密并Base64编码"""
if key is None:
key = "kBxSj373GhC18iOc" # 默认密钥
# XOR加密
key_bytes = key.encode('utf-8')
encrypted_bytes = []
for i, char in enumerate(video_id):
key_byte = key_bytes[i % len(key_bytes)]
encrypted_byte = ord(char) ^ key_byte
encrypted_bytes.append(encrypted_byte)
# Base64编码
encrypted_base64 = base64.b64encode(bytes(encrypted_bytes)).decode('utf-8')
return encrypted_base64
def decrypt_media(self, encrypted_media, key="kBxSj373GhC18iOc"):
"""使用指定密钥解密媒体数据"""
# Base64解码
encrypted_bytes = base64.b64decode(encrypted_media)
# XOR解密
key_bytes = key.encode('utf-8')
decrypted_chars = []
for i, byte in enumerate(encrypted_bytes):
key_byte = key_bytes[i % len(key_bytes)]
decrypted_char = byte ^ key_byte
decrypted_chars.append(chr(decrypted_char))
# 组合成字符串并URL解码
decrypted_text = ''.join(decrypted_chars)
url_decoded_text = unquote(decrypted_text)
return url_decoded_text
def localProxy(self, param):
pass
def liveContent(self, url):
pass
def getvl(self, data):
videos = []
for i in data.items():
img = i('.img')
imgurl = img('.image img').attr('src')
if imgurl:
imgurl = imgurl.replace("/s360/", "/s1080/")
videos.append({
'vod_id': img('a').attr('href'),
'vod_name': i('.info .title').text(),
'vod_pic': imgurl,
'vod_year': i('.info .meta div').eq(-1).text(),
'vod_remarks': i('.duration').text(),
'style': {"type": "rect", "ratio": 1.33}
})
return videos
def de_url(self, encoded_str):
decoded = b64decode(encoded_str).decode()
key = "G9zhUyphqPWZGWzZ" # 更新为第一个密钥
result = []
for i, char in enumerate(decoded):
key_char = key[i % len(key)]
decrypted_char = chr(ord(char) ^ ord(key_char))
result.append(decrypted_char)
return unquote(''.join(result))
def getpgc(self, data):
try:
if data:
if data('a'):
return int(data('a').attr('href').split('page=')[-1])
else:
return int(data.text())
else:
raise Exception("获取页数失败")
except:
return 1
def p_qjs(self, js_code):
try:
from com.whl.quickjs.wrapper import QuickJSContext
ctx = QuickJSContext.create()
jctx = ctx.evaluate(js_code)
code = jctx.strip().split('const posterUrl', 1)[0].split('{', 1)[-1]
result = ctx.evaluate(f"{code}\nJSON.stringify(media)")
ctx.destroy()
return json.loads(result)
except Exception as e:
self.log(f"执行失败: {e}")
return []
def ungzip(self, data):
result = gzip.decompress(b64decode(data)).decode()
return json.loads(result)
def getpq(self, data):
try:
return pq(data)
except Exception as e:
print(f"{str(e)}")
return pq(data.encode('utf-8'))