724 lines
31 KiB
Python
724 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
import sys
|
|
import re
|
|
import json
|
|
import base64
|
|
import threading
|
|
import requests
|
|
import urllib3
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from socketserver import ThreadingMixIn
|
|
from urllib.parse import unquote, quote
|
|
|
|
urllib3.disable_warnings()
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
# ===== 纯 Python AES-128 工具 =====
|
|
_sbox = bytes([
|
|
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
|
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
|
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
|
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
|
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
|
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
|
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
|
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
|
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
|
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
|
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
|
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
|
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
|
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
|
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
|
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16])
|
|
|
|
_inv_sbox = bytes([
|
|
0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
|
|
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
|
|
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
|
|
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
|
|
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
|
|
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
|
|
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
|
|
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
|
|
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
|
|
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
|
|
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
|
|
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
|
|
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
|
|
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
|
|
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
|
|
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d])
|
|
|
|
_rcon = [0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36]
|
|
|
|
def _xtime(a):
|
|
return ((a << 1) ^ 0x1b) & 0xff if a & 0x80 else (a << 1) & 0xff
|
|
|
|
def _gf_mul(a, b):
|
|
r = 0
|
|
for _ in range(8):
|
|
if b & 1: r ^= a
|
|
a = _xtime(a)
|
|
b >>= 1
|
|
return r
|
|
|
|
_mul_e = bytes(_gf_mul(0x0e, i) for i in range(256))
|
|
_mul_b = bytes(_gf_mul(0x0b, i) for i in range(256))
|
|
_mul_d = bytes(_gf_mul(0x0d, i) for i in range(256))
|
|
_mul_9 = bytes(_gf_mul(0x09, i) for i in range(256))
|
|
|
|
_key_schedules = {}
|
|
|
|
def _key_schedule(key):
|
|
k = bytes(key)
|
|
if k in _key_schedules: return _key_schedules[k]
|
|
w = []
|
|
for i in range(4):
|
|
w.append([key[4*i], key[4*i+1], key[4*i+2], key[4*i+3]])
|
|
for i in range(4, 44):
|
|
temp = w[i-1][:]
|
|
if i % 4 == 0:
|
|
temp = temp[1:] + temp[:1]
|
|
temp = [_sbox[b] for b in temp]
|
|
temp[0] ^= _rcon[i//4 - 1]
|
|
w.append([w[i-4][j] ^ temp[j] for j in range(4)])
|
|
_key_schedules[k] = w
|
|
return w
|
|
|
|
def _dec_block(block, w):
|
|
s0,s1,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15 = block
|
|
s0 ^= w[40][0]; s1 ^= w[40][1]; s2 ^= w[40][2]; s3 ^= w[40][3]
|
|
s4 ^= w[41][0]; s5 ^= w[41][1]; s6 ^= w[41][2]; s7 ^= w[41][3]
|
|
s8 ^= w[42][0]; s9 ^= w[42][1]; s10^= w[42][2]; s11^= w[42][3]
|
|
s12^= w[43][0]; s13^= w[43][1]; s14^= w[43][2]; s15^= w[43][3]
|
|
box = _inv_sbox
|
|
for rnd in range(9, 0, -1):
|
|
t0=box[s0]; t1=box[s13]; t2=box[s10]; t3=box[s7]
|
|
t4=box[s4]; t5=box[s1]; t6=box[s14]; t7=box[s11]
|
|
t8=box[s8]; t9=box[s5]; t10=box[s2]; t11=box[s15]
|
|
t12=box[s12]; t13=box[s9]; t14=box[s6]; t15=box[s3]
|
|
rk=w[rnd*4]; t0^=rk[0]; t1^=rk[1]; t2^=rk[2]; t3^=rk[3]
|
|
rk=w[rnd*4+1]; t4^=rk[0]; t5^=rk[1]; t6^=rk[2]; t7^=rk[3]
|
|
rk=w[rnd*4+2]; t8^=rk[0]; t9^=rk[1]; t10^=rk[2]; t11^=rk[3]
|
|
rk=w[rnd*4+3]; t12^=rk[0]; t13^=rk[1]; t14^=rk[2]; t15^=rk[3]
|
|
s0 =_mul_e[t0]^_mul_b[t1]^_mul_d[t2]^_mul_9[t3]
|
|
s1 =_mul_9[t0]^_mul_e[t1]^_mul_b[t2]^_mul_d[t3]
|
|
s2 =_mul_d[t0]^_mul_9[t1]^_mul_e[t2]^_mul_b[t3]
|
|
s3 =_mul_b[t0]^_mul_d[t1]^_mul_9[t2]^_mul_e[t3]
|
|
s4 =_mul_e[t4]^_mul_b[t5]^_mul_d[t6]^_mul_9[t7]
|
|
s5 =_mul_9[t4]^_mul_e[t5]^_mul_b[t6]^_mul_d[t7]
|
|
s6 =_mul_d[t4]^_mul_9[t5]^_mul_e[t6]^_mul_b[t7]
|
|
s7 =_mul_b[t4]^_mul_d[t5]^_mul_9[t6]^_mul_e[t7]
|
|
s8 =_mul_e[t8]^_mul_b[t9]^_mul_d[t10]^_mul_9[t11]
|
|
s9 =_mul_9[t8]^_mul_e[t9]^_mul_b[t10]^_mul_d[t11]
|
|
s10=_mul_d[t8]^_mul_9[t9]^_mul_e[t10]^_mul_b[t11]
|
|
s11=_mul_b[t8]^_mul_d[t9]^_mul_9[t10]^_mul_e[t11]
|
|
s12=_mul_e[t12]^_mul_b[t13]^_mul_d[t14]^_mul_9[t15]
|
|
s13=_mul_9[t12]^_mul_e[t13]^_mul_b[t14]^_mul_d[t15]
|
|
s14=_mul_d[t12]^_mul_9[t13]^_mul_e[t14]^_mul_b[t15]
|
|
s15=_mul_b[t12]^_mul_d[t13]^_mul_9[t14]^_mul_e[t15]
|
|
t0=box[s0]; t1=box[s13]; t2=box[s10]; t3=box[s7]
|
|
t4=box[s4]; t5=box[s1]; t6=box[s14]; t7=box[s11]
|
|
t8=box[s8]; t9=box[s5]; t10=box[s2]; t11=box[s15]
|
|
t12=box[s12]; t13=box[s9]; t14=box[s6]; t15=box[s3]
|
|
rk=w[0]; t0^=rk[0]; t1^=rk[1]; t2^=rk[2]; t3^=rk[3]
|
|
rk=w[1]; t4^=rk[0]; t5^=rk[1]; t6^=rk[2]; t7^=rk[3]
|
|
rk=w[2]; t8^=rk[0]; t9^=rk[1]; t10^=rk[2]; t11^=rk[3]
|
|
rk=w[3]; t12^=rk[0]; t13^=rk[1]; t14^=rk[2]; t15^=rk[3]
|
|
return bytes([t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12,t13,t14,t15])
|
|
|
|
def _aes_cbc_decrypt(data, key, iv):
|
|
if not data or len(data) % 16: return data
|
|
n = len(data) // 16
|
|
w = _key_schedule(key)
|
|
out = bytearray(len(data))
|
|
prev = iv
|
|
for i in range(n):
|
|
block = data[i*16:(i+1)*16]
|
|
dec = _dec_block(block, w)
|
|
for j in range(16):
|
|
out[i*16+j] = dec[j] ^ prev[j]
|
|
prev = block
|
|
pad = out[-1]
|
|
if 1 <= pad <= 16:
|
|
return bytes(out[:-pad])
|
|
return bytes(out)
|
|
|
|
|
|
# ===== 全局代理服务(封面图走代理绕过 SSL) =====
|
|
_proxy_port = 0
|
|
_proxy_started = False
|
|
_proxy_session = requests.Session()
|
|
_proxy_session.verify = False
|
|
_proxy_headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Referer': 'https://mjv011.com/',
|
|
}
|
|
|
|
class _ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
daemon_threads = True
|
|
|
|
class _ProxyHandler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
try:
|
|
real_url = unquote(self.path[1:])
|
|
if not real_url or not real_url.startswith('http'):
|
|
self.send_response(404); self.end_headers(); return
|
|
r = _proxy_session.get(real_url, headers=_proxy_headers, timeout=20, verify=False)
|
|
ct = r.headers.get('Content-Type', 'image/jpeg')
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', ct)
|
|
self.send_header('Content-Length', len(r.content))
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
self.wfile.write(r.content)
|
|
except BrokenPipeError:
|
|
pass
|
|
except Exception:
|
|
self.send_response(404); self.end_headers()
|
|
def log_message(self, format, *args): pass
|
|
|
|
def _find_free_port():
|
|
import socket
|
|
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sk.bind(('127.0.0.1', 0))
|
|
port = sk.getsockname()[1]
|
|
sk.close()
|
|
return port
|
|
|
|
def _start_proxy():
|
|
global _proxy_port, _proxy_started
|
|
if _proxy_started: return
|
|
_proxy_port = _find_free_port()
|
|
server = _ThreadedHTTPServer(('127.0.0.1', _proxy_port), _ProxyHandler)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
_proxy_started = True
|
|
|
|
|
|
# ===== 内容类型判断 =====
|
|
def _is_novel(tid_or_vid):
|
|
if not tid_or_vid: return False
|
|
return tid_or_vid.startswith('novel')
|
|
|
|
def _is_image(tid_or_vid):
|
|
if not tid_or_vid: return False
|
|
return any(tid_or_vid.startswith(p) for p in ['18H', 'doujin', 'cg', 'cwp'])
|
|
|
|
|
|
# ===== Spider =====
|
|
class Spider(Spider):
|
|
session = requests.Session()
|
|
host = 'https://mjv011.com'
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
'Referer': 'https://mjv011.com/',
|
|
}
|
|
|
|
def getName(self): return "mjv011"
|
|
|
|
def isVideoFormat(self, url):
|
|
if not url: return False
|
|
return '.m3u8' in url or '.mp4' in url or '.ts' in url
|
|
|
|
def manualVideoCheck(self): return False
|
|
def destroy(self): pass
|
|
|
|
def localProxy(self, param):
|
|
return [404, 'text/plain', '']
|
|
|
|
def init(self, extend=""):
|
|
self.session.verify = False
|
|
_start_proxy()
|
|
try:
|
|
self.session.get(
|
|
f'{self.host}/zh/chinese_IamOverEighteenYearsOld/19/index.html',
|
|
headers=self.headers, timeout=15, verify=False)
|
|
except Exception:
|
|
pass
|
|
|
|
def _proxy_url(self, url):
|
|
if not url: return ''
|
|
if url.startswith('http://127.0.0.1'):
|
|
return url
|
|
return f'http://127.0.0.1:{_proxy_port}/{quote(url, safe="")}'
|
|
|
|
def _fetch(self, url):
|
|
try:
|
|
r = self.session.get(url, headers=self.headers, timeout=20, verify=False)
|
|
r.encoding = 'utf-8'
|
|
if r.status_code == 200:
|
|
text = r.text
|
|
# 年龄验证页检测:自动绕过
|
|
if len(text) < 3000 and ('同意(enter)' in text or 'IamOverEighteenYearsOld' in text):
|
|
self.session.get(
|
|
f'{self.host}/zh/chinese_IamOverEighteenYearsOld/19/index.html',
|
|
headers=self.headers, timeout=20, verify=False)
|
|
r = self.session.get(url, headers=self.headers, timeout=20, verify=False)
|
|
r.encoding = 'utf-8'
|
|
if r.status_code == 200:
|
|
return r.text
|
|
return ''
|
|
return text
|
|
return ''
|
|
except Exception:
|
|
return ''
|
|
|
|
# ===== 列表解析 =====
|
|
def _parse_text_posts(self, text):
|
|
"""小说列表(无图)"""
|
|
items = []
|
|
for m in re.finditer(r"<div class='post'>\s*<div class='con'>\s*<h3[^>]*><a[^>]*href=\"([^\"]+)\"[^>]*>([^<]+)</a></h3>", text):
|
|
href, title = m.groups()
|
|
mm = re.search(r'/([^/]+)_content/(\d+)/([^/]+)\.html', href)
|
|
if not mm: continue
|
|
ctype, vid, slug = mm.group(1), mm.group(2), mm.group(3)
|
|
items.append({
|
|
'vod_id': f'{ctype}#{vid}#{slug}',
|
|
'vod_name': title.strip(),
|
|
'vod_pic': '',
|
|
'vod_remarks': '',
|
|
})
|
|
return items
|
|
|
|
def _parse_posts(self, text, tid=''):
|
|
"""根据分类解析列表:小说单独处理,其他统一按带图 post 处理"""
|
|
if _is_novel(tid):
|
|
return self._parse_text_posts(text)
|
|
|
|
items = []
|
|
for m in re.finditer(r"<div class='post'>\s*<a[^>]*href=\"([^\"]+)\"[^>]*><img[^>]*src='([^']+)'[^>]*>\s*</a>\s*<div class='con'>\s*<h3[^>]*><a[^>]*>([^<]+)</a></h3>(?:\s*<div class='meta'>([^<]*)</div>)?", text):
|
|
href, pic, title, date = m.groups()
|
|
date = date or ''
|
|
mm = re.search(r'/([^/]+)_content/(\d+)/([^/]+)\.html', href)
|
|
if not mm: continue
|
|
ctype, vid, slug = mm.group(1), mm.group(2), mm.group(3)
|
|
items.append({
|
|
'vod_id': f'{ctype}#{vid}#{slug}',
|
|
'vod_name': title.strip(),
|
|
'vod_pic': self._proxy_url(pic),
|
|
'vod_remarks': date.strip(),
|
|
})
|
|
return items
|
|
|
|
def _build_cat_url(self, tid, page, extend=None):
|
|
if extend and extend.get('sub'):
|
|
sub = quote(unquote(extend['sub']), safe='/()')
|
|
return f'{self.host}/zh/{sub}/{page}.html'
|
|
if tid.startswith('search_'):
|
|
kw = tid[7:]
|
|
return f'{self.host}/zh/chinese_search/all/{kw}/{page}.html'
|
|
# TVBox 可能截断 type_id,自动补全
|
|
if tid.endswith('_random'):
|
|
tid = tid + '/all'
|
|
elif '/' not in tid:
|
|
tid = tid + '_random/all'
|
|
if page == 1:
|
|
return f'{self.host}/zh/{tid}/index.html'
|
|
return f'{self.host}/zh/{tid}/index_{page}.html'
|
|
|
|
# ===== 接口 =====
|
|
def homeContent(self, filter):
|
|
classes = [
|
|
{'type_id': 'chinese_random/all', 'type_name': '中文字幕'},
|
|
{'type_id': 'censored_random/all', 'type_name': '有码'},
|
|
{'type_id': 'uncensored_random/all', 'type_name': '无码'},
|
|
{'type_id': 'reducing-mosaic_random/all', 'type_name': '无码破解'},
|
|
{'type_id': 'amateurjav_random/all', 'type_name': '素人'},
|
|
{'type_id': 'animation_random/all', 'type_name': 'H动画'},
|
|
{'type_id': 'dt_random/all', 'type_name': '国产自拍'},
|
|
{'type_id': '18H_random/all', 'type_name': '18H漫画'},
|
|
{'type_id': 'cg_random/all', 'type_name': '写真图集'},
|
|
{'type_id': 'cwp_random/all', 'type_name': '国产写真'},
|
|
{'type_id': 'novel_random/all', 'type_name': '小说'},
|
|
]
|
|
|
|
filter = self._build_filters()
|
|
return {'class': classes, 'filters': filter, 'type': '影视'}
|
|
|
|
def _build_filters(self):
|
|
"""构造 TVBox filter 格式(网站左侧导航真实子分类)"""
|
|
filters = {}
|
|
chinese_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': '随机', 'v': 'chinese_randomall/all'},
|
|
{'n': '类别清单', 'v': 'chinese_categorylist/list'},
|
|
]
|
|
filters['chinese_random/all'] = [{'key': 'sub', 'name': '中文字幕', 'value': chinese_opts}]
|
|
uncensored_opts: list[dict[str, str]] = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': '一本道(1pondo)', 'v': 'uncensored_makersr/32/一本道(1pondo)'},
|
|
{'n': 'カリビアンコム(Caribbeancom)', 'v': 'uncensored_makersr/30/カリビアンコム(Caribbeancom)'},
|
|
{'n': 'カリビアンコムPPV', 'v': 'uncensored_makersr/40/カリビアンコムPPV(Caribbeancompr)'},
|
|
{'n': '天然むすめ(10musume)', 'v': 'uncensored_makersr/31/天然むすめ(10musume)'},
|
|
{'n': 'HEYZO', 'v': 'uncensored_makersr/17/HEYZO'},
|
|
{'n': '東京熱(Tokyo Hot)', 'v': 'uncensored_makersr/29/東京熱(Tokyo Hot)'},
|
|
{'n': 'ガチん娘!(Gachinco)', 'v': 'uncensored_makersr/35/ガチん娘!(Gachinco)'},
|
|
{'n': 'パコパコママ(pacopacomama)', 'v': 'uncensored_makersr/36/パコパコママ(pacopacomama)'},
|
|
{'n': 'エッチな4610', 'v': 'uncensored_makersr/34/エッチな4610'},
|
|
{'n': '人妻斬り0930', 'v': 'uncensored_makersr/38/人妻斬り0930'},
|
|
{'n': 'エッチな0930', 'v': 'uncensored_makersr/39/エッチな0930'},
|
|
{'n': 'トリプルエックス(XXX-AV)', 'v': 'uncensored_makersr/126/トリプルエックス (XXX-AV)'},
|
|
]
|
|
filters['uncensored_random/all'] = [{'key': 'sub', 'name': '厂商', 'value': uncensored_opts}]
|
|
|
|
animation_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': 'H有码动画', 'v': 'CensoredAnimation_random/all'},
|
|
{'n': 'H无码动画', 'v': 'UncensoredAnimation_random/all'},
|
|
{'n': 'H_3D动画', 'v': 'tdAnimation_random/all'},
|
|
]
|
|
filters['animation_random/all'] = [{'key': 'sub', 'name': '动漫', 'value': animation_opts}]
|
|
|
|
comic_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': '短篇同人', 'v': 'doujin_random/all'},
|
|
]
|
|
filters['18H_random/all'] = [{'key': 'sub', 'name': '漫画', 'value': comic_opts}]
|
|
|
|
cg_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': 'Bejean On Line', 'v': 'cg_search/all/Bejean On Line'},
|
|
{'n': 'Bomb.tv', 'v': 'cg_search/all/Bomb.tv'},
|
|
{'n': 'DGC', 'v': 'cg_search/all/DGC'},
|
|
{'n': 'Graphis Gals', 'v': 'cg_search/all/Graphis Gals'},
|
|
{'n': 'Graphis Hatsunugi', 'v': 'cg_search/all/Graphis Hatsunugi'},
|
|
{'n': 'image.tv', 'v': 'cg_search/all/image.tv'},
|
|
{'n': 'Sabra.net', 'v': 'cg_search/all/Sabra.net'},
|
|
{'n': 'S-Cute', 'v': 'cg_search/all/S-Cute'},
|
|
{'n': 'X-City', 'v': 'cg_search/all/X-City'},
|
|
{'n': 'YS Web', 'v': 'cg_search/all/YS Web'},
|
|
]
|
|
filters['cg_random/all'] = [{'key': 'sub', 'name': '系列', 'value': cg_opts}]
|
|
|
|
cwp_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': '3AGirl AAA女郎', 'v': 'cwp_search/all/3AGirl AAA女郎'},
|
|
{'n': 'ROSI寫真', 'v': 'cwp_search/all/ROSI寫真'},
|
|
{'n': 'RU1MM 如壹寫真', 'v': 'cwp_search/all/RU1MM 如壹寫真'},
|
|
{'n': 'DISI第四印象', 'v': 'cwp_search/all/DISI第四印象'},
|
|
]
|
|
filters['cwp_random/all'] = [{'key': 'sub', 'name': '系列', 'value': cwp_opts}]
|
|
|
|
novel_opts = [
|
|
{'n': '全部', 'v': ''},
|
|
{'n': '學生校園', 'v': 'novel_search/all/學生校園'},
|
|
{'n': '職場激情', 'v': 'novel_search/all/職場激情'},
|
|
{'n': '經驗故事', 'v': 'novel_search/all/經驗故事'},
|
|
{'n': '暴力虐待', 'v': 'novel_search/all/暴力虐待'},
|
|
{'n': '不倫戀情', 'v': 'novel_search/all/不倫戀情'},
|
|
{'n': '群體換伴', 'v': 'novel_search/all/群體換伴'},
|
|
{'n': '人妻熟女', 'v': 'novel_search/all/人妻熟女'},
|
|
{'n': '科學幻想', 'v': 'novel_search/all/科學幻想'},
|
|
{'n': '其他故事', 'v': 'novel_search/all/其他故事'},
|
|
{'n': '玄幻仙俠', 'v': 'novel_search/all/玄幻仙俠'},
|
|
{'n': '動漫修改', 'v': 'novel_search/all/動漫修改'},
|
|
{'n': '長篇連載', 'v': 'novel_search/all/長篇連載'},
|
|
]
|
|
filters['novel_random/all'] = [{'key': 'sub', 'name': '主题', 'value': novel_opts}]
|
|
return filters
|
|
|
|
def homeVideoContent(self):
|
|
text = self._fetch(f'{self.host}/zh/chinese_IamOverEighteenYearsOld/19/index.html')
|
|
items = self._parse_posts(text, 'content_news/all')
|
|
return {'list': items}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
try:
|
|
return self._categoryContent_inner(tid, pg, filter, extend)
|
|
except Exception:
|
|
return {'list': [], 'page': int(pg) if pg else 1, 'pagecount': 1, 'limit': 0, 'total': 0}
|
|
|
|
def _categoryContent_inner(self, tid, pg, filter, extend):
|
|
if isinstance(extend, str):
|
|
try:
|
|
extend = json.loads(extend)
|
|
except Exception:
|
|
extend = {}
|
|
if not extend:
|
|
extend = {}
|
|
page = int(pg) if pg else 1
|
|
# ===== 类别清单(中文字幕的子分类): 文件夹形式(仿 18av) =====
|
|
if extend.get('sub') == 'chinese_categorylist/list':
|
|
return self._category_folder(page)
|
|
if '@' in str(tid):
|
|
return self._folder_detail(tid, page)
|
|
url = self._build_cat_url(tid, page, extend)
|
|
# 子分类筛选时按子分类内容类型解析
|
|
parse_tid = tid
|
|
if parse_tid.endswith('_random'):
|
|
parse_tid = parse_tid + '/all'
|
|
elif '/' not in parse_tid:
|
|
parse_tid = parse_tid + '_random/all'
|
|
if extend and extend.get('sub'):
|
|
sub = extend['sub']
|
|
if _is_novel(sub):
|
|
parse_tid = 'novel_random/all'
|
|
elif _is_image(sub):
|
|
parse_tid = sub.split('_')[0] + '_random/all' if '_' in sub else 'cg_random/all'
|
|
text = self._fetch(url)
|
|
items = self._parse_posts(text, parse_tid)
|
|
return {'list': items, 'page': page, 'pagecount': page + 1,
|
|
'limit': len(items), 'total': page * len(items) + 1}
|
|
|
|
# ===== 类别清单(文件夹形式,仿 18av) =====
|
|
def _category_folder(self, pg):
|
|
"""类别清单: 抓取子分类索引页,以 folder 形式返回,点文件夹进入视频列表"""
|
|
html = self._fetch(f'{self.host}/zh/chinese_categorylist/list/index.html')
|
|
lst = []
|
|
for m in re.finditer(
|
|
r"<a[^>]+href=[\"']([^\"']*chinese_category/(\d+)/([^\"'/]+)/[^\"']*)[\"'][^>]*>([^<]+)</a>",
|
|
html, re.I):
|
|
category_url = m.group(1).strip()
|
|
category_name = m.group(4).strip()
|
|
if category_url.startswith(self.host):
|
|
category_url = category_url[len(self.host):]
|
|
lst.append({
|
|
'vod_id': category_url + '@',
|
|
'vod_name': category_name,
|
|
'vod_pic': self.host.rstrip('/') + '/images/1v.jpg',
|
|
'vod_tag': 'folder',
|
|
'vod_remarks': '分类',
|
|
})
|
|
return {'list': lst, 'page': 1, 'pagecount': 1,
|
|
'limit': len(lst), 'total': len(lst)}
|
|
|
|
def _folder_detail(self, tid, pg):
|
|
"""@ 文件夹: 去掉 @, 把子分类链接当作真实 URL 拉取视频列表(支持翻页)"""
|
|
tid = str(tid).replace('@', '')
|
|
url = self.host.rstrip('/') + tid
|
|
if re.search(r'/\d+\.html$', url):
|
|
url = re.sub(r'/\d+\.html$', '/' + str(pg) + '.html', url)
|
|
else:
|
|
base = url.rstrip('/')
|
|
url = base + ('/index.html' if pg <= 1 else f'/index_{pg}.html')
|
|
html = self._fetch(url)
|
|
lst = self._parse_posts(html, 'chinese_category')
|
|
return {'list': lst, 'page': pg, 'pagecount': 9999,
|
|
'limit': len(lst), 'total': 9999}
|
|
|
|
def detailContent(self, ids):
|
|
try:
|
|
return self._detailContent_inner(ids)
|
|
except Exception:
|
|
return {'list': []}
|
|
|
|
def _detailContent_inner(self, ids):
|
|
vid = str(ids[0] if isinstance(ids, list) else ids)
|
|
ctype, num, slug = vid.split('#', 2)
|
|
if _is_novel(ctype):
|
|
return self._novel_detail(vid, ctype, num, slug)
|
|
elif _is_image(ctype):
|
|
return self._image_detail(vid, ctype, num, slug)
|
|
else:
|
|
return self._video_detail(vid, ctype, num, slug)
|
|
|
|
def _video_detail(self, vid, ctype, num, slug):
|
|
"""视频详情(有播放器解密)"""
|
|
url = f'{self.host}/zh/{ctype}_content/{num}/{slug}.html'
|
|
text = self._fetch(url)
|
|
if not text: return {'list': []}
|
|
|
|
title = ''
|
|
m = re.search(r'<h1[^>]*>(.*?)</h1>', text, re.S)
|
|
if m: title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
|
if not title:
|
|
m = re.search(r'<title>([^<]+)</title>', text)
|
|
if m: title = m.group(1).strip()
|
|
|
|
cover = ''
|
|
m = re.search(r'"thumbnailUrl"\s*:\s*"([^"]+)"', text)
|
|
if m: cover = m.group(1)
|
|
if not cover:
|
|
m = re.search(r"<meta[^>]*property=\"og:image\"[^>]*content=\"([^\"]+)\"", text)
|
|
if m: cover = m.group(1)
|
|
if not cover:
|
|
m = re.search(r"<div class='post'>.*?<img[^>]*src='([^']+)'", text, re.S)
|
|
if m: cover = m.group(1)
|
|
|
|
m = re.search(r'hadeedg252=(\d+)', text)
|
|
if not m: return {'list': []}
|
|
hadeedg252 = int(m.group(1))
|
|
m = re.search(r'hcdeedg252=(\d+)', text)
|
|
if not m: return {'list': []}
|
|
hcdeedg252 = int(m.group(1))
|
|
m = re.search(r"var argdeqweqweqwe = '([^']+)'", text)
|
|
if not m: return {'list': []}
|
|
aes_key = m.group(1)
|
|
m = re.search(r"var hdddedg252 = '([^']+)'", text)
|
|
if not m: return {'list': []}
|
|
aes_iv = m.group(1)
|
|
|
|
mm = re.search(r"mvarr\['10_1'\]=(\[.*?\]);", text, re.S)
|
|
if not mm: return {'list': []}
|
|
mvarr_str = mm.group(1)
|
|
items = re.findall(r"\['([^']*)','([^']*)','([^']*)','([^']*)','([^']*)','([^']*)'\]", mvarr_str)
|
|
if not items: return {'list': []}
|
|
|
|
urls = []
|
|
for iframe_id, enc, html, prefix, empty, label in items:
|
|
if not enc or not prefix: continue
|
|
pid = self._decrypt_id(enc, hadeedg252, hcdeedg252, aes_key, aes_iv)
|
|
if not pid: continue
|
|
for res, label_name in [('1080', '1080P'), ('720', '720P'), ('480', '480P')]:
|
|
urls.append(f'{label_name}${num}|{slug}|{pid}|{res}')
|
|
|
|
if not urls: return {'list': []}
|
|
|
|
vod = {
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': self._proxy_url(cover),
|
|
'vod_content': '',
|
|
'vod_remarks': '',
|
|
'vod_play_from': 'mjv011',
|
|
'vod_play_url': '#'.join(urls),
|
|
}
|
|
return {'list': [vod]}
|
|
|
|
def _novel_detail(self, vid, ctype, num, slug):
|
|
"""小说详情"""
|
|
url = f'{self.host}/zh/{ctype}_content/{num}/{slug}.html'
|
|
text = self._fetch(url)
|
|
if not text: return {'list': []}
|
|
|
|
title = ''
|
|
m = re.search(r'<h1[^>]*>(.*?)</h1>', text, re.S)
|
|
if m: title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
|
|
|
content = ''
|
|
m = re.search(r"id=['\"]novel_content_txtsize['\"][^>]*>(.*?)</div>", text, re.S)
|
|
if m:
|
|
raw = m.group(1)
|
|
content = re.sub(r'<[^>]+>', '', raw)
|
|
content = re.sub(r' ', ' ', content)
|
|
content = re.sub(r'\s+', ' ', content).strip()
|
|
if not content:
|
|
m = re.search(r"<span class='content_18h_wpcg'>([\s\S]*?)</span>\s*<div class='contents'", text)
|
|
if m:
|
|
raw = m.group(1)
|
|
content = re.sub(r'<[^>]+>', '', raw)
|
|
content = re.sub(r' ', ' ', content)
|
|
content = re.sub(r'\s+', ' ', content).strip()
|
|
|
|
if len(content) > 3000:
|
|
content = content[:3000] + '...'
|
|
|
|
novel_json = json.dumps({'title': title, 'content': content}, ensure_ascii=False)
|
|
play_url = f'阅读$novel://{novel_json}'
|
|
|
|
vod = {
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': '',
|
|
'vod_content': '',
|
|
'vod_remarks': '',
|
|
'vod_play_from': '小说',
|
|
'vod_play_url': play_url,
|
|
'vod_tag': 'text',
|
|
'vod_player': '书',
|
|
}
|
|
return {'list': [vod]}
|
|
|
|
def _image_detail(self, vid, ctype, num, slug):
|
|
"""图片详情(写真/漫画)"""
|
|
url = f'{self.host}/zh/{ctype}_content/{num}/{slug}.html'
|
|
text = self._fetch(url)
|
|
if not text: return {'list': []}
|
|
|
|
title = ''
|
|
m = re.search(r'<h1[^>]*>(.*?)</h1>', text, re.S)
|
|
if m: title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
|
|
|
cover = ''
|
|
m = re.search(r'"thumbnailUrl"\s*:\s*"([^"]+)"', text)
|
|
if m: cover = m.group(1)
|
|
|
|
# 提取详情页所有大图(第一页已包含全部)
|
|
all_imgs = re.findall(r"src=['\"]([^'\"]*eemmhh02\.com/[^'\"]+\.(?:jpg|png|webp))['\"]", text, re.I)
|
|
big_imgs = []
|
|
seen = set()
|
|
for img in all_imgs:
|
|
if img in seen:
|
|
continue
|
|
seen.add(img)
|
|
big_imgs.append(self._proxy_url(img))
|
|
|
|
if not big_imgs:
|
|
return {'list': []}
|
|
|
|
pics = '&&'.join(big_imgs)
|
|
play_url = f'查看$pics://{pics}'
|
|
|
|
vod = {
|
|
'vod_id': vid,
|
|
'vod_name': title,
|
|
'vod_pic': self._proxy_url(cover),
|
|
'vod_content': f'共 {len(big_imgs)} 张图片',
|
|
'vod_remarks': str(len(big_imgs)) + 'P',
|
|
'vod_play_from': '图片',
|
|
'vod_play_url': play_url,
|
|
'vod_tag': 'image',
|
|
'vod_player': '画',
|
|
}
|
|
return {'list': [vod]}
|
|
|
|
def _decrypt_id(self, enc, xor_key, base, aes_key, aes_iv):
|
|
try:
|
|
sep = chr(base + 97)
|
|
parts = enc.split(sep)
|
|
s1 = ''.join(chr(int(p, base) ^ xor_key) for p in parts if p)
|
|
data = base64.b64decode(s1)
|
|
plain = _aes_cbc_decrypt(data, aes_key.encode(), aes_iv.encode())
|
|
return plain.decode('utf-8')
|
|
except Exception:
|
|
return ''
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
try:
|
|
return self._searchContent_inner(key, quick, pg)
|
|
except Exception:
|
|
return {'list': [], 'page': int(pg) if pg else 1, 'pagecount': 1, 'limit': 0, 'total': 0}
|
|
|
|
def _searchContent_inner(self, key, quick, pg="1"):
|
|
page = int(pg) if pg else 1
|
|
return self._categoryContent_inner(f'search_{key}', page, False, {})
|
|
|
|
def playerContent(self, flag, id, vipFlags=None):
|
|
try:
|
|
return self._playerContent_inner(flag, id, vipFlags)
|
|
except Exception:
|
|
return {'parse': 0, 'url': '', 'header': {}, 'position': '0'}
|
|
|
|
def _playerContent_inner(self, flag, id, vipFlags=None):
|
|
# 小说
|
|
if id.startswith('novel://'):
|
|
return {'parse': 0, 'url': id, 'header': '', 'vod_player': '书'}
|
|
# 图片
|
|
if id.startswith('pics://'):
|
|
return {'parse': 0, 'playUrl': '', 'url': id, 'header': self.headers}
|
|
# 视频播放
|
|
num, slug, pid, res = id.split('|', 3)
|
|
url = f'{self.host}/js/player/play.php?numresolution={res}&lo=on&id={pid}'
|
|
text = self._fetch(url)
|
|
m3u8 = ''
|
|
if text:
|
|
mm = re.search(r'videoSources\s*=\s*(\[.*?\]);', text, re.S)
|
|
if mm:
|
|
arr = mm.group(1)
|
|
sources = re.findall(r"src:\s*'([^']+)'[^}]*?size:\s*(\d+)", arr, re.S)
|
|
if sources:
|
|
target = int(res) if str(res).isdigit() else 0
|
|
for u, s in sources:
|
|
if int(s) == target:
|
|
m3u8 = u
|
|
break
|
|
if not m3u8:
|
|
m3u8 = sources[0][0]
|
|
if not m3u8:
|
|
mm = re.search(r'https?://[^\s"<>\']+?\.m3u8', text)
|
|
if mm: m3u8 = mm.group(0)
|
|
return {'parse': 0, 'url': m3u8, 'header': {'Referer': self.host + '/'}, 'position': '0'} |