Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-26 16:05:01 +00:00
parent 3ec7ec4d08
commit c94ef693df
22 changed files with 34176 additions and 13129 deletions
+16 -4
View File
@@ -803,10 +803,10 @@
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/黑料网.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞[成人]",
"key": "真实人妻",
"name": "🐬真实人妻.py|🔞[成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/真实人妻.py"
},
{
"key": "猫咪TV",
@@ -940,6 +940,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
},
{
"key": "AdultpornaAV",
"name": "🐬AdultpornaAV.py|🔞[成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/AdultpornaAV.py"
},
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞[成人]",
@@ -1006,9 +1012,15 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/麻豆免费在线播放.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞[采集成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
{
"key": "大地采集",
"name": "🐬大地av|🔞[成人]",
"name": "🐬大地av|🔞[采集成人]",
"type": 0,
"api": "https://dadiapi.com/apple_m3u8.php",
"playUrl": "https://play.dadiapi.com/watch?url=",
+16 -4
View File
@@ -546,10 +546,10 @@
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/黑料网.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞[成人]",
"key": "真实人妻",
"name": "🐬真实人妻.py|🔞[成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/真实人妻.py"
},
{
"key": "猫咪TV",
@@ -683,6 +683,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
},
{
"key": "AdultpornaAV",
"name": "🐬AdultpornaAV.py|🔞[成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/AdultpornaAV.py"
},
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞[成人]",
@@ -748,7 +754,13 @@
"name": "🐬麻豆免费在线播放.py|🔞[成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/麻豆免费在线播放.py"
}
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞[采集成人]",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
],
"lives": [
{
+18 -6
View File
@@ -735,18 +735,18 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/黑料网.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
{
"key": "mtvm",
"name": "🐬蜜桃视频.py(羊壳观看)|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
},
{
"key": "真实人妻",
"name": "🐬真实人妻.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/真实人妻.py"
},
{
"key": "猫咪TV",
"name": "🐬猫咪TV.py|🔞",
@@ -879,6 +879,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
},
{
"key": "AdultpornaAV",
"name": "🐬AdultpornaAV.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/AdultpornaAV.py"
},
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞",
@@ -945,6 +951,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/麻豆免费在线播放.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
{
"key": "大地采集",
"name": "🐬大地av|🔞",
+18 -6
View File
@@ -568,18 +568,18 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/黑料网.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
{
"key": "mtvm",
"name": "🐬蜜桃视频.py(羊壳观看)|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
},
{
"key": "真实人妻",
"name": "🐬真实人妻.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/真实人妻.py"
},
{
"key": "猫咪TV",
"name": "🐬猫咪TV.py|🔞",
@@ -712,6 +712,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
},
{
"key": "AdultpornaAV",
"name": "🐬AdultpornaAV.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/AdultpornaAV.py"
},
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞",
@@ -777,6 +783,12 @@
"name": "🐬麻豆免费在线播放.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/麻豆免费在线播放.py"
},
{
"key": "采集聚合成人",
"name": "🐬采集聚合成人.py|🔞",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
}
],
"parses": [
+118
View File
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
import re,json,requests
from urllib.parse import quote,urljoin
try:
from base.spider import Spider as BaseSpider
except Exception:
class BaseSpider:
def __init__(self):
return None
class Spider(BaseSpider):
def __init__(self):
self.host='https://www.adultporna-av107.com'
self.session=requests.Session()
self.headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36','Referer':self.host+'/zzzz','Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'}
self.classes=[('每日最新','/topic/'),('国产视频','/t/163/'),('网曝黑料','/t/232/'),('主播大秀','/t/236/'),('AV解说','/t/233/'),('国产自拍','/t/48/'),('抖阴视频','/t/231/'),('国模私拍','/t/45/'),('空姐模特','/t/67/'),('91制片厂','/t/131/'),('糖心VLOG','/t/128/'),('日本有码','/label/sortjp/'),('国产传媒','/label/sortcnseries/'),('番号专区','/label/sortseries/')]
def init(self,extend=''):
return None
def getName(self):
return 'AdultPorna'
def isVideoFormat(self,url):
return str(url).split('?')[0].lower().endswith(('.m3u8','.mp4','.flv','.avi','.mkv','.mov'))
def manualVideoCheck(self):
return True
def destroy(self):
return None
def _get(self,url):
u=url if str(url).startswith('http') else urljoin(self.host,url)
r=self.session.get(u,headers=self.headers,timeout=15,verify=False)
r.encoding=r.apparent_encoding or 'utf-8'
return r.text
def _url(self,u):
return urljoin(self.host,u.replace('\\/','/')) if u else ''
def _txt(self,s):
return re.sub(r'\s+',' ',re.sub(r'<[^>]+>',' ',s or '')).strip()
def _pic(self,s):
m=re.search(r'<img[^>]+(?:data-original|data-src|data-lazyload|src)=["\']([^"\']+)["\']',s,re.I)
return self._url(m.group(1)) if m else ''
def _parse_list(self,html):
arr=[];seen=set()
blocks=re.findall(r'<a[^>]+href=["\'](/voddetail/(\d+)/?)["\'][^>]*>.*?</a>',html,re.I|re.S)
for href,vid in blocks:
if vid in seen: continue
i=html.find(href);chunk=html[max(0,i-800):i+2500]
tm=re.search(r'<a[^>]+href=["\']'+re.escape(href)+r'["\'][^>]*(?:title=["\']([^"\']+)["\'])?',chunk,re.I|re.S)
am=re.search(r'<img[^>]+alt=["\']([^"\']+)["\']',chunk,re.I)
name=(tm.group(1) if tm and tm.group(1) else '') or (am.group(1) if am else '') or vid
sm=re.search(r'<small[^>]*>(.*?)</small>',chunk,re.I|re.S)
arr.append({'vod_id':vid,'vod_name':self._txt(name),'vod_pic':self._pic(chunk),'vod_remarks':self._txt(sm.group(1)) if sm else ''})
seen.add(vid)
return arr
def homeContent(self,filter=False):
html=self._get('/zzzz')
return {'class':[{'type_id':i,'type_name':n} for n,i in self.classes],'list':self._parse_list(html),'filters':{}}
def homeVideoContent(self):
return {'list':self._parse_list(self._get('/zzzz'))}
def categoryContent(self,tid,pg,filter,extend):
tid=str(tid);pg=str(pg or '1')
path=tid if tid.startswith('/') else '/t/'+tid.strip('/')+'/'
if pg!='1': path=path.rstrip('/')+'/page/'+pg+'/'
html=self._get(path)
data=self._parse_list(html)
return {'list':data,'page':int(pg),'pagecount':999 if data else int(pg),'limit':24,'total':999999}
def detailContent(self,ids):
vid=str(ids[0]).strip('/').split('/')[-1]
html=self._get('/voddetail/'+vid+'/')
name='';pic='';content=''
for p in [r'<meta[^>]+property=["\']og:title["\'][^>]+content=["\']([^"\']+)["\']',r'<h1[^>]*>(.*?)</h1>',r'title=["\']([^"\']+)["\']']:
m=re.search(p,html,re.I|re.S)
if m and self._txt(m.group(1)): name=self._txt(m.group(1));break
m=re.search(r'<meta[^>]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']',html,re.I)
pic=self._url(m.group(1)) if m else self._pic(html)
em=re.search(r'(?:剧情|简介|介绍|详情)[^<]*</[^>]+>\s*<[^>]+>(.*?)</',html,re.I|re.S)
content=self._txt(em.group(1)) if em else name
plays=[]
for href in re.findall(r'href=["\'](/v/'+re.escape(vid)+r'/?[^"\']*)["\']',html,re.I):
if href not in plays: plays.append(href)
if not plays: plays=['/v/'+vid+'/']
vod={'vod_id':vid,'vod_name':name or vid,'vod_pic':pic,'type_name':'','vod_year':'','vod_area':'','vod_remarks':'','vod_actor':'','vod_director':'','vod_content':content,'vod_play_from':'播放','vod_play_url':'#'.join(['%d集$%s'%(i+1,self._url(u)) for i,u in enumerate(plays)])}
return {'list':[vod]}
def searchContent(self,key,quick,pg='1'):
html=self._get('/s/?wd='+quote(str(key)))
data=self._parse_list(html)
return {'list':data,'page':int(pg or 1),'pagecount':1,'limit':24,'total':len(data)}
def playerContent(self,flag,id,vipFlags):
pid=str(id)
url=pid if pid.startswith('http') else self._url('/v/'+pid.strip('/').split('/')[-1]+'/')
if self.isVideoFormat(url): return {'parse':0,'playUrl':'','url':url,'header':self.headers}
html=self._get(url)
m=re.search(r'player_aaaa\s*=\s*(\{.*?\})\s*<',html,re.I|re.S) or re.search(r'player_aaaa\s*=\s*(\{.*?\})',html,re.I|re.S)
play=''
if m:
try:
play=json.loads(m.group(1).replace('\\/','/')).get('url','')
except json.JSONDecodeError:
mm=re.search(r'["\']url["\']\s*:\s*["\']([^"\']+)["\']',m.group(1));play=mm.group(1).replace('\\/','/') if mm else ''
if not play:
mm=re.search(r'(https?:\\?/\\?/[^"\']+?\.(?:m3u8|mp4)[^"\']*)',html,re.I);play=mm.group(1).replace('\\/','/') if mm else url
return {'parse':0 if self.isVideoFormat(play) else 1,'playUrl':'','url':play,'header':self.headers}
def localProxy(self,param):
return [404,'text/plain','']
+228
View File
@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
import re
import urllib.parse
import requests
try:
from base.spider import Spider as BaseSpider
except ImportError:
class BaseSpider:
pass
class Spider(BaseSpider):
BASE_URL = "https://91nt.com"
FALLBACK_URLS = ["https://91nt.com"]
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.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,en;q=0.8",
"Referer": BASE_URL + "/",
}
def __init__(self):
super().__init__()
self.name = "91NT"
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
self._class_cache = None
def init(self, extend="{}"):
return None
def getName(self):
return self.name
def homeContent(self, filter):
html = self._get(self.BASE_URL + "/videos")
return {"class": self._classes(), "filters": {}, "list": self._parse_list(html), "parse": 0, "jx": 0}
def homeVideoContent(self):
return {"list": self._parse_list(self._get(self.BASE_URL + "/videos"))}
def categoryContent(self, tid, pg, filter, extend):
page = self._to_int(pg, 1)
path = str(tid or "/videos").strip()
url = self._fix_url(path if path.startswith("http") else self.BASE_URL + (path if path.startswith("/") else "/" + path))
url = url if page <= 1 else url + ("&page=" if "?" in url else "?page=") + str(page)
data = self._parse_list(self._get(url))
return {"page": page, "pagecount": page if len(data) < 12 else page + 1, "limit": 24, "total": 99999, "list": data, "parse": 0, "jx": 0}
def detailContent(self, ids):
result = {"list": [], "parse": 0, "jx": 0}
if not ids:
return result
url = self._fix_url(ids[0] if str(ids[0]).startswith("http") else self.BASE_URL + "/" + str(ids[0]).strip("/"))
html = self._get(url)
name = self._clean(self._match(html, r'<meta[^>]+property=["\']og:title["\'][^>]+content=["\']([^"\']+)') or self._match(html, r'"title"\s*:\s*"([^"]+)') or self._match(html, r'<title>(.*?)</title>').split("-")[0])
pic = self._match(html, r'<meta[^>]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)') or self._match(html, r'"poster"\s*:\s*"([^"]+)') or self._match(html, r'<img[^>]+(?:data-original|data-src|data-lazy-src|data-lazyload|src)=["\']([^"\']+)')
content = self._clean(self._match(html, r'<meta[^>]+property=["\']og:description["\'][^>]+content=["\']([^"\']+)') or self._match(html, r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)') or name)
duration = self._match(html, r'<meta[^>]+property=["\']video:duration["\'][^>]+content=["\']([^"\']+)')
year = self._match(html, r'<meta[^>]+property=["\']video:release_date["\'][^>]+content=["\']([^"\']+)')[:10]
tags = ",".join([self._clean(x) for x in re.findall(r'<meta[^>]+property=["\']video:tag["\'][^>]+content=["\']([^"\']+)', html, re.S | re.I)])
play = self._play_url(url, html) or url
result["list"].append({"vod_id": url, "vod_name": name, "vod_pic": urllib.parse.urljoin(self.BASE_URL, pic), "type_name": tags, "vod_year": year, "vod_area": "", "vod_remarks": self._dur(duration), "vod_actor": tags, "vod_director": "", "vod_content": content, "vod_play_from": "91NT", "vod_play_url": "正片$" + play})
return result
def searchContent(self, key, quick, pg="1"):
page = self._to_int(pg, 1)
q = urllib.parse.quote(str(key))
url = self.BASE_URL + "/videos/search/" + q
url = url if page <= 1 else url + "?page=" + str(page)
data = self._parse_list(self._get(url))
return {"page": page, "pagecount": page if len(data) < 12 else page + 1, "limit": 24, "total": 99999, "list": data, "parse": 0, "jx": 0}
def playerContent(self, flag, id, vipFlags):
result = {"parse": 0, "playUrl": "", "url": id or "", "jx": 0, "header": {"User-Agent": self.HEADERS["User-Agent"], "Referer": self.BASE_URL + "/"}}
if not id:
return result
if ".m3u8" in id or ".mp4" in id:
return result
play_page = self._fix_url(id if str(id).startswith("http") else self.BASE_URL + "/" + str(id).strip("/"))
html = self._get(play_page)
play = self._play_url(play_page, html)
if play:
result["url"] = play
result["header"] = {"User-Agent": self.HEADERS["User-Agent"], "Referer": play_page, "Origin": self.BASE_URL}
else:
result["url"] = play_page
result["parse"] = 1
return result
def localProxy(self, params):
return None
def _classes(self, html=None):
if self._class_cache:
return self._class_cache
self._class_cache = [
{"type_id": "/videos", "type_name": "精选影片"},
{"type_id": "/videos/category/rhgv", "type_name": "日韩专区"},
{"type_id": "/videos/category/omjd", "type_name": "欧美专区"},
{"type_id": "/videos/all/xiaolan", "type_name": "原创专区"},
{"type_id": "/videos/all/20min", "type_name": "长片专区"},
{"type_id": "/videos/all/10min", "type_name": "短片速看"},
]
return self._class_cache
def _parse_list(self, html):
data, seen = [], set()
for block in re.findall(r'(\{[^{}]*"@type"\s*:\s*"VideoObject"[\s\S]*?\})', html or "", re.S | re.I):
name = self._clean(self._j(block, "name"))
href = self._j(block, "url") or self._j(block, "embedUrl")
pic = self._j(block, "thumbnailUrl")
if href and "/videos/" in href and href not in seen:
seen.add(href)
data.append({"vod_id": self._fix_url(href), "vod_name": name, "vod_pic": urllib.parse.urljoin(self.BASE_URL, pic), "vod_remarks": self._dur(self._j(block, "duration"))})
cards = re.findall(r'(<a[^>]+href=["\'][^"\']*/videos/vd-[^"\']+["\'][\s\S]{0,2500}?</a>)', html or "", re.S | re.I)
if not cards:
cards = re.findall(r'(<div[^>]+class=["\'][^"\']*(?:video|item|card|list)[^"\']*["\'][\s\S]{0,3500}?/videos/vd-[\s\S]{0,1200}?</div>)', html or "", re.S | re.I)
for item in cards:
href = self._match(item, r'href=["\']([^"\']*/videos/vd-[^"\']+)["\']')
name = self._clean(self._match(item, r'title=["\']([^"\']+)') or self._match(item, r'alt=["\']([^"\']+)') or self._match(item, r'<h[1-6][^>]*>(.*?)</h[1-6]>') or self._match(item, r'<a[^>]*>(.*?)</a>'))
pic = self._match(item, r'(?:data-original|data-src|data-lazyload|data-lazy-src)=["\']([^"\']+)') or self._match(item, r'<img[^>]+src=["\']([^"\']+)')
remarks = self._clean(self._match(item, r'(\d{1,2}:\d{2}(?::\d{2})?)') or self._match(item, r'<span[^>]*>(.*?)</span>'))
full = self._fix_url(urllib.parse.urljoin(self.BASE_URL, href))
if full not in seen and name and "/static/web/images/poster_loading" not in pic:
seen.add(full)
data.append({"vod_id": full, "vod_name": name, "vod_pic": urllib.parse.urljoin(self.BASE_URL, pic), "vod_remarks": remarks})
return data
def _play_url(self, page, html):
play = self._match(html, r'data-url=["\']([^"\']+\.m3u8[^"\']*)')
if play:
return play.replace("&amp;", "&")
js = self._unpack_first(html)
src = self._match(js, r'<script\s+src=\\?["\']([^"\']*detail_play\.js[^"\']+)') or self._match(js, r'src=\\?["\']([^"\']*detail_play\.js[^"\']+)')
if not src:
src = self._match(html, r'src=["\']([^"\']*detail_play\.js[^"\']+)')
if not src:
return ""
src = src.replace('\\/', '/').replace('\\"', '"').replace("\\'", "'")
src = re.sub(r'"\s*\+\s*encodeURIComponent\(["\']([^"\']+)["\']\)\s*\+\s*"', urllib.parse.quote(self._last_group(src)), src) if "encodeURIComponent" in src else src
t = str(int(__import__("time").time() / 1000 / 1800)) if False else ""
src = re.sub(r'"\s*\+\s*parseInt\(\(new Date\(\)\)\.getTime\(\)/1000/1800\)\s*\+\s*"', str(int(self._now() / 1800)), src)
src = urllib.parse.urljoin(page, src.replace("&amp;", "&"))
html2 = self._get(src, {"Referer": page})
js2 = self._unpack_first(html2)
return (self._match(js2, r'data-url=\\?["\']([^"\']+\.m3u8[^"\']*)') or self._match(html2, r'data-url=\\?["\']([^"\']+\.m3u8[^"\']*)') or self._match(js2, r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']')).replace("&amp;", "&").replace("\\/", "/")
def _unpack_first(self, text):
m = re.search(r'eval\(function\(p,a,c,k,e,d\)[\s\S]*?\)\)', text or "", re.I)
if not m:
return text or ""
code = m.group(0)
p = self._match(code, r"\}\('([\s\S]*)',\s*(\d+),\s*(\d+),\s*'([^']*)'\.split\('\|'\)")
m2 = re.search(r"\}\('([\s\S]*)',\s*(\d+),\s*(\d+),\s*'([^']*)'\.split\('\|'\)", code, re.S)
if not m2:
return text or ""
payload, base, count, words = m2.group(1), int(m2.group(2)), int(m2.group(3)), m2.group(4).split("|")
for i in range(count - 1, -1, -1):
if i < len(words) and words[i]:
payload = re.sub(r'\b' + self._base(i, base) + r'\b', words[i], payload)
return payload.replace("\\'", "'").replace('\\"', '"').replace("\\/", "/")
def _base(self, n, b):
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
return chars[n] if n < b and n < len(chars) else self._base(n // b, b) + chars[n % b]
def _get(self, url, headers=None):
for real in self._candidate_urls(self._fix_url(url)):
h = dict(self.HEADERS)
h["Referer"] = self.BASE_URL + "/"
if headers:
h.update(headers)
try:
r = self.session.get(real, headers=h, timeout=15, verify=False)
r.encoding = "utf-8"
if r.status_code < 400 and "Just a moment" not in r.text and "cf-browser-verification" not in r.text:
return r.text
except Exception:
continue
return ""
def _candidate_urls(self, url):
urls = [url]
for host in self.FALLBACK_URLS:
p = urllib.parse.urlparse(url)
if p.netloc and host not in url:
urls.append(host + p.path + ("?" + p.query if p.query else ""))
return list(dict.fromkeys(urls))
def _fix_url(self, url):
return str(url or "").replace("http://91nt.com", self.BASE_URL).replace("https://www.91nt.com", self.BASE_URL)
def _match(self, text, pattern):
m = re.search(pattern, text or "", re.S | re.I)
return m.group(1).strip() if m else ""
def _j(self, text, key):
return self._match(text, r'"' + re.escape(key) + r'"\s*:\s*"([^"]*)').replace("\\/", "/")
def _clean(self, text):
text = re.sub(r'<.*?>', '', text or '')
text = text.replace('&nbsp;', ' ').replace('&amp;', '&').replace('&#038;', '&').replace('&quot;', '"').replace('&#34;', '"')
return re.sub(r'\s+', ' ', text).strip()
def _dur(self, value):
value = str(value or "").strip()
if value.isdigit():
s = int(value)
return "%02d:%02d" % (s // 60, s % 60)
return value.replace("PT", "").replace("H", ":").replace("M", ":").replace("S", "")
def _to_int(self, value, default=0):
try:
return int(value)
except Exception:
return default
def _last_group(self, text):
m = re.findall(r'encodeURIComponent\(["\']([^"\']+)["\']\)', text or "", re.S | re.I)
return m[-1] if m else ""
def _now(self):
try:
import time
return int(time.time())
except Exception:
return 0
+376
View File
@@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
import base64
import threading
import requests
import urllib3
import time
import random
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import unquote, quote, urljoin, urlparse
urllib3.disable_warnings()
sys.path.append('..')
from base.spider import Spider as BaseSpider
# ===== 本地代理(用于图片防盗链)=====
_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',
'Referer': 'https://zsrqab03.zsrenqi.xyz/',
}
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
# ===== Spider =====
class Spider(BaseSpider):
session = requests.Session()
host = 'https://zsrqab03.zsrenqi.xyz'
def __init__(self):
super().__init__()
self._categories_cache = None
self._debug = True
def _log(self, msg):
if self._debug:
print(f'[zsrenqi] {msg}')
def getName(self):
return '真实人妻'
def isVideoFormat(self, url):
if not url or not isinstance(url, str):
return False
# 只认明确的视频扩展名,且必须是 http 开头
return url.startswith('http') and any(ext in url for ext in ['.m3u8', '.mp4', '.ts', '.flv', '.mkv'])
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
self.session.headers.update(self._get_headers())
_start_proxy()
text = self._fetch(self.host)
if text:
self._load_categories(text)
def _get_headers(self, referer=None):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Referer': referer or self.host + '/',
}
return headers
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, referer=None, retries=3):
for i in range(retries):
try:
if referer is None:
referer = self.host + '/'
headers = self._get_headers(referer)
if i > 0:
time.sleep(random.uniform(0.5, 1.5))
r = self.session.get(url, headers=headers, timeout=30, verify=False)
r.encoding = 'utf-8'
if r.status_code == 200:
return r.text
elif r.status_code in [403, 429, 503]:
self._log(f'请求被拦截 [{r.status_code}],重试 {i+1}/{retries}')
else:
return ''
except Exception as e:
self._log(f'请求异常 [{e}],重试 {i+1}/{retries}')
return ''
# ===== 分类加载(完整版)=====
def _load_categories(self, text):
if not text:
return
cats = []
seen = set()
cats.append({'type_id': 'home', 'type_name': '首页'})
seen.add('home')
pattern = r'<div class="row-item[^"]*">\s*<div class="row-item-title[^"]*">\s*<a href="(/index\.php/vod/type/id/(\d+)\.html)"[^>]*>([^<]+)</a>'
for href, tid, name in re.findall(pattern, text, re.S):
if tid in seen:
continue
seen.add(tid)
cats.append({'type_id': tid, 'type_name': name.strip()})
if len(cats) <= 2:
alt_pattern = r'<a[^>]+href="(/index\.php/vod/type/id/(\d+)\.html)"[^>]*>([^<]+)</a>'
for href, tid, name in re.findall(alt_pattern, text, re.S):
if tid in seen or 'https' in href or '外链' in name:
continue
seen.add(tid)
cats.append({'type_id': tid, 'type_name': name.strip()})
self._categories_cache = cats
self._log(f'加载分类: {len(cats)}')
# ===== 列表解析 =====
def _parse_items(self, html):
items = []
pattern = r'<li\s+class="content-item">\s*<a[^>]+href="(/index\.php/vod/detail/id/(\d+)\.html)"[^>]*>.*?<img[^>]+data-original="([^"]+)"[^>]*>.*?</a>\s*<div\s+class="title">\s*<h5[^>]*>\s*<a[^>]*>([^<]+)</a>'
for href, vid, pic, title in re.findall(pattern, html, re.S):
items.append({
'vod_id': vid,
'vod_name': title.strip(),
'vod_pic': self._proxy_url(pic) if pic.startswith('http') else pic,
'vod_remarks': '',
})
return items
def _get_list(self, tid, page):
if page == 1:
url = f'{self.host}/index.php/vod/type/id/{tid}.html'
else:
url = f'{self.host}/index.php/vod/type/id/{tid}.html?page={page}'
html = self._fetch(url, referer=f'{self.host}/index.php/vod/type/id/{tid}.html')
if not html and page > 1:
url = f'{self.host}/index.php/vod/type/id/{tid}/page/{page}.html'
html = self._fetch(url, referer=f'{self.host}/index.php/vod/type/id/{tid}.html')
if not html:
return []
return self._parse_items(html)
# ===== 详情解析(提取线路播放页)=====
def _fetch_detail(self, vid):
url = f'{self.host}/index.php/vod/detail/id/{vid}.html'
self._log(f'获取详情: {url}')
html = self._fetch(url, referer=self.host)
if not html:
return None
return self._parse_detail(html, vid, url)
def _parse_detail(self, html, vid, base_url):
# 标题
title = ''
m = re.search(r'<h1[^>]*>(.*?)</h1>', html, re.S)
if m:
title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
if not title:
m = re.search(r'<title>([^<]+)</title>', html)
if m:
raw_title = m.group(1).strip()
title = re.split(r'\s*[-|]\s*', raw_title)[0].strip()
if not title:
title = vid
# 封面
cover = ''
m = re.search(r'<meta[^>]*property="og:image"[^>]*content="([^"]+)"', html)
if m:
cover = m.group(1)
if not cover:
m = re.search(r'<img[^>]+class="[^"]*cover[^"]*"[^>]+src="([^"]+)"', html, re.S)
if m:
cover = m.group(1)
if not cover:
m = re.search(r'data-original="([^"]+)"', html)
if m:
cover = m.group(1)
# 提取所有线路播放页链接(如 /index.php/vod/play/id/xxx/sid/1/nid/1.html
play_links = []
pattern = r'href=["\'](/index\.php/vod/play/id/\d+/sid/\d+/nid/\d+\.html)["\']'
for link in set(re.findall(pattern, html)):
full = urljoin(base_url, link)
if full.startswith('http'):
play_links.append(full)
# 若没有,尝试构造默认线路
if not play_links:
play_links.append(f'{self.host}/index.php/vod/play/id/{vid}/sid/1/nid/1.html')
# 组装播放串(线路名以“线路一、线路二”命名)
sources = []
urls = []
for idx, play_url in enumerate(play_links, 1):
label = f'线路{idx}'
sources.append(label)
urls.append(f'{label}${play_url}')
play_from = '$$$'.join(sources)
play_url = '#'.join(urls)
return {
'vod_id': vid,
'vod_name': title,
'vod_pic': self._proxy_url(cover) if cover else '',
'vod_play_from': play_from,
'vod_play_url': play_url,
'vod_content': title,
}
# ===== 首页、分类、详情、搜索 =====
def homeContent(self, filter):
try:
text = self._fetch(self.host)
if text and self._categories_cache is None:
self._load_categories(text)
cats = self._categories_cache or []
home_list = self._parse_items(text) if text else []
return {
'class': cats,
'filters': {},
'type': '影视',
'list': home_list,
'page': 1,
'pagecount': 1,
'limit': len(home_list),
'total': len(home_list)
}
except Exception as e:
self._log(f'homeContent 异常: {e}')
return {'class': [], 'filters': {}, 'type': '影视', 'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}
def homeVideoContent(self):
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try:
if tid.startswith('search_'):
keyword = tid[7:]
return self.searchContent(keyword, False, pg)
if tid == 'home':
html = self._fetch(self.host)
items = self._parse_items(html) if html else []
return {'list': items, 'page': 1, 'pagecount': 1, 'limit': len(items), 'total': len(items)}
else:
page = int(pg) if pg else 1
items = self._get_list(tid, page)
total_page = page + 1
if page == 1:
first_html = self._fetch(f'{self.host}/index.php/vod/type/id/{tid}.html')
if first_html:
pages = re.findall(r'/page/(\d+)\.html', first_html)
if not pages:
pages = re.findall(r'[?&]page=(\d+)', first_html)
if pages:
total_page = max(int(p) for p in pages)
return {'list': items, 'page': page, 'pagecount': total_page, 'limit': len(items), 'total': total_page * len(items)}
except Exception as e:
self._log(f'categoryContent 异常: {e}')
return {'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}
def detailContent(self, ids):
try:
vid = str(ids[0] if isinstance(ids, list) else ids)
detail = self._fetch_detail(vid)
if not detail:
detail = {'vod_id': vid, 'vod_name': vid, 'vod_pic': '', 'vod_play_from': '', 'vod_play_url': '无$'}
return {'list': [detail]}
except Exception as e:
self._log(f'detailContent 异常: {e}')
return {'list': []}
# ===== 播放器(关键:返回 parse=1 让 TVBox 解析播放页)=====
def playerContent(self, flag, id, vipFlags=None):
try:
if not id:
return {'parse': 0, 'url': '', 'header': {}}
# 如果 id 是数字,则取第一个线路播放页
if id.isdigit():
detail = self._fetch_detail(id)
if detail and detail.get('vod_play_url'):
first_line = detail['vod_play_url'].split('#')[0]
if '$' in first_line:
play_url = first_line.split('$', 1)[1]
else:
play_url = first_line
id = play_url
# 如果 id 是播放页(以 .html 结尾),则让 TVBox 解析
if id.endswith('.html') and id.startswith('http'):
# 返回 parse=1,让 TVBox 自动解析该播放页
return {
'parse': 1,
'url': id,
'header': {
'Referer': self.host,
'User-Agent': 'Mozilla/5.0',
}
}
# 否则直接播放(可能是视频地址)
referer = self.host
if id.startswith('http'):
parsed = urlparse(id)
if parsed.netloc:
referer = f'{parsed.scheme}://{parsed.netloc}/'
return {
'parse': 0,
'url': id,
'header': {
'Referer': referer,
'User-Agent': 'Mozilla/5.0',
}
}
except Exception as e:
self._log(f'playerContent 异常: {e}')
return {'parse': 0, 'url': '', 'header': {}}
def searchContent(self, key, quick, pg='1'):
try:
page = int(pg) if pg else 1
url = f'{self.host}/index.php/vod/search.html?wd={quote(key)}&page={page}'
html = self._fetch(url, referer=self.host)
items = self._parse_items(html) if html else []
return {'list': items, 'page': page, 'pagecount': page + 1, 'limit': len(items), 'total': page * len(items)}
except Exception as e:
self._log(f'searchContent 异常: {e}')
return {'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}