Sync all projects
This commit is contained in:
@@ -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
@@ -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("&", "&")
|
||||
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("&", "&"))
|
||||
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("&", "&").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(' ', ' ').replace('&', '&').replace('&', '&').replace('"', '"').replace('"', '"')
|
||||
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
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user