diff --git a/adult.json b/adult.json index 2b51c68..dc070c5 100644 --- a/adult.json +++ b/adult.json @@ -1338,6 +1338,24 @@ "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", "name": "cam4直播", diff --git a/py/adult/91RB.py b/py/adult/91RB.py new file mode 100644 index 0000000..5e4cab2 --- /dev/null +++ b/py/adult/91RB.py @@ -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) \ No newline at end of file diff --git a/py/adult/javxx.py b/py/adult/javxx.py new file mode 100644 index 0000000..a5a7d3d --- /dev/null +++ b/py/adult/javxx.py @@ -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'))