xxxx
This commit is contained in:
265
PY1/103.py
Normal file
265
PY1/103.py
Normal file
@@ -0,0 +1,265 @@
|
||||
# coding=utf-8
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import json
|
||||
from pyquery import PyQuery as pq
|
||||
import requests
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base_url = "http://oxax.tv"
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'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': self.base_url,
|
||||
'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',
|
||||
})
|
||||
|
||||
# 完整的频道列表(从实际页面提取的40个频道)
|
||||
self.all_channels = [
|
||||
{"title": "ОХ-АХ HD", "href": "/oh-ah.html"},
|
||||
{"title": "CineMan XXX HD", "href": "/sl-hot1.html"},
|
||||
{"title": "CineMan XXX2 HD", "href": "/sl-hot2.html"},
|
||||
{"title": "Brazzers TV Europe", "href": "/brazzers-tv-europe.html"},
|
||||
{"title": "Brazzers TV", "href": "/brazzers-tv.html"},
|
||||
{"title": "Red Lips", "href": "/red-lips.html"},
|
||||
{"title": "KinoXXX", "href": "/kino-xxx.html"},
|
||||
{"title": "XY Max HD", "href": "/xy-max-hd.html"},
|
||||
{"title": "XY Plus HD", "href": "/xy-plus-hd.html"},
|
||||
{"title": "XY Mix HD", "href": "/xy-mix-hd.html"},
|
||||
{"title": "Barely legal", "href": "/barely-legal.html"},
|
||||
{"title": "Playboy TV", "href": "/playboy-tv.html"},
|
||||
{"title": "Vivid Red HD", "href": "/vivid-red.html"},
|
||||
{"title": "Exxxotica HD", "href": "/hot-pleasure.html"},
|
||||
{"title": "Babes TV", "href": "/babes-tv.html"},
|
||||
{"title": "Русская ночь", "href": "/russkaya-noch.html"},
|
||||
{"title": "Pink O TV", "href": "/pink-o.html"},
|
||||
{"title": "Erox HD", "href": "/erox-hd.html"},
|
||||
{"title": "Eroxxx HD", "href": "/eroxxx-hd.html"},
|
||||
{"title": "Hustler HD", "href": "/hustler-hd.html"},
|
||||
{"title": "Private TV", "href": "/private-tv.html"},
|
||||
{"title": "Redlight HD", "href": "/redlight-hd.html"},
|
||||
{"title": "Penthouse Gold HD", "href": "/penthouse-gold.html"},
|
||||
{"title": "Penthouse Quickies", "href": "/penthouse-2.html"},
|
||||
{"title": "O-la-la", "href": "/o-la-la.html"},
|
||||
{"title": "Blue Hustler", "href": "/blue-hustler.html"},
|
||||
{"title": "Шалун", "href": "/shalun.html"},
|
||||
{"title": "Dorcel TV", "href": "/dorcel-tv.html"},
|
||||
{"title": "Extasy HD", "href": "/extasyhd.html"},
|
||||
{"title": "XXL", "href": "/xxl.html"},
|
||||
{"title": "FAP TV 2", "href": "/fap-tv-2.html"},
|
||||
{"title": "FAP TV 3", "href": "/fap-tv-3.html"},
|
||||
{"title": "FAP TV 4", "href": "/fap-tv-4.html"},
|
||||
{"title": "FAP TV Parody", "href": "/fap-tv-parody.html"},
|
||||
{"title": "FAP TV Compilation", "href": "/fap-tv-compilation.html"},
|
||||
{"title": "FAP TV Anal", "href": "/fap-tv-anal.html"},
|
||||
{"title": "FAP TV Teens", "href": "/fap-tv-teens.html"},
|
||||
{"title": "FAP TV Lesbian", "href": "/fap-tv-lesbian.html"},
|
||||
{"title": "FAP TV BBW", "href": "/fap-tv-bbw.html"},
|
||||
{"title": "FAP TV Trans", "href": "/fap-tv-trans.html"},
|
||||
]
|
||||
|
||||
# ========= 工具方法 =========
|
||||
|
||||
def _abs_url(self, base, url):
|
||||
"""转换为绝对URL"""
|
||||
if not url:
|
||||
return ''
|
||||
if url.startswith('http'):
|
||||
return url
|
||||
if url.startswith('//'):
|
||||
return 'http:' + url
|
||||
if url.startswith('/'):
|
||||
return self.base_url + url
|
||||
return base.rsplit('/', 1)[0] + '/' + url
|
||||
|
||||
def _get_channel_image(self, channel_name):
|
||||
"""根据频道名称生成图片URL(使用占位图)"""
|
||||
# 为每个频道生成唯一的颜色
|
||||
color_map = {
|
||||
'brazzers': 'FFD700', 'playboy': 'FF69B4', 'hustler': 'DC143C',
|
||||
'penthouse': '9370DB', 'vivid': 'FF1493', 'private': '8B008B',
|
||||
'dorcel': 'FF6347', 'cineman': '4169E1', 'fap': 'FF4500',
|
||||
'xy': 'DA70D6', 'erox': 'FF00FF', 'kino': '8A2BE2',
|
||||
}
|
||||
|
||||
color = '1E90FF' # 默认蓝色
|
||||
name_lower = channel_name.lower()
|
||||
for key, col in color_map.items():
|
||||
if key in name_lower:
|
||||
color = col
|
||||
break
|
||||
|
||||
text = urllib.parse.quote(channel_name[:20])
|
||||
return f"https://via.placeholder.com/400x225/{color}/FFFFFF?text={text}"
|
||||
|
||||
# ========= Spider接口实现 =========
|
||||
|
||||
def getName(self):
|
||||
return "OXAX直播"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""返回分类列表"""
|
||||
return {
|
||||
'class': [
|
||||
{'type_name': '全部频道', 'type_id': 'all'},
|
||||
{'type_name': 'HD频道', 'type_id': 'hd'},
|
||||
{'type_name': 'FAP系列', 'type_id': 'fap'},
|
||||
]
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""首页推荐 - 显示所有频道"""
|
||||
videos = []
|
||||
for ch in self.all_channels:
|
||||
videos.append({
|
||||
'vod_id': ch['href'],
|
||||
'vod_name': ch['title'],
|
||||
'vod_pic': self._get_channel_image(ch['title']),
|
||||
'vod_remarks': '直播',
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容 - 支持分页和过滤"""
|
||||
pg = int(pg)
|
||||
items_per_page = 30
|
||||
|
||||
# 根据分类ID过滤频道
|
||||
if tid == 'hd':
|
||||
channels = [ch for ch in self.all_channels if 'HD' in ch['title'].upper()]
|
||||
elif tid == 'fap':
|
||||
channels = [ch for ch in self.all_channels if 'FAP' in ch['title'].upper()]
|
||||
else: # all
|
||||
channels = self.all_channels
|
||||
|
||||
# 分页
|
||||
start = (pg - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
page_channels = channels[start:end]
|
||||
|
||||
# 构建视频列表
|
||||
videos = []
|
||||
for ch in page_channels:
|
||||
videos.append({
|
||||
'vod_id': ch['href'],
|
||||
'vod_name': ch['title'],
|
||||
'vod_pic': self._get_channel_image(ch['title']),
|
||||
'vod_remarks': '直播',
|
||||
})
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': max(1, (len(channels) + items_per_page - 1) // items_per_page),
|
||||
'limit': items_per_page,
|
||||
'total': len(channels),
|
||||
}
|
||||
|
||||
def detailContent(self, array):
|
||||
"""详情页 - 直接返回页面URL,不做m3u8提取"""
|
||||
if not array or not array[0]:
|
||||
return {'list': []}
|
||||
|
||||
relative_path = array[0]
|
||||
detail_url = self._abs_url(self.base_url, relative_path)
|
||||
|
||||
# 提取标题(从相对路径推断)
|
||||
title = relative_path.replace('.html', '').replace('/', '').replace('-', ' ').title()
|
||||
|
||||
# 从 all_channels 查找真实标题
|
||||
for ch in self.all_channels:
|
||||
if ch['href'] == relative_path:
|
||||
title = ch['title']
|
||||
break
|
||||
|
||||
vod = {
|
||||
'vod_id': relative_path,
|
||||
'vod_name': title,
|
||||
'vod_pic': self._get_channel_image(title),
|
||||
'vod_remarks': '直播',
|
||||
'vod_content': '成人电视直播频道',
|
||||
'vod_play_from': 'OXAX',
|
||||
|
||||
'vod_play_url': f'{title}${detail_url}',
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
"""搜索功能"""
|
||||
if not key:
|
||||
return {'list': []}
|
||||
|
||||
key_lower = key.lower()
|
||||
results = []
|
||||
|
||||
# 在本地频道列表中搜索
|
||||
for ch in self.all_channels:
|
||||
if key_lower in ch['title'].lower():
|
||||
results.append({
|
||||
'vod_id': ch['href'],
|
||||
'vod_name': ch['title'],
|
||||
'vod_pic': self._get_channel_image(ch['title']),
|
||||
'vod_remarks': '直播',
|
||||
})
|
||||
|
||||
return {'list': results}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""
|
||||
播放器内容解析 - 关键修改点
|
||||
直接返回 video:// 协议的URL,让播放器自行解析页面
|
||||
"""
|
||||
result = {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": "",
|
||||
"header": {
|
||||
"User-Agent": self.session.headers.get('User-Agent'),
|
||||
"Referer": self.base_url
|
||||
}
|
||||
}
|
||||
|
||||
if not id:
|
||||
return result
|
||||
|
||||
try:
|
||||
# id格式: "标题$URL"
|
||||
url = id
|
||||
if '$' in url:
|
||||
url = url.split('$')[1]
|
||||
|
||||
|
||||
result["url"] = f"video://{url}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 播放器解析失败: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
"""判断是否为视频格式 - video:// 协议不是直接的视频格式"""
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
"""不需要手动视频检查"""
|
||||
return False
|
||||
|
||||
def localProxy(self, param):
|
||||
"""不使用本地代理"""
|
||||
return {}
|
||||
319
PY1/18.py
Normal file
319
PY1/18.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import urllib.parse
|
||||
import re
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "快递🔞"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.xjjkdfw.sbs"
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; M2007J3SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 MQQBrowser/6.2 TBS/045713 Mobile Safari/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,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': self.host
|
||||
}
|
||||
self.log(f"快递🔞爬虫初始化完成,主站: {self.host}")
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类"""
|
||||
result = {}
|
||||
classes = self._getCategories()
|
||||
result['class'] = classes
|
||||
try:
|
||||
rsp = self.fetch(self.host, headers=self.headers)
|
||||
html = rsp.text
|
||||
videos = self._getVideos(html)
|
||||
result['list'] = videos
|
||||
except Exception as e:
|
||||
self.log(f"首页获取出错: {str(e)}")
|
||||
result['list'] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""首页视频内容(可留空)"""
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容"""
|
||||
try:
|
||||
pg_int = int(pg)
|
||||
if pg_int == 1:
|
||||
url = f"{self.host}/vodtype/{tid}.html"
|
||||
else:
|
||||
url = f"{self.host}/vodtype/{tid}/page/{pg_int}.html"
|
||||
|
||||
self.log(f"访问分类URL: {url}")
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
html = rsp.text
|
||||
|
||||
videos = self._getVideos(html)
|
||||
|
||||
pagecount = 999
|
||||
page_links = re.findall(r'<a href="/vodtype/{}/page/(\d+)\.html"'.format(tid), html)
|
||||
if page_links:
|
||||
pagecount = max([int(p) for p in page_links if p.isdigit()])
|
||||
|
||||
if not videos:
|
||||
self.log(f"警告: 分类ID {tid}, 页码 {pg} 未找到任何视频。URL: {url}")
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': pg_int,
|
||||
'pagecount': pagecount,
|
||||
'limit': 20,
|
||||
'total': 999999
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"分类内容获取出错 (tid={tid}, pg={pg}): {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索功能(使用官方 AJAX 接口)"""
|
||||
try:
|
||||
search_url = f"{self.host}/index.php/ajax/suggest?mid=1&wd={urllib.parse.quote(key)}"
|
||||
self.log(f"搜索URL: {search_url}")
|
||||
|
||||
rsp = self.fetch(search_url, headers=self.headers)
|
||||
data = json.loads(rsp.text)
|
||||
|
||||
videos = []
|
||||
for item in data:
|
||||
video = {
|
||||
'vod_id': item.get('id', ''),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('pic', ''),
|
||||
'vod_remarks': item.get('actor', '')
|
||||
}
|
||||
videos.append(video)
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
self.log(f"搜索出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页面"""
|
||||
try:
|
||||
vid = ids[0]
|
||||
detail_url = f"{self.host}/voddetail/{vid}.html"
|
||||
self.log(f"详情URL: {detail_url}")
|
||||
rsp = self.fetch(detail_url, headers=self.headers)
|
||||
html = rsp.text
|
||||
video_info = self._getDetail(html, vid)
|
||||
return {'list': [video_info]} if video_info else {'list': []}
|
||||
except Exception as e:
|
||||
self.log(f"详情获取出错 (vid: {ids[0]}): {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放链接解析"""
|
||||
try:
|
||||
play_page_url = f"{self.host}/vodplay/{id}.html"
|
||||
self.log(f"播放页面URL: {play_page_url}")
|
||||
|
||||
rsp = self.fetch(play_page_url, headers=self.headers)
|
||||
if rsp.status_code != 200:
|
||||
self.log(f"播放页请求失败,状态码: {rsp.status_code}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_page_url}
|
||||
|
||||
html = rsp.text
|
||||
|
||||
# 1. 优先解析 JS 中的 player_aaaa 变量
|
||||
player_pattern = r'var player_aaaa=({.*?});'
|
||||
player_match = re.search(player_pattern, html, re.DOTALL)
|
||||
|
||||
if player_match:
|
||||
try:
|
||||
player_data = json.loads(player_match.group(1).replace("'", '"'))
|
||||
video_url = player_data.get('url', '').strip()
|
||||
|
||||
if video_url:
|
||||
if video_url.startswith('//'):
|
||||
video_url = 'https:' + video_url
|
||||
elif video_url.startswith('/') and not video_url.startswith('http'):
|
||||
video_url = self.host.rstrip('/') + video_url
|
||||
|
||||
self.log(f"✅ 找到视频直链: {video_url}")
|
||||
return {
|
||||
'parse': 0,
|
||||
'playUrl': '',
|
||||
'url': video_url,
|
||||
'header': json.dumps(self.headers)
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"解析player_aaaa失败: {str(e)}")
|
||||
|
||||
# 2. 解析 iframe 播放器
|
||||
iframe_match = re.search(r'<iframe[^>]*src=["\']([^"\']+)["\']', html)
|
||||
if iframe_match:
|
||||
iframe_url = iframe_match.group(1).strip()
|
||||
if iframe_url.startswith('//'):
|
||||
iframe_url = 'https:' + iframe_url
|
||||
elif iframe_url.startswith('/') and not iframe_url.startswith('http'):
|
||||
iframe_url = self.host.rstrip('/') + iframe_url
|
||||
|
||||
self.log(f"📹 找到iframe播放源: {iframe_url}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': iframe_url}
|
||||
|
||||
# 3. 最后手段:返回播放页本身,让播放器自己嗅探
|
||||
self.log(f"⚠️ 未找到播放源,返回原始播放页")
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_page_url}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"播放链接获取出错 (id: {id}): {str(e)}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': f"{self.host}/vodplay/{id}.html"}
|
||||
|
||||
# ========== 辅助方法 ==========
|
||||
|
||||
def _getCategories(self):
|
||||
"""从首页提取分类"""
|
||||
try:
|
||||
rsp = self.fetch(self.host, headers=self.headers)
|
||||
html = rsp.text
|
||||
categories = []
|
||||
pattern = r'<a href="/vodtype/(\d+)\.html"[^>]*>([^<]+)</a>'
|
||||
matches = re.findall(pattern, html)
|
||||
|
||||
seen = set()
|
||||
for tid, name in matches:
|
||||
if name.strip() and tid not in seen:
|
||||
seen.add(tid)
|
||||
categories.append({'type_id': tid, 'type_name': name.strip()})
|
||||
return categories
|
||||
except Exception as e:
|
||||
self.log(f"获取分类出错: {str(e)}")
|
||||
return []
|
||||
|
||||
def _getVideos(self, html):
|
||||
"""从HTML中提取视频列表"""
|
||||
videos = []
|
||||
|
||||
# 匹配结构:
|
||||
# <a class="thumbnail" href="/vodplay/123-1-1.html">
|
||||
# <img data-original="https://xxx.jpg" ...>
|
||||
# </a>
|
||||
# <a href="/voddetail/123.html">标题</a>
|
||||
# <p class="vodtitle">分类 - <span class="title">日期</span></p>
|
||||
|
||||
pattern = r'<a\s+class="thumbnail"[^>]*href="(/vodplay/(\d+)-\d+-\d+\.html)"[^>]*>.*?data-original="([^"]+)".*?</a>.*?<a\s+href="/voddetail/\d+\.html"[^>]*>([^<]+)</a>.*?<p\s+class="vodtitle">([^<]+?)\s*-\s*<span\s+class="title">([^<]+)</span>'
|
||||
|
||||
matches = re.findall(pattern, html, re.DOTALL | re.IGNORECASE)
|
||||
|
||||
for full_play_link, vid, pic, title, category, date in matches:
|
||||
if not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else 'https:' + pic if pic.startswith('//') else pic
|
||||
|
||||
video = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': f"{category.strip()} | {date.strip()}"
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
return videos
|
||||
|
||||
def _getDetail(self, html, vid):
|
||||
"""获取详情信息"""
|
||||
try:
|
||||
# 标题
|
||||
title = self.regStr(r'<h2\s+class="title">([^<]+)</h2>', html)
|
||||
|
||||
# 封面
|
||||
pic = self.regStr(r'data-original="([^"]+)"', html)
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else 'https:' + pic if pic.startswith('//') else pic
|
||||
|
||||
# 简介
|
||||
desc = self.regStr(r'<div\s+class="content">([\s\S]*?)</div>', html)
|
||||
if desc:
|
||||
desc = desc.strip().replace('<br>', '\n').replace('</br>', '')
|
||||
else:
|
||||
desc = title
|
||||
|
||||
# 演员 (从标题中提取)
|
||||
actor = ""
|
||||
actor_match = re.search(r'([\u4e00-\u9fa5]{2,4})[-\s]+[A-Z0-9-]+', title)
|
||||
if actor_match:
|
||||
actor = actor_match.group(1).strip()
|
||||
|
||||
# 导演信息,网站未提供,留空
|
||||
director = ""
|
||||
|
||||
# 播放源
|
||||
play_from = []
|
||||
play_url_list = []
|
||||
|
||||
playlist_matches = re.findall(r'<ul\s+class="playlist">([\s\S]*?)</ul>', html)
|
||||
if playlist_matches:
|
||||
for i, pl_html in enumerate(playlist_matches):
|
||||
source_name = f"线路{i+1}"
|
||||
episodes = []
|
||||
ep_matches = re.findall(r'<a\s+href="(/vodplay/(\d+-\d+-\d+)\.html)"[^>]*>([^<]+)</a>', pl_html)
|
||||
for full_url, ep_id, ep_name in ep_matches:
|
||||
episodes.append(f"{ep_name.strip()}${ep_id}")
|
||||
if episodes:
|
||||
play_from.append(source_name)
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
# 如果没有播放列表,则创建一个默认的
|
||||
if not play_url_list:
|
||||
play_from = ["默认源"]
|
||||
play_url_list = [f"第1集${vid}-1-1"]
|
||||
|
||||
# 其他字段
|
||||
type_name = self.regStr(r'<a\s+href="/vodtype/\d+\.html"[^>]*>([^<]+)</a>', html)
|
||||
|
||||
return {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': type_name.strip() if type_name else "未知",
|
||||
'vod_year': "2025",
|
||||
'vod_area': "网络",
|
||||
'vod_remarks': "高清",
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url_list)
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"获取详情失败 (vid={vid}): {str(e)}")
|
||||
return {
|
||||
'vod_id': vid,
|
||||
'vod_name': "加载失败",
|
||||
'vod_pic': "",
|
||||
'type_name': "",
|
||||
'vod_year': "",
|
||||
'vod_area': "",
|
||||
'vod_remarks': "",
|
||||
'vod_actor': "",
|
||||
'vod_director': "",
|
||||
'vod_content': "详情加载失败",
|
||||
'vod_play_from': "默认源",
|
||||
'vod_play_url': f"第1集${vid}-1-1"
|
||||
}
|
||||
|
||||
def regStr(self, pattern, string):
|
||||
"""正则提取第一个匹配组"""
|
||||
try:
|
||||
match = re.search(pattern, string)
|
||||
return match.group(1) if match else ""
|
||||
except:
|
||||
return ""
|
||||
290
PY1/1AV研究所.py
Normal file
290
PY1/1AV研究所.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
import base64
|
||||
from urllib.parse import urljoin, unquote
|
||||
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "AV研究所"
|
||||
|
||||
def init(self, extend=""):
|
||||
super().init(extend)
|
||||
self.site_url = "https://xn--cdn0308-1yjs01cc-rf0zn60cta5031amw9i.yanjiusuo0038.top"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Mobile Safari/537.36",
|
||||
"Referer": self.site_url,
|
||||
"Accept-Language": "zh-CN,zh;q=0.9"
|
||||
}
|
||||
self.sess = requests.Session()
|
||||
retry = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
|
||||
self.sess.mount("https://", HTTPAdapter(max_retries=retry))
|
||||
self.sess.mount("http://", HTTPAdapter(max_retries=retry))
|
||||
self.page_size = 20
|
||||
self.total = 9999
|
||||
|
||||
def fetch(self, url, timeout=10):
|
||||
try:
|
||||
return self.sess.get(url, headers=self.headers, timeout=timeout, verify=False)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _abs(self, u):
|
||||
if not u:
|
||||
return ""
|
||||
if u.startswith("//"):
|
||||
return "https:" + u
|
||||
if u.startswith(("http://", "https://")):
|
||||
return u
|
||||
return self.site_url + (u if u.startswith("/") else "/" + u)
|
||||
|
||||
def _clean(self, s):
|
||||
if not s:
|
||||
return ""
|
||||
s = re.sub(r"<[^>]+>", "", s, flags=re.S)
|
||||
return re.sub(r"\s+", " ", s).strip()
|
||||
|
||||
def homeContent(self, filter):
|
||||
# 可按你的站点改成动态抓取;这里先给固定分类
|
||||
cate_list = [
|
||||
{"type_name": "最新", "type_id": "latest-insert"},
|
||||
{"type_name": "最近发布", "type_id": "recent-release"},
|
||||
{"type_name": "评分榜", "type_id": "top-rating"},
|
||||
{"type_name": "收藏榜", "type_id": "top-favorites"},
|
||||
]
|
||||
return {"class": cate_list}
|
||||
|
||||
def _parse_video_list(self, html):
|
||||
video_list = []
|
||||
# 匹配 <dl> ... <dt><a href=...><img data-src=...><i>日期</i> ... <h3>标题</h3>
|
||||
dls = re.findall(r"<dl>([\s\S]*?)</dl>", html, flags=re.S)
|
||||
for item in dls:
|
||||
m_href = re.search(r'<dt>\s*<a[^>]+href=["\']([^"\']+)["\']', item, flags=re.S)
|
||||
m_pic = re.search(r'<img[^>]+(?:data-src|src)=["\']([^"\']+)["\']', item, flags=re.S)
|
||||
m_date = re.search(r"<i>([^<]+)</i>", item, flags=re.S)
|
||||
m_name = re.search(r"<h3>([\s\S]*?)</h3>", item, flags=re.S)
|
||||
|
||||
if not (m_href and m_pic and m_name):
|
||||
continue
|
||||
|
||||
video_list.append({
|
||||
"vod_id": self._abs(m_href.group(1).strip()),
|
||||
"vod_name": self._clean(m_name.group(1)),
|
||||
"vod_pic": self._abs(m_pic.group(1).strip()),
|
||||
"vod_remarks": self._clean(m_date.group(1)) if m_date else "",
|
||||
"style": {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return video_list
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
|
||||
# 兼容两种分页:
|
||||
# /list/AvDB/latest-insert.html
|
||||
# /list/AvDB/latest-insert/2.html
|
||||
if pg == 1:
|
||||
list_url = f"{self.site_url}/list/AvDB/{tid}.html"
|
||||
else:
|
||||
list_url = f"{self.site_url}/list/AvDB/{tid}/{pg}.html"
|
||||
|
||||
res = self.fetch(list_url)
|
||||
video_list = self._parse_video_list(res.text) if (res and res.ok) else []
|
||||
|
||||
pagecount = pg + 1 if len(video_list) else pg
|
||||
return {
|
||||
"list": video_list,
|
||||
"page": pg,
|
||||
"pagecount": pagecount,
|
||||
"limit": self.page_size,
|
||||
"total": self.total
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
# 页面结构里的搜索参数是 GET: /?m=search&u=AvDB&p=1&k=关键词
|
||||
search_url = f"{self.site_url}/?m=search&u=AvDB&p={pg}&k={requests.utils.quote(key)}"
|
||||
res = self.fetch(search_url)
|
||||
video_list = self._parse_video_list(res.text) if (res and res.ok) else []
|
||||
|
||||
pagecount = pg + 1 if len(video_list) else pg
|
||||
return {
|
||||
"list": video_list,
|
||||
"page": pg,
|
||||
"pagecount": pagecount,
|
||||
"limit": self.page_size,
|
||||
"total": len(video_list) if len(video_list) < self.total else self.total
|
||||
}
|
||||
|
||||
|
||||
def detailContent(self, ids):
|
||||
vod_id = ids[0] if ids else ""
|
||||
if not vod_id:
|
||||
return {"list": [{"vod_name": "视频ID为空"}]}
|
||||
|
||||
res = self.fetch(vod_id)
|
||||
if not (res and res.ok):
|
||||
return {"list": [{"vod_id": vod_id, "vod_name": "视频详情解析失败"}]}
|
||||
|
||||
html = res.text
|
||||
|
||||
# 标题
|
||||
m_name = re.search(r"<h1>([\s\S]*?)</h1>", html, re.S)
|
||||
vod_name = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", "", m_name.group(1))).strip() if m_name else "未知名称"
|
||||
|
||||
# 封面(详情页通常没有主封面,兜底从猜你喜欢首图拿)
|
||||
m_pic = re.search(r'<img[^>]+(?:data-src|src)=["\']([^"\']+)["\']', html, re.I)
|
||||
vod_pic = ""
|
||||
if m_pic:
|
||||
p = m_pic.group(1).strip()
|
||||
if p.startswith("//"):
|
||||
vod_pic = "https:" + p
|
||||
elif p.startswith(("http://", "https://")):
|
||||
vod_pic = p
|
||||
else:
|
||||
vod_pic = self.site_url + (p if p.startswith("/") else "/" + p)
|
||||
|
||||
# 关键:提取 iframe 播放页地址
|
||||
m_iframe = re.search(
|
||||
r'<iframe[^>]*class=["\']player-iframe["\'][^>]*src=["\']([^"\']+)["\']',
|
||||
html, re.I | re.S
|
||||
)
|
||||
play_entry = ""
|
||||
if m_iframe:
|
||||
iframe_src = m_iframe.group(1).strip()
|
||||
if iframe_src.startswith("//"):
|
||||
iframe_src = "https:" + iframe_src
|
||||
elif not iframe_src.startswith(("http://", "https://")):
|
||||
iframe_src = self.site_url + (iframe_src if iframe_src.startswith("/") else "/" + iframe_src)
|
||||
|
||||
# !!! 必须是 名称$地址
|
||||
play_entry = f"在线播放${iframe_src}"
|
||||
else:
|
||||
# 兜底:给详情页,让 playerContent 再二次提 iframe
|
||||
play_entry = f"在线播放${vod_id}"
|
||||
|
||||
detail_info = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": vod_name,
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": "",
|
||||
"type_name": "",
|
||||
"vod_play_from": "主线路",
|
||||
"vod_play_url": play_entry
|
||||
}
|
||||
return {"list": [detail_info]}
|
||||
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
import re
|
||||
import json
|
||||
from urllib.parse import urljoin, urlparse, parse_qsl, urlencode, urlunparse, unquote
|
||||
|
||||
def force_m_stream(u):
|
||||
"""强制线路为 M原版:src=missav,移除 variant"""
|
||||
pr = urlparse(u)
|
||||
q = dict(parse_qsl(pr.query, keep_blank_values=True))
|
||||
q["res"] = "720P"
|
||||
q["src"] = "missav"
|
||||
q.pop("variant", None)
|
||||
new_query = urlencode(q, doseq=True)
|
||||
return urlunparse((pr.scheme, pr.netloc, pr.path, pr.params, new_query, pr.fragment))
|
||||
|
||||
def abs_url(u, base):
|
||||
if u.startswith("//"):
|
||||
return "https:" + u
|
||||
if u.startswith(("http://", "https://")):
|
||||
return u
|
||||
return urljoin(base, u)
|
||||
|
||||
play_url = id.split("$", 1)[1] if "$" in id else id
|
||||
if not play_url:
|
||||
return {"parse": 0, "url": "", "header": self.headers}
|
||||
|
||||
play_url = abs_url(play_url, self.site_url)
|
||||
|
||||
# 直链直接返回
|
||||
if re.search(r"\.(m3u8|mp4|flv)(\?|$)", play_url, re.I):
|
||||
return {"parse": 0, "url": play_url, "header": self.headers}
|
||||
|
||||
# 先请求当前地址(可能是详情页,也可能是 iframe 页)
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.site_url
|
||||
r = self.sess.get(play_url, headers=h, timeout=10, verify=False)
|
||||
if not (r and r.ok):
|
||||
return {"parse": 1, "url": play_url, "header": self.headers}
|
||||
html = r.text
|
||||
except Exception:
|
||||
return {"parse": 1, "url": play_url, "header": self.headers}
|
||||
|
||||
# 若是详情页,先提取 iframe
|
||||
m_iframe = re.search(
|
||||
r'<iframe[^>]*class=["\']player-iframe["\'][^>]*src=["\']([^"\']+)["\']',
|
||||
html, re.I | re.S
|
||||
)
|
||||
if m_iframe:
|
||||
iframe_url = abs_url(m_iframe.group(1).strip(), play_url)
|
||||
else:
|
||||
iframe_url = play_url
|
||||
|
||||
# 强制 M原版
|
||||
iframe_url = force_m_stream(iframe_url)
|
||||
|
||||
# 请求 iframe 页面
|
||||
try:
|
||||
h2 = dict(self.headers)
|
||||
h2["Referer"] = play_url
|
||||
r2 = self.sess.get(iframe_url, headers=h2, timeout=10, verify=False)
|
||||
if not (r2 and r2.ok):
|
||||
return {"parse": 1, "url": iframe_url, "header": self.headers}
|
||||
ihtml = r2.text
|
||||
except Exception:
|
||||
return {"parse": 1, "url": iframe_url, "header": self.headers}
|
||||
|
||||
final_url = ""
|
||||
|
||||
# 主解析:var playurls = [...]
|
||||
m = re.search(r"var\s+playurls\s*=\s*(\[[\s\S]*?\])\s*;", ihtml, re.I)
|
||||
if m:
|
||||
try:
|
||||
arr = json.loads(m.group(1))
|
||||
if isinstance(arr, list) and arr:
|
||||
# playurls 通常已是当前线路的结果,取 auto 或第一个
|
||||
target = None
|
||||
for it in arr:
|
||||
if isinstance(it, dict) and str(it.get("name", "")).lower() == "auto" and it.get("url"):
|
||||
target = it
|
||||
break
|
||||
if target is None:
|
||||
target = arr[0]
|
||||
|
||||
if isinstance(target, dict):
|
||||
u = str(target.get("url", "")).strip().replace("\\/", "/")
|
||||
u = unquote(u)
|
||||
final_url = abs_url(u, iframe_url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 兜底正则
|
||||
if not final_url:
|
||||
mm = re.search(r'https?://[^\s"\'<>]+?\.(?:m3u8|mp4|flv)(?:\?[^\s"\'<>]*)?', ihtml, re.I)
|
||||
if mm:
|
||||
final_url = mm.group(0).strip()
|
||||
|
||||
if final_url:
|
||||
return {
|
||||
"parse": 0,
|
||||
"url": final_url,
|
||||
"header": {
|
||||
"User-Agent": self.headers.get("User-Agent", ""),
|
||||
"Referer": iframe_url
|
||||
}
|
||||
}
|
||||
|
||||
return {"parse": 1, "url": iframe_url, "header": self.headers}
|
||||
335
PY1/4K.py
Normal file
335
PY1/4K.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from base.spider import Spider
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import urllib.parse
|
||||
from Crypto.Cipher import ARC4
|
||||
from Crypto.Util.Padding import unpad
|
||||
import binascii
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://www.fullhd.xxx/zh/"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
pm = ''
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{'📽️' + match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{'📽️' + match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'✨{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result = {"class": [{"type_id": "latest-updates", "type_name": "最新视频🌠"},
|
||||
{"type_id": "top-rated", "type_name": "最佳视频🌠"},
|
||||
{"type_id": "most-popular", "type_name": "热门影片🌠"}],
|
||||
}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
# Get videos from different sections
|
||||
sections = {
|
||||
"latest-updates": "最新视频",
|
||||
"top-rated": "最佳视频",
|
||||
"most-popular": "热门影片"
|
||||
}
|
||||
|
||||
for section_id, section_name in sections.items():
|
||||
section = doc.find('div', id=f"list_videos_videos_watched_right_now_items")
|
||||
if not section:
|
||||
continue
|
||||
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else section_name
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
pic = pics['data-src'] if pics and 'data-src' in pics.attrs else ""
|
||||
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Error in homeVideoContent: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
try:
|
||||
if pg and int(pg) > 1:
|
||||
url = f'{xurl}/{cid}/{pg}/'
|
||||
else:
|
||||
url = f'{xurl}/{cid}/'
|
||||
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
section = doc.find('div', class_="list-videos")
|
||||
if section:
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else ""
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
pic = pics['data-src'] if pics and 'data-src' in pics.attrs else ""
|
||||
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in categoryContent: {str(e)}")
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
global pm
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
playurl = ''
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
res1 = requests.get(url=did, headers=headerx)
|
||||
res1.encoding = "utf-8"
|
||||
res = res1.text
|
||||
|
||||
content = '👉' + self.extract_middle_text(res,'<h1>','</h1>', 0)
|
||||
|
||||
yanuan = self.extract_middle_text(res, '<span>Pornstars:</span>','</div>',1, 'href=".*?">(.*?)</a>')
|
||||
|
||||
bofang = did
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_actor": yanuan,
|
||||
"vod_director": '',
|
||||
"vod_content": content,
|
||||
"vod_play_from": '💗4K💗',
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parts = id.split("http")
|
||||
xiutan = 0
|
||||
if xiutan == 0:
|
||||
if len(parts) > 1:
|
||||
before_https, after_https = parts[0], 'http' + parts[1]
|
||||
res = requests.get(url=after_https, headers=headerx)
|
||||
res = res.text
|
||||
|
||||
url2 = self.extract_middle_text(res, '<video', '</video>', 0).replace('\\', '')
|
||||
soup = BeautifulSoup(url2, 'html.parser')
|
||||
first_source = soup.find('source')
|
||||
src_value = first_source.get('src')
|
||||
|
||||
response = requests.head(src_value, allow_redirects=False)
|
||||
if response.status_code == 302:
|
||||
redirect_url = response.headers['Location']
|
||||
|
||||
response = requests.head(redirect_url, allow_redirects=False)
|
||||
if response.status_code == 302:
|
||||
redirect_url = response.headers['Location']
|
||||
|
||||
result = {}
|
||||
result["parse"] = xiutan
|
||||
result["playUrl"] = ''
|
||||
result["url"] = redirect_url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
result = {}
|
||||
videos = []
|
||||
if not page:
|
||||
page = '1'
|
||||
if page == '1':
|
||||
url = f'{xurl}/search/{key}/'
|
||||
else:
|
||||
url = f'{xurl}/search/{key}/{str(page)}/'
|
||||
|
||||
try:
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
section = doc.find('div', class_="list-videos")
|
||||
if section:
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else ""
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
pic = pics['data-src'] if pics and 'data-src' in pics.attrs else ""
|
||||
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
except Exception as e:
|
||||
print(f"Error in searchContentPage: {str(e)}")
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': page,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
330
PY1/51吸瓜.py
Normal file
330
PY1/51吸瓜.py
Normal file
@@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
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.domin=config['site']
|
||||
self.proxies = config.get('proxy',{}) or {}
|
||||
self.plp = config.get('plp', '')
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9'
|
||||
}
|
||||
self.host=self.host_late(self.gethosts())
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
self.getcnh()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=pq(requests.get(self.host, headers=self.headers,proxies=self.proxies).content)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in list(data('.navbar-nav.mr-auto').children('li').items())[1:-3]:
|
||||
if k('ul'):
|
||||
for j in k('ul li').items():
|
||||
classes.append({
|
||||
'type_name': j('a').text(),
|
||||
'type_id': j('a').attr('href').strip(),
|
||||
})
|
||||
else:
|
||||
classes.append({
|
||||
'type_name': k('a').text(),
|
||||
'type_id': k('a').attr('href').strip(),
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('#index article a'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if '@folder' in tid:
|
||||
id=tid.replace('@folder','')
|
||||
videos=self.getfod(id)
|
||||
else:
|
||||
data=pq(requests.get(f"{self.host}{tid}{pg}", headers=self.headers,proxies=self.proxies).content)
|
||||
videos=self.getlist(data('#archive article a'),tid)
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 1 if '@folder' in tid else 99999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url=ids[0] if ids[0].startswith("http") else f"{self.host}{ids[0]}"
|
||||
data=pq(requests.get(url, headers=self.headers,proxies=self.proxies).content)
|
||||
vod = {'vod_play_from': '51吸瓜'}
|
||||
try:
|
||||
clist = []
|
||||
if data('.tags .keywords a'):
|
||||
for k in data('.tags .keywords a').items():
|
||||
title = k.text()
|
||||
href = k.attr('href')
|
||||
clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]')
|
||||
vod['vod_content'] = '点击展开↓↓↓\n'+' '.join(clist)
|
||||
except:
|
||||
vod['vod_content'] = data('.post-title').text()
|
||||
try:
|
||||
plist=[]
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
config = json.loads(k.attr('data-config'))
|
||||
plist.append(f"视频{c}${config['video']['url']}")
|
||||
vod['vod_play_url']='#'.join(plist)
|
||||
except:
|
||||
vod['vod_play_url']=f"请停止活塞运动,可能没有视频${url}"
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=pq(requests.get(f"{self.host}/search/{key}/{pg}", headers=self.headers,proxies=self.proxies).content)
|
||||
return {'list':self.getlist(data('#archive article a')),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
p=0 if re.search(r'\.(m3u8|mp4|flv|ts|mkv|mov|avi|webm)', id) else 1
|
||||
return {'parse': p, 'url': f"{self.plp}{id}", 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
try:
|
||||
url=self.d64(param['url'])
|
||||
match = re.search(r"loadBannerDirect\('([^']*)'", url)
|
||||
if match:
|
||||
url=match.group(1)
|
||||
res = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
return [200, res.headers.get('Content-Type'), self.aesimg(res.content)]
|
||||
except Exception as e:
|
||||
self.log(f"图片代理错误: {str(e)}")
|
||||
return [500, 'text/html', '']
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
self.log(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
self.log(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def gethosts(self):
|
||||
url=self.domin
|
||||
curl=self.getCache('host_51cn')
|
||||
if curl:
|
||||
try:
|
||||
data=pq(requests.get(curl, headers=self.headers, proxies=self.proxies).content)('a').attr('href')
|
||||
if data:
|
||||
parsed_url = urlparse(data)
|
||||
url = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
html = pq(requests.get(url, headers=self.headers, proxies=self.proxies).content)
|
||||
html_pattern = r"Base64\.decode\('([^']+)'\)"
|
||||
html_match = re.search(html_pattern, html('script').eq(-1).text(), re.DOTALL)
|
||||
if not html_match:raise Exception("未找到html")
|
||||
html = pq(b64decode(html_match.group(1)).decode())('script').eq(-4).text()
|
||||
return self.hstr(html)
|
||||
except Exception as e:
|
||||
self.log(f"获取: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getcnh(self):
|
||||
try:
|
||||
data=pq(requests.get(f"{self.host}/homeway.html", headers=self.headers,proxies=self.proxies).content)
|
||||
url=data('.post-content[itemprop="articleBody"] blockquote p').eq(0)('a').attr('href')
|
||||
parsed_url = urlparse(url)
|
||||
host = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
self.setCache('host_51cn',host)
|
||||
except Exception as e:
|
||||
self.log(f"获取: {str(e)}")
|
||||
|
||||
def hstr(self, html):
|
||||
pattern = r"(backupLine\s*=\s*\[\])\s+(words\s*=)"
|
||||
replacement = r"\1, \2"
|
||||
html = re.sub(pattern, replacement, html)
|
||||
data = f"""
|
||||
var Vx = {{
|
||||
range: function(start, end) {{
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {{
|
||||
result.push(i);
|
||||
}}
|
||||
return result;
|
||||
}},
|
||||
|
||||
map: function(array, callback) {{
|
||||
const result = [];
|
||||
for (let i = 0; i < array.length; i++) {{
|
||||
result.push(callback(array[i], i, array));
|
||||
}}
|
||||
return result;
|
||||
}}
|
||||
}};
|
||||
|
||||
Array.prototype.random = function() {{
|
||||
return this[Math.floor(Math.random() * this.length)];
|
||||
}};
|
||||
|
||||
var location = {{
|
||||
protocol: "https:"
|
||||
}};
|
||||
|
||||
function executeAndGetResults() {{
|
||||
var allLines = lineAry.concat(backupLine);
|
||||
var resultStr = JSON.stringify(allLines);
|
||||
return resultStr;
|
||||
}};
|
||||
{html}
|
||||
executeAndGetResults();
|
||||
"""
|
||||
return self.p_qjs(data)
|
||||
|
||||
def p_qjs(self, js_code):
|
||||
try:
|
||||
from com.whl.quickjs.wrapper import QuickJSContext
|
||||
ctx = QuickJSContext.create()
|
||||
result_json = ctx.evaluate(js_code)
|
||||
ctx.destroy()
|
||||
return json.loads(result_json)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"执行失败: {e}")
|
||||
return []
|
||||
|
||||
def get_domains(self):
|
||||
html = pq(requests.get(self.domin, headers=self.headers,proxies=self.proxies).content)
|
||||
html_pattern = r"Base64\.decode\('([^']+)'\)"
|
||||
html_match = re.search(html_pattern, html('script').eq(-1).text(), re.DOTALL)
|
||||
if not html_match:
|
||||
raise Exception("未找到html")
|
||||
html = b64decode(html_match.group(1)).decode()
|
||||
words_pattern = r"words\s*=\s*'([^']+)'"
|
||||
words_match = re.search(words_pattern, html, re.DOTALL)
|
||||
if not words_match:
|
||||
raise Exception("未找到words")
|
||||
words = words_match.group(1).split(',')
|
||||
main_pattern = r"lineAry\s*=.*?words\.random\(\)\s*\+\s*'\.([^']+)'"
|
||||
domain_match = re.search(main_pattern, html, re.DOTALL)
|
||||
if not domain_match:
|
||||
raise Exception("未找到主域名")
|
||||
domain_suffix = domain_match.group(1)
|
||||
domains = []
|
||||
for _ in range(3):
|
||||
random_word = random.choice(words)
|
||||
domain = f"https://{random_word}.{domain_suffix}"
|
||||
domains.append(domain)
|
||||
return domains
|
||||
|
||||
def getfod(self, id):
|
||||
url = f"{self.host}{id}"
|
||||
data = pq(requests.get(url, headers=self.headers, proxies=self.proxies).content)
|
||||
vdata=data('.post-content[itemprop="articleBody"]')
|
||||
r=['.txt-apps','.line','blockquote','.tags','.content-tabs']
|
||||
for i in r:vdata.remove(i)
|
||||
p=vdata('p')
|
||||
videos=[]
|
||||
for i,x in enumerate(vdata('h2').items()):
|
||||
c=i*2
|
||||
videos.append({
|
||||
'vod_id': p.eq(c)('a').attr('href'),
|
||||
'vod_name': p.eq(c).text(),
|
||||
'vod_pic': f"{self.getProxyUrl()}&url={self.e64(p.eq(c+1)('img').attr('data-xkrkllgl'))}",
|
||||
'vod_remarks':x.text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.head(url,headers=self.headers,proxies=self.proxies,timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def getlist(self,data,tid=''):
|
||||
videos = []
|
||||
l='/mrdg' in tid
|
||||
for k in data.items():
|
||||
a=k.attr('href')
|
||||
b=k('h2').text()
|
||||
c=k('span[itemprop="datePublished"]').text()
|
||||
if a and b and c:
|
||||
videos.append({
|
||||
'vod_id': f"{a}{'@folder' if l else ''}",
|
||||
'vod_name': b.replace('\n', ' '),
|
||||
'vod_pic': f"{self.getProxyUrl()}&url={self.e64(k('script').text())}",
|
||||
'vod_remarks': c,
|
||||
'vod_tag':'folder' if l else '',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return videos
|
||||
|
||||
def aesimg(self, word):
|
||||
key = b'f5d965df75336270'
|
||||
iv = b'97b60394abc2fbe1'
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted = unpad(cipher.decrypt(word), AES.block_size)
|
||||
return decrypted
|
||||
404
PY1/51吸瓜动态版.py
Normal file
404
PY1/51吸瓜动态版.py
Normal file
@@ -0,0 +1,404 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 🌈 Love
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'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',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
# Use working dynamic URLs directly
|
||||
self.host = self.get_working_host()
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
self.log(f"使用站点: {self.host}")
|
||||
print(f"使用站点: {self.host}")
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "🌈 51吸瓜"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
# Treat direct media formats as playable without parsing
|
||||
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
try:
|
||||
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
result = {}
|
||||
classes = []
|
||||
|
||||
# Try to get categories from different possible locations
|
||||
category_selectors = [
|
||||
'.category-list ul li',
|
||||
'.nav-menu li',
|
||||
'.menu li',
|
||||
'nav ul li'
|
||||
]
|
||||
|
||||
for selector in category_selectors:
|
||||
for k in data(selector).items():
|
||||
link = k('a')
|
||||
href = (link.attr('href') or '').strip()
|
||||
name = (link.text() or '').strip()
|
||||
# Skip placeholder or invalid entries
|
||||
if not href or href == '#' or not name:
|
||||
continue
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': href
|
||||
})
|
||||
if classes:
|
||||
break
|
||||
|
||||
# If no categories found, create some default ones
|
||||
if not classes:
|
||||
classes = [
|
||||
{'type_name': '首页', 'type_id': '/'},
|
||||
{'type_name': '最新', 'type_id': '/latest/'},
|
||||
{'type_name': '热门', 'type_id': '/hot/'}
|
||||
]
|
||||
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('#index article a'))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"homeContent error: {e}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
return {'list': []}
|
||||
data = self.getpq(response.text)
|
||||
return {'list': self.getlist(data('#index article a, #archive article a'))}
|
||||
except Exception as e:
|
||||
print(f"homeVideoContent error: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
try:
|
||||
if '@folder' in tid:
|
||||
id = tid.replace('@folder', '')
|
||||
videos = self.getfod(id)
|
||||
else:
|
||||
# Build URL properly
|
||||
if tid.startswith('/'):
|
||||
if pg and pg != '1':
|
||||
url = f"{self.host}{tid}page/{pg}/"
|
||||
else:
|
||||
url = f"{self.host}{tid}"
|
||||
else:
|
||||
url = f"{self.host}/{tid}"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article a, #index article a'), tid)
|
||||
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 1 if '@folder' in tid else 99999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"categoryContent error: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
url = f"{self.host}{ids[0]}" if not ids[0].startswith('http') else ids[0]
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'页面加载失败${url}'}]}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
vod = {'vod_play_from': '51吸瓜'}
|
||||
|
||||
# Get content/description
|
||||
try:
|
||||
clist = []
|
||||
if data('.tags .keywords a'):
|
||||
for k in data('.tags .keywords a').items():
|
||||
title = k.text()
|
||||
href = k.attr('href')
|
||||
if title and href:
|
||||
clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]')
|
||||
vod['vod_content'] = ' '.join(clist) if clist else data('.post-title').text()
|
||||
except:
|
||||
vod['vod_content'] = data('.post-title').text() or '51吸瓜视频'
|
||||
|
||||
# Get video URLs (build episode list when multiple players exist)
|
||||
try:
|
||||
plist = []
|
||||
used_names = set()
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
try:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
# Determine a readable episode name from nearby headings if present
|
||||
ep_name = ''
|
||||
try:
|
||||
parent = k.parents().eq(0)
|
||||
# search up to a few ancestors for a heading text
|
||||
for _ in range(3):
|
||||
if not parent: break
|
||||
heading = parent.find('h2, h3, h4').eq(0).text() or ''
|
||||
heading = heading.strip()
|
||||
if heading:
|
||||
ep_name = heading
|
||||
break
|
||||
parent = parent.parents().eq(0)
|
||||
except Exception:
|
||||
ep_name = ''
|
||||
base_name = ep_name if ep_name else f"视频{c}"
|
||||
name = base_name
|
||||
count = 2
|
||||
# Ensure the name is unique
|
||||
while name in used_names:
|
||||
name = f"{base_name} {count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
if video_url:
|
||||
self.log(f"解析到视频: {name} -> {video_url}")
|
||||
print(f"解析到视频: {name} -> {video_url}")
|
||||
plist.append(f"{name}${video_url}")
|
||||
except:
|
||||
continue
|
||||
|
||||
if plist:
|
||||
self.log(f"拼装播放列表,共{len(plist)}个")
|
||||
print(f"拼装播放列表,共{len(plist)}个")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
else:
|
||||
vod['vod_play_url'] = f"未找到视频源${url}"
|
||||
|
||||
except Exception as e:
|
||||
vod['vod_play_url'] = f"视频解析失败${url}"
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
except Exception as e:
|
||||
print(f"detailContent error: {e}")
|
||||
return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'详情页加载失败${ids[0] if ids else ""}'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
url = f"{self.host}/search/{key}/{pg}" if pg != "1" else f"{self.host}/search/{key}/"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article a, #index article a'))
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
except Exception as e:
|
||||
print(f"searchContent error: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
p = 1
|
||||
if self.isVideoFormat(url):
|
||||
# m3u8/mp4 direct play; when using proxy setting, wrap to proxy for m3u8
|
||||
if '.m3u8' in url:
|
||||
url = self.proxy(url)
|
||||
p = 0
|
||||
self.log(f"播放请求: parse={p}, url={url}")
|
||||
print(f"播放请求: parse={p}, url={url}")
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
if param.get('type') == 'img':
|
||||
res=requests.get(param['url'], headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
return [200,res.headers.get('Content-Type'),self.aesimg(res.content)]
|
||||
elif param.get('type') == 'm3u8':return self.m3Proxy(param['url'])
|
||||
else:return self.tsProxy(param['url'])
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
url=self.d64(url)
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
iskey=True
|
||||
for index, string in enumerate(lines):
|
||||
if iskey and 'URI' in string:
|
||||
pattern = r'URI="([^"]*)"'
|
||||
match = re.search(pattern, string)
|
||||
if match:
|
||||
lines[index] = re.sub(pattern, f'URI="{self.proxy(match.group(1), "mkey")}"', string)
|
||||
iskey=False
|
||||
continue
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain = last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def tsProxy(self, url):
|
||||
url = self.d64(url)
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers['Content-Type'], data.content]
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def get_working_host(self):
|
||||
"""Get working host from known dynamic URLs"""
|
||||
# Known working URLs from the dynamic gateway
|
||||
dynamic_urls = [
|
||||
'https://actor.zncielnl.cc/',
|
||||
'https://ability.vgwtswi.xyz',
|
||||
'https://am.vgwtswi.xyz'
|
||||
]
|
||||
|
||||
# Test each URL to find a working one
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code == 200:
|
||||
# Verify it has the expected content structure
|
||||
data = self.getpq(response.text)
|
||||
articles = data('#index article a')
|
||||
if len(articles) > 0:
|
||||
self.log(f"选用可用站点: {url}")
|
||||
print(f"选用可用站点: {url}")
|
||||
return url
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Fallback to first URL if none work (better than crashing)
|
||||
self.log(f"未检测到可用站点,回退: {dynamic_urls[0]}")
|
||||
print(f"未检测到可用站点,回退: {dynamic_urls[0]}")
|
||||
return dynamic_urls[0]
|
||||
|
||||
|
||||
def getlist(self, data, tid=''):
|
||||
videos = []
|
||||
l = '/mrdg' in tid
|
||||
for k in data.items():
|
||||
a = k.attr('href')
|
||||
b = k('h2').text()
|
||||
# Some pages might not include datePublished; use a fallback
|
||||
c = k('span[itemprop="datePublished"]').text() or k('.post-meta, .entry-meta, time').text()
|
||||
if a and b:
|
||||
videos.append({
|
||||
'vod_id': f"{a}{'@folder' if l else ''}",
|
||||
'vod_name': b.replace('\n', ' '),
|
||||
'vod_pic': self.getimg(k('script').text()),
|
||||
'vod_remarks': c or '',
|
||||
'vod_tag': 'folder' if l else '',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return videos
|
||||
|
||||
def getfod(self, id):
|
||||
url = f"{self.host}{id}"
|
||||
data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text)
|
||||
vdata=data('.post-content[itemprop="articleBody"]')
|
||||
r=['.txt-apps','.line','blockquote','.tags','.content-tabs']
|
||||
for i in r:vdata.remove(i)
|
||||
p=vdata('p')
|
||||
videos=[]
|
||||
for i,x in enumerate(vdata('h2').items()):
|
||||
c=i*2
|
||||
videos.append({
|
||||
'vod_id': p.eq(c)('a').attr('href'),
|
||||
'vod_name': p.eq(c).text(),
|
||||
'vod_pic': f"{self.getProxyUrl()}&url={p.eq(c+1)('img').attr('data-xkrkllgl')}&type=img",
|
||||
'vod_remarks':x.text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getimg(self, text):
|
||||
match = re.search(r"loadBannerDirect\('([^']+)'", text)
|
||||
if match:
|
||||
url = match.group(1)
|
||||
return f"{self.getProxyUrl()}&url={url}&type=img"
|
||||
else:
|
||||
return ''
|
||||
|
||||
def aesimg(self, word):
|
||||
key = b'f5d965df75336270'
|
||||
iv = b'97b60394abc2fbe1'
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted = unpad(cipher.decrypt(word), AES.block_size)
|
||||
return decrypted
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
318
PY1/58视频.py
Normal file
318
PY1/58视频.py
Normal file
@@ -0,0 +1,318 @@
|
||||
from Crypto.Cipher import AES
|
||||
from base.spider import Spider
|
||||
import re,sys,json,base64,requests
|
||||
from Crypto.Util.Padding import unpad
|
||||
from urllib.parse import quote, unquote, urljoin
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
headers = {
|
||||
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36 uni-app Html5Plus/1.0 (Immersed/0.6666667)",
|
||||
'Connection': "Keep-Alive",
|
||||
'Accept-Encoding': "gzip",
|
||||
'Version': "1.3.26",
|
||||
'Token': ""
|
||||
}
|
||||
play_headers = {'User-Agent': 'io.dcloud.application.DCloudApplication/1.3.26 (Linux;Android 12)'}
|
||||
host, datakey, dataiv, deviceid, home_class, block_id,bn = '', '', '', '', '',[],b'\xe6\x83\x85\xe8\x89\xb2'
|
||||
|
||||
def init(self, extend=""):
|
||||
try:
|
||||
config = json.loads(extend)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
config = {}
|
||||
|
||||
self.host = config.get("host", "https://58api.zggggs.com")
|
||||
self.datakey = config.get("datakey", "58928cae68092afc")
|
||||
self.dataiv = config.get("dataiv", "e9d732a1edcdcc0a")
|
||||
self.deviceid = config.get("deviceid", "d60ddbcd469741f68e2755dca38f5171")
|
||||
payload = {
|
||||
'UserId': "0",
|
||||
'device_id': self.host
|
||||
}
|
||||
response = self.post(f'{self.host}/addons/appto/app.php/tindex/home_config2', data=payload, headers=self.headers).json()
|
||||
data = self.decrypt(response['data'])
|
||||
data2 = json.loads(data)
|
||||
block_id = []
|
||||
for i in data2['viphome']:
|
||||
block_id.append(i['id'])
|
||||
self.block_id = block_id
|
||||
self.home_class = data2['home']
|
||||
|
||||
def homeContent(self, filter):
|
||||
home_class = self.home_class
|
||||
classes = []
|
||||
for i in home_class:
|
||||
if i['id'] == 0:
|
||||
continue
|
||||
classes.append({'type_id':i['id'],'type_name':i['title']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
payload = {
|
||||
'UserId': "0",
|
||||
'device_id': self.deviceid,
|
||||
'Id': "0",
|
||||
'Type': "1",
|
||||
'Page': "1",
|
||||
'Limit': "10"
|
||||
}
|
||||
response = self.post(f'{self.host}/addons/appto/app.php/tindex/home_vod_list2', data=payload, headers=self.headers).json()
|
||||
data = self.decrypt(response['data'])
|
||||
data2 = json.loads(data)
|
||||
vods = []
|
||||
for i in data2['sections']:
|
||||
vods.extend(i['vods'])
|
||||
vods.extend(data2['vods'])
|
||||
videos = []
|
||||
for i in vods:
|
||||
if i['type_id'] in self.block_id or i['group_id'] != 0 or self.bn.decode('utf-8') in i['vod_class']:
|
||||
continue
|
||||
vod_pic = i.get('vod_pic')
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'https://', 1)
|
||||
videos.append({
|
||||
'vod_id': i.get('vod_id'),
|
||||
'vod_name': i.get('vod_name'),
|
||||
'vod_class': i.get('vod_class'),
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': i.get('vod_remarks'),
|
||||
'vod_score': i.get('vod_score')
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def detailContent(self, ids):
|
||||
payload = {
|
||||
'UserId': "0",
|
||||
'device_id': self.deviceid,
|
||||
'id': ids
|
||||
}
|
||||
data = self.post(f"{self.host}/addons/appto/app.php/tindex/page_player", data=payload, headers=self.headers).json()
|
||||
data2 = self.decrypt(data['data'])
|
||||
data3 = json.loads(data2)
|
||||
if data3['type_id'] in self.block_id:
|
||||
return {'list': []}
|
||||
if not data3['group_id'] == 0:
|
||||
return {'list': []}
|
||||
videos = []
|
||||
videos.append({
|
||||
'vod_id': data3.get('vod_id'),
|
||||
'vod_name': data3.get('vod_name'),
|
||||
'vod_content': data3.get('vod_blurb'),
|
||||
'vod_remarks': data3.get('vod_serial'),
|
||||
'vod_year': data3.get('vod_year'),
|
||||
'vod_area': data3.get('vod_area'),
|
||||
'vod_play_from': '58视频',
|
||||
'vod_play_url': data3['vod_play_url']
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f"{self.host}/addons/appto/app.php/tindex/search_film"
|
||||
videos = []
|
||||
type_list = {'film','short'}
|
||||
for search_type in type_list:
|
||||
payload = {
|
||||
'UserId': "0",
|
||||
'device_id': self.deviceid,
|
||||
'Search': key,
|
||||
'type': search_type,
|
||||
'Page': pg,
|
||||
'Limit': "10"
|
||||
}
|
||||
response = self.post(url, data=payload, headers=self.headers).json()
|
||||
data = self.decrypt(response['data'])
|
||||
vods =json.loads(data)['vods']
|
||||
|
||||
for i in vods['list']:
|
||||
if i['type_id'] in self.block_id or self.bn.decode('utf-8') in i['vod_class'] or b'\xe4\xbc\x9a\xe5\x91\x98'.decode('utf-8') in i['vod_type_name']:
|
||||
continue
|
||||
vod_pic = i['vod_pic']
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'https://', 1)
|
||||
video = {
|
||||
"vod_id": i['vod_id'],
|
||||
"vod_name": i['vod_name'],
|
||||
"vod_class": i['vod_class'],
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": i['vod_remarks']
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
return {'list': videos, 'page': pg, 'limit': vods['limit'], 'total': vods['total']}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
payload = {
|
||||
'UserId': "0",
|
||||
'device_id': self.host,
|
||||
'Id': tid,
|
||||
'Type': "1",
|
||||
'Page': pg,
|
||||
'Limit': "10"
|
||||
}
|
||||
response = self.post(f'{self.host}/addons/appto/app.php/tindex/home_vod_list2', data=payload,headers=self.headers).json()
|
||||
data = self.decrypt(response['data'])
|
||||
data2 = json.loads(data)
|
||||
videos = []
|
||||
for i in data2['vods']:
|
||||
if 'payload' in i or 'banner' in i['vod_class']:
|
||||
continue
|
||||
vod_pic = i.get('vod_pic')
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'https://', 1)
|
||||
videos.append({
|
||||
'vod_id': i.get('vod_id'),
|
||||
'vod_name': i.get('vod_name'),
|
||||
'vod_class': i.get('vod_class'),
|
||||
'vod_pic': vod_pic,
|
||||
'vod_score':i.get('vod_score'),
|
||||
'vod_remarks': i.get('vod_remarks'),
|
||||
'vod_score': i.get('vod_score')
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if '.m3u8' in id:
|
||||
try:
|
||||
proxyurl = f'{self.getProxyUrl(True)}&type=58sp'
|
||||
except Exception:
|
||||
proxyurl = 'http://127.0.0.1:9978/proxy?do=py&type=58sp'
|
||||
url = f"{proxyurl}&url={quote(id,safe='')}"
|
||||
return {'jx': 0, 'playUrl': '', 'parse': 0, 'url': url,'header': self.play_headers}
|
||||
|
||||
def proxy58sp(self, params):
|
||||
url = unquote(params['url'])
|
||||
data = self.modify_m3u8(url)
|
||||
return [200, "application/vnd.apple.mpegurl", data]
|
||||
|
||||
def decrypt(self,ciphertext):
|
||||
try:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
key_bytes = self.datakey.encode('utf-8')
|
||||
iv_bytes = self.dataiv.encode('utf-8')
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||||
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||
return decrypted_data.decode('utf-8')
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def modify_m3u8(self, url, retries=3, timeout=10):
|
||||
current_url = url
|
||||
while True:
|
||||
try:
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.get(current_url, timeout=timeout,headers=self.play_headers)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
break
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
if attempt == retries - 1:
|
||||
raise Exception(f"请求失败: {str(e)}")
|
||||
print(f"请求尝试 {attempt + 1}/{retries} 失败,正在重试...")
|
||||
base_url = current_url.rsplit('/', 1)[0] + '/'
|
||||
lines = content.strip().split('\n')
|
||||
is_master_playlist = any(line.startswith('#EXT-X-STREAM-INF:') for line in lines)
|
||||
if is_master_playlist:
|
||||
highest_bandwidth = 0
|
||||
best_playlist_url = None
|
||||
bandwidth_regex = re.compile(r'BANDWIDTH=(\d+)')
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#EXT-X-STREAM-INF:'):
|
||||
match = bandwidth_regex.search(line)
|
||||
if match:
|
||||
bandwidth = int(match.group(1))
|
||||
if i + 1 < len(lines) and not lines[i + 1].startswith('#'):
|
||||
playlist_url = lines[i + 1].strip()
|
||||
if not playlist_url.startswith(('http:', 'https:')):
|
||||
playlist_url = urljoin(base_url, playlist_url)
|
||||
if bandwidth > highest_bandwidth:
|
||||
highest_bandwidth = bandwidth
|
||||
best_playlist_url = playlist_url
|
||||
if best_playlist_url:
|
||||
print(f"选择最高清晰度流: {highest_bandwidth}bps")
|
||||
current_url = best_playlist_url
|
||||
continue
|
||||
else:
|
||||
raise Exception("未找到有效的子播放列表")
|
||||
key_regex = re.compile(r'#EXT-X-KEY:(.*)URI="([^"]+)"(.*)')
|
||||
segment_regex = re.compile(r'#EXTINF:([\d.]+),')
|
||||
m3u8_output = []
|
||||
first_segment_index = -1
|
||||
segment_durations = []
|
||||
segment_indices = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#EXTINF:'):
|
||||
match = segment_regex.search(line)
|
||||
if match:
|
||||
duration = float(match.group(1))
|
||||
segment_durations.append(duration)
|
||||
segment_indices.append(i)
|
||||
modified_remove_start_indices = []
|
||||
if len(segment_durations) >= 2:
|
||||
second_duration_str = "{0:.3f}".format(segment_durations[1])
|
||||
if second_duration_str.endswith('67'):
|
||||
print(f"第2个分片({second_duration_str})符合规则,将删除前2个分片")
|
||||
modified_remove_start_indices = segment_indices[:2]
|
||||
elif len(segment_durations) >= 3:
|
||||
third_duration_str = "{0:.3f}".format(segment_durations[2])
|
||||
if third_duration_str.endswith('67'):
|
||||
print(f"第3个分片({third_duration_str})符合规则,将删除前3个分片")
|
||||
modified_remove_start_indices = segment_indices[:3]
|
||||
lines_to_remove = set()
|
||||
for seg_idx in modified_remove_start_indices:
|
||||
lines_to_remove.add(seg_idx)
|
||||
if seg_idx + 1 < len(lines):
|
||||
lines_to_remove.add(seg_idx + 1)
|
||||
for i, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith('#EXT-X-KEY:'):
|
||||
match = key_regex.search(line)
|
||||
if match:
|
||||
prefix = match.group(1)
|
||||
key_url = match.group(2)
|
||||
suffix = match.group(3)
|
||||
if not key_url.startswith(('http:', 'https:')):
|
||||
key_url = urljoin(base_url, key_url)
|
||||
updated_key_line = f'#EXT-X-KEY:{prefix}URI="{key_url}"{suffix}'
|
||||
m3u8_output.append(updated_key_line)
|
||||
print(f"补全加密KEY URL: {key_url}")
|
||||
continue
|
||||
if i in lines_to_remove:
|
||||
print(f"移除行: {line}")
|
||||
continue
|
||||
if not line.startswith('#') and i > first_segment_index:
|
||||
segment_url = line
|
||||
if not segment_url.startswith(('http:', 'https:')):
|
||||
segment_url = urljoin(base_url, segment_url)
|
||||
m3u8_output.append(segment_url)
|
||||
else:
|
||||
m3u8_output.append(line)
|
||||
if first_segment_index == -1 and line.startswith('#EXTINF:'):
|
||||
first_segment_index = i
|
||||
if not any(not line.startswith('#') for line in m3u8_output):
|
||||
raise Exception("未找到TS片段")
|
||||
if not m3u8_output or not m3u8_output[0].startswith('#EXTM3U'):
|
||||
m3u8_output.insert(0, '#EXTM3U')
|
||||
return '\n'.join(m3u8_output)
|
||||
except Exception as e:
|
||||
return f"#EXTM3U\n#EXT-X-ERROR:{str(e)}"
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "58sp":
|
||||
return self.proxy58sp(params)
|
||||
return None
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
110
PY1/91.py
Normal file
110
PY1/91.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
from Crypto.Cipher import AES
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend='{}'):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://91-short.com'
|
||||
|
||||
headers = {
|
||||
'accept': 'application/json, text/plain, */*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="141", "Google Chrome";v="141"',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.55 Safari/537.36',
|
||||
}
|
||||
|
||||
cache={}
|
||||
|
||||
def getvs(self,data):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
a = i("a")
|
||||
videos.append({
|
||||
'vod_id': a.attr('href'),
|
||||
'vod_name': a.attr('title'),
|
||||
'vod_pic': self.getProxyUrl()+"&url="+i("img").attr("data-cover"),
|
||||
'vod_remark': i(".module-item-caption").text() or i(".module-item-ru").text(),
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeContent(self, filter):
|
||||
resp=self.fetch(self.host,headers=self.headers)
|
||||
tab1=pq(resp.content)("#tablist > a")
|
||||
resp = self.fetch(f"{self.host}/film/home_recommend_list", headers=self.headers)
|
||||
tab2 = pq(resp.content)("#tablist > a")
|
||||
classes = []
|
||||
for k in (tab1+tab2).items():
|
||||
href=k.attr('href')
|
||||
if not href or "http" in href:
|
||||
continue
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': href,
|
||||
})
|
||||
return {'class':classes}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if pg=="1":
|
||||
resp=self.fetch(self.host+tid,headers=self.headers)
|
||||
qu=".module-items > .module-item > .module-item-cover"
|
||||
doc=pq(resp.content)
|
||||
stext=doc('main').next('script').html()
|
||||
self.cache[tid]=stext.strip().split('\n',1)[0].strip().split('=',1)[-1].replace('"','').strip()
|
||||
else:
|
||||
resp=self.fetch(self.host+self.cache[tid],headers=self.headers)
|
||||
qu = ".module-item > .module-item-cover"
|
||||
doc=pq(resp.content.decode())
|
||||
self.cache[tid]=doc("script").eq(-1).text()
|
||||
result = {}
|
||||
result['list'] = self.getvs(doc(qu))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
resp=self.fetch(self.host+ids[0],headers=self.headers)
|
||||
doc=pq(resp.content)
|
||||
stext=doc('.player-wrapper > script').eq(-1).html().strip()
|
||||
try:
|
||||
url=stext.split('\n')[-1].split('=')[-1].replace('"','').strip()
|
||||
p=0
|
||||
except Exception as e:
|
||||
url=self.host+ids[0]
|
||||
p=1
|
||||
vod = {
|
||||
'vod_director': '沐辰',
|
||||
'vod_play_from': '91——short',
|
||||
'vod_play_url': f'{doc(".module-item-in").text() or doc("h2.module-title").text()}${url}@@{p}'
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
resp=self.fetch(f'{self.host}/search',headers=self.headers,params={'wd':key})
|
||||
qu = ".module-items > .module-item > .module-item-cover"
|
||||
data = pq(resp.content)(qu)
|
||||
return {'list':self.getvs(data),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url,p=id.split('@@')
|
||||
return {'parse': int(p), 'url': url}
|
||||
|
||||
def localProxy(self, param):
|
||||
res=self.fetch(param['url'])
|
||||
key = b'Jui7X#cdleN^3eZb'
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
decrypted = cipher.decrypt(res.content)
|
||||
return [200,res.headers.get('Content-Type'),decrypted]
|
||||
236
PY1/911.py
Normal file
236
PY1/911.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# coding=utf-8
|
||||
# !/python
|
||||
import sys
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from urllib.parse import urljoin
|
||||
from base.spider import Spider
|
||||
import time
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
# 全局配置
|
||||
xurl = "https://911blw.com"
|
||||
backup_urls = ["https://hlj.fun", "https://911bl16.com"]
|
||||
headerx = {
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
|
||||
"Referer": "https://911blw.com",
|
||||
"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"
|
||||
}
|
||||
IMAGE_FILTER = ["/usr/themes/ads-close.png", "close", "icon", "logo"]
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "911爆料网"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def fetch_page(self, url, use_backup=False):
|
||||
global xurl
|
||||
original_url = url
|
||||
if use_backup:
|
||||
for backup in backup_urls:
|
||||
test_url = url.replace(xurl, backup)
|
||||
try:
|
||||
time.sleep(1)
|
||||
res = requests.get(test_url, headers=headerx, timeout=10)
|
||||
res.raise_for_status()
|
||||
res.encoding = "utf-8"
|
||||
text = res.text
|
||||
if len(text) > 1000:
|
||||
print(f"[DEBUG] 使用备用 {backup}: {test_url}")
|
||||
return text
|
||||
except:
|
||||
continue
|
||||
print(f"[ERROR] 所有备用失败,回退原 URL")
|
||||
|
||||
try:
|
||||
time.sleep(1)
|
||||
res = requests.get(original_url, headers=headerx, timeout=10)
|
||||
res.raise_for_status()
|
||||
res.encoding = "utf-8"
|
||||
text = res.text
|
||||
doc = BeautifulSoup(text, "html.parser")
|
||||
title = doc.title.string if doc.title else "无标题"
|
||||
print(f"[DEBUG] 页面 {original_url}: 长度={len(text)}, 标题={title}")
|
||||
if len(text) < 1000:
|
||||
print(f"[DEBUG] 内容过短,尝试备用域名")
|
||||
return self.fetch_page(original_url, use_backup=True)
|
||||
return text
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 请求失败 {original_url}: {e}")
|
||||
return None
|
||||
|
||||
def extract_content(self, html, url):
|
||||
videos = []
|
||||
if not html:
|
||||
return videos
|
||||
|
||||
doc = BeautifulSoup(html, "html.parser")
|
||||
containers = doc.select("ul.row li, div.article-item, article, .post-item, div[class*='item']")
|
||||
print(f"[DEBUG] 找到 {len(containers)} 个容器")
|
||||
|
||||
for i, vod in enumerate(containers[:20], 1):
|
||||
try:
|
||||
# 标题
|
||||
title_elem = vod.select_one("h2.headline, .headline, a[title]")
|
||||
name = title_elem.get("title") or title_elem.get_text(strip=True) if title_elem else ""
|
||||
if not name:
|
||||
name_match = re.search(r'headline">(.+?)<', str(vod))
|
||||
name = name_match.group(1).strip() if name_match else ""
|
||||
|
||||
# 链接
|
||||
link_elem = vod.select_one("a")
|
||||
id = urljoin(xurl, link_elem["href"]) if link_elem else ""
|
||||
|
||||
# 备注
|
||||
remarks_elem = vod.select_one("span.small, time, .date")
|
||||
remarks = remarks_elem.get_text(strip=True) if remarks_elem else ""
|
||||
if not remarks:
|
||||
remarks_match = re.search(r'datePublished[^>]*>(.+?)<', str(vod))
|
||||
remarks = remarks_match.group(1).strip() if remarks_match else ""
|
||||
|
||||
# 图片 - 扩展属性
|
||||
img = vod.select_one("img")
|
||||
pic = None
|
||||
if img:
|
||||
# 检查多种图片属性
|
||||
for attr in ["data-lazy-src", "data-original", "data-src", "src"]:
|
||||
pic = img.get(attr)
|
||||
if pic:
|
||||
break
|
||||
# 检查背景图片
|
||||
if not pic:
|
||||
bg_div = vod.select_one("div[style*='background-image']")
|
||||
if bg_div and "background-image" in bg_div.get("style", ""):
|
||||
bg_match = re.search(r'url\([\'"]?(.+?)[\'"]?\)', bg_div["style"])
|
||||
pic = bg_match.group(1) if bg_match else None
|
||||
if pic:
|
||||
pic = urljoin(xurl, pic)
|
||||
alt = img.get("alt", "").lower() if img else ""
|
||||
if any(f in pic.lower() or f in alt for f in IMAGE_FILTER):
|
||||
pic = None
|
||||
print(f"[DEBUG] 项 {i} 图片: {pic}, 属性={img.attrs if img else '无img'}")
|
||||
|
||||
# 简介
|
||||
desc_match = re.search(r'og:description" content="(.+?)"', html)
|
||||
description = desc_match.group(1) if desc_match else ""
|
||||
|
||||
if name and id:
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name[:100],
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks,
|
||||
"vod_content": description
|
||||
}
|
||||
videos.append(video)
|
||||
print(f"[DEBUG] 项 {i}: 标题={name[:50]}..., 链接={id}, 图片={pic}")
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] 项 {i} 错误: {e}")
|
||||
continue
|
||||
|
||||
print(f"[DEBUG] 提取 {len(videos)} 个项")
|
||||
return videos
|
||||
|
||||
def homeVideoContent(self):
|
||||
url = f"{xurl}/category/jrgb/1/"
|
||||
html = self.fetch_page(url)
|
||||
videos = self.extract_content(html, url)
|
||||
return {'list': videos}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': []}
|
||||
categories = [
|
||||
{"type_id": "/category/jrgb/", "type_name": "最新爆料"},
|
||||
{"type_id": "/category/rmgb/", "type_name": "精选大瓜"},
|
||||
{"type_id": "/category/blqw/", "type_name": "猎奇吃瓜"},
|
||||
{"type_id": "/category/rlph/", "type_name": "TOP5大瓜"},
|
||||
{"type_id": "/category/ssdbl/", "type_name": "社会热点"},
|
||||
{"type_id": "/category/hjsq/", "type_name": "海角社区"},
|
||||
{"type_id": "/category/mrds/", "type_name": "每日大赛"},
|
||||
{"type_id": "/category/xyss/", "type_name": "校园吃瓜"},
|
||||
{"type_id": "/category/mxhl/", "type_name": "明星吃瓜"},
|
||||
{"type_id": "/category/whbl/", "type_name": "网红爆料"},
|
||||
{"type_id": "/category/bgzq/", "type_name": "反差爆料"},
|
||||
{"type_id": "/category/fljq/", "type_name": "网黄福利"},
|
||||
{"type_id": "/category/crfys/", "type_name": "午夜剧场"},
|
||||
{"type_id": "/category/thjx/", "type_name": "探花经典"},
|
||||
{"type_id": "/category/dmhv/", "type_name": "禁漫天堂"},
|
||||
{"type_id": "/category/slec/", "type_name": "吃瓜精选"},
|
||||
{"type_id": "/category/zksr/", "type_name": "重口调教"},
|
||||
{"type_id": "/category/crlz/", "type_name": "精选连载"}
|
||||
]
|
||||
result['class'] = categories
|
||||
return result
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
url = f"{xurl}{cid}{pg}/" if pg != "1" else f"{xurl}{cid}"
|
||||
html = self.fetch_page(url)
|
||||
videos = self.extract_content(html, url)
|
||||
return {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
videos = []
|
||||
did = ids[0]
|
||||
html = self.fetch_page(did)
|
||||
if html:
|
||||
source_match = re.search(r'"url":"(.*?)"', html)
|
||||
purl = source_match.group(1).replace("\\", "") if source_match else ""
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_play_from": "爆料",
|
||||
"vod_play_url": purl,
|
||||
"vod_content": re.search(r'og:description" content="(.+?)"', html).group(1) if re.search(r'og:description" content="(.+?)"', html) else ""
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {"parse": 0, "playUrl": "", "url": id, "header": headerx}
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
return self.searchContentPage(key, quick, "1")
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
url = f"{xurl}/search/{key}/{page}/"
|
||||
html = self.fetch_page(url)
|
||||
videos = self.extract_content(html, url)
|
||||
return {'list': videos, 'page': page, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
spider = Spider()
|
||||
# 测试首页推荐
|
||||
result = spider.homeVideoContent()
|
||||
print(f"测试首页推荐: {len(result['list'])} 个项")
|
||||
for item in result['list'][:3]:
|
||||
print(item)
|
||||
# 测试分类
|
||||
for cate in ["jrgb", "rmgb", "blqw"]:
|
||||
result = spider.categoryContent(f"/category/{cate}/", "1", False, {})
|
||||
print(f"测试分类 {cate}: {len(result['list'])} 个项")
|
||||
for item in result['list'][:2]:
|
||||
print(item)
|
||||
84
PY1/91RB1.py
Normal file
84
PY1/91RB1.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#author Kyle
|
||||
import re, sys, time, urllib.parse
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as BaseSpider
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
super().__init__(); self.base = 'https://www.91rb.com'; self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': self.base + '/'}
|
||||
def getName(self): return '91热爆'
|
||||
def init(self, extend=""): self.extend = extend or ''; return {'class': 'movie'}
|
||||
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4)(\?|$)', url))
|
||||
def manualVideoCheck(self): return False
|
||||
def destroy(self): pass
|
||||
def homeContent(self, filter): return {'class': [{'type_name': '最新上传', 'type_id': 'latest-updates'}, {'type_name': '热门视频', 'type_id': 'most-popular'}, {'type_name': '收藏最多', 'type_id': 'most-favourited'}, {'type_name': '日本AV', 'type_id': 'tags/av2/'}, {'type_name': 'jav', 'type_id': 'tags/jav/'}, {'type_name': '韩国', 'type_id': 'tags/20c3f16d021b069ce3af1da50b15bd83/'}]}
|
||||
def homeVideoContent(self):
|
||||
try: return self._listPage(self._buildListUrl('latest-updates', '1'))
|
||||
except Exception as e: self.log(f'homeVideoContent error: {e}'); return {'list': []}
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
try: return self._listPage(self._buildListUrl(tid, pg), page=pg)
|
||||
except Exception as e: self.log(f'categoryContent error: {e}'); return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 48, 'total': 0}
|
||||
def detailContent(self, ids):
|
||||
vid = self._ensure_id(ids[0]); detail_url = f"{self.base}/videos/{vid}/"; name = f'视频 {vid}'; pic = ''
|
||||
try:
|
||||
r = self.fetch(detail_url, headers=self.headers, timeout=10, allow_redirects=True)
|
||||
if r and hasattr(r, 'text'):
|
||||
doc = self.html(r.text)
|
||||
if doc:
|
||||
name = ''.join(doc.xpath('//h1//text()')).strip() or name
|
||||
ogs = doc.xpath('//meta[@property="og:image"]/@content'); tws = doc.xpath('//meta[@name="twitter:image"]/@content')
|
||||
pic = ogs[0].strip() if ogs else (tws[0].strip() if tws else '')
|
||||
if pic: pic = self._abs_url(pic)
|
||||
except Exception as e: self.log(f'detailContent fetch error: {e}')
|
||||
if not pic: pic = self._cover_fallback(vid)
|
||||
vod = {'vod_id': str(vid), 'vod_name': name, 'vod_pic': pic, 'type_name': '', 'vod_year': '', 'vod_remarks': '', 'vod_content': '', 'vod_play_from': '91RB', 'vod_play_url': f'正片${vid}'}
|
||||
return {'list': [vod]}
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
key_enc = urllib.parse.quote(key); url = f"{self.base}/search/{key_enc}/"
|
||||
if pg != '1': url = url.rstrip('/') + f'/{pg}/'
|
||||
try: return self._listPage(url, page=pg)
|
||||
except Exception as e: self.log(f'searchContent error: {e}'); return {'list': [], 'page': pg, 'pagecount': 1, 'total': 0}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
vid = self._ensure_id(id); group = int(vid) - (int(vid) % 1000)
|
||||
m3u8 = f"https://91rbnet.gslb-al.com/hls/contents/videos/{group}/{vid}/{vid}.mp4/index.m3u8"
|
||||
try:
|
||||
r = self.fetch(m3u8, headers=self.headers, timeout=5, allow_redirects=True, verify=True, stream=True)
|
||||
if r.status_code >= 400: self.log(f'm3u8 head status={r.status_code}, fallback to direct anyway')
|
||||
except Exception as e: self.log(f'playerContent HEAD error: {e}')
|
||||
return {'parse': 0, 'playUrl': '', 'url': m3u8, 'header': self.headers}
|
||||
def localProxy(self, param): return None
|
||||
def _buildListUrl(self, tid, pg):
|
||||
path = tid.strip('/') or 'latest-updates'; page_suffix = f"/{pg}/" if str(pg) != '1' else '/'
|
||||
if path.startswith('categories') or path in ['latest-updates', 'most-popular', 'most-favourited']: return f"{self.base}/{path}{page_suffix}"
|
||||
return f"{self.base}/{path}{page_suffix}"
|
||||
def _abs_url(self, url):
|
||||
if not url: return url
|
||||
u = url.strip()
|
||||
return 'https:' + u if u.startswith('//') else (self.base + u if u.startswith('/') else u)
|
||||
def _parse_srcset_first(self, srcset):
|
||||
if not srcset: return ''
|
||||
return srcset.split(',')[0].strip().split(' ')[0]
|
||||
def _cover_fallback(self, vid):
|
||||
try: iv = int(vid); group = iv - (iv % 1000); return f'https://rimg.iomycdn.com/videos_screenshots/{group}/{iv}/preview.jpg'
|
||||
except Exception: return ''
|
||||
def _listPage(self, url, page='1'):
|
||||
doc = self.html(self.fetch(url, headers=self.headers, timeout=10).text)
|
||||
if doc is None: return {'list': [], 'page': page, 'pagecount': 1, 'total': 0}
|
||||
nodes, videos, seen = doc.xpath('//main//a[contains(@href, "/videos/")]'), [], set()
|
||||
for a in nodes:
|
||||
href = a.get('href') or ''; m = re.search(r'/videos/(\d+)/', href)
|
||||
if not m or '/login' in href: continue
|
||||
vid = m.group(1);
|
||||
if vid in seen: continue
|
||||
seen.add(vid); title = ''; img = a.xpath('.//img')
|
||||
if img:
|
||||
im = img[0]; title = (im.get('alt') or '').strip()
|
||||
pic = (im.get('src') or im.get('data-src') or im.get('data-original') or '').strip()
|
||||
if not pic: pic = self._parse_srcset_first(im.get('data-srcset') or im.get('srcset') or '')
|
||||
pic = self._abs_url(pic)
|
||||
else: title = (a.text or '').strip(); pic = ''
|
||||
title = title or f'视频 {vid}'
|
||||
if not pic or pic.startswith('data:'): pic = self._cover_fallback(vid)
|
||||
videos.append({'vod_id': vid, 'vod_name': title, 'vod_pic': pic, 'vod_remarks': ''})
|
||||
return {'list': videos, 'page': str(page), 'pagecount': 9999, 'limit': 48, 'total': 0}
|
||||
def _ensure_id(self, s):
|
||||
m = re.search(r'(\d+)', str(s)); return m.group(1) if m else str(s)
|
||||
186
PY1/91rb.py
Normal file
186
PY1/91rb.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#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 + '/'
|
||||
}
|
||||
self.proxy_base = 'https://vpsdn.leuse.top/proxy?single=true&url='
|
||||
|
||||
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_with_proxy(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_with_proxy(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_with_proxy(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)
|
||||
|
||||
def _fetch_with_proxy(self, url, headers=None, timeout=10, **kwargs):
|
||||
"""通过代理服务器获取内容"""
|
||||
proxy_url = f'{self.proxy_base}{urllib.parse.quote(url)}'
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
return super().fetch(proxy_url, headers=headers, timeout=timeout, **kwargs)
|
||||
313
PY1/AppMuou.py
Normal file
313
PY1/AppMuou.py
Normal file
@@ -0,0 +1,313 @@
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
|
||||
"""
|
||||
示例
|
||||
{
|
||||
"key": "key",
|
||||
"name": "name",
|
||||
"type": 3,
|
||||
"api": "./AppMuou.py",
|
||||
"ext": {
|
||||
"host": "https://muouapp.oss-cn-hangzhou.domain.com/xxx/xxx.txt", 应用域名(支持txt或域名)
|
||||
"name": "xxx", 应用名称
|
||||
"version": "4.2.0" 应用版本号
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from base.spider import Spider
|
||||
from Crypto.Util.Padding import unpad
|
||||
import re,sys,time,json,base64,hashlib,urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
host,name,version,data_key,data_iv,cms_host,jx_api,playerinfo,= '', '', '', '', '', '', '',[]
|
||||
headers = {
|
||||
'User-Agent': "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 MUOUAPP/10.8.4506.400",
|
||||
'Accept-Encoding': "gzip",
|
||||
'brand-model': "xiaomi",
|
||||
'app-device': "nodata",
|
||||
'app-time': "",
|
||||
'sys-version': "12",
|
||||
'device': "831395239bddf2e6",
|
||||
'os': "Android",
|
||||
'app-version': version
|
||||
}
|
||||
|
||||
def init(self, extend=""):
|
||||
try:
|
||||
config = json.loads(extend)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
config = {}
|
||||
name = config.get("name", "muou")
|
||||
self.headers['app-version'] = config.get("version", "4.2.0")
|
||||
self.host = config['host']
|
||||
if not re.match(r'^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?(\/)?$', self.host):
|
||||
self.host = self.fetch(self.host, headers=self.headers, timeout=10, verify=False).text.rstrip('/')
|
||||
timestamp = int(time.time())
|
||||
self.headers['app-time'] = str(timestamp)
|
||||
inner_sha1 = hashlib.sha1(f"{timestamp}{name}".encode('utf-8')).hexdigest()
|
||||
outer_sha1 = hashlib.sha1(f"{timestamp}{inner_sha1}muouapp".encode('utf-8')).hexdigest()
|
||||
payload = { 't': timestamp, 'n': inner_sha1, 'm': outer_sha1 }
|
||||
try:
|
||||
response = self.post(f'{self.host}/app_info.php', data=payload)
|
||||
if response.status_code != 200:
|
||||
return '-2 数据获取失败'
|
||||
dat = response.text
|
||||
except:
|
||||
return '-2 数据获取失败'
|
||||
try:
|
||||
dat2 = json.loads(dat)
|
||||
except:
|
||||
return '-2 数据获取失败'
|
||||
data = dat2.get('data', '')
|
||||
a = dat2.get('a', '')
|
||||
e = dat2.get('e', '')
|
||||
s = dat2.get('s', '')
|
||||
if not a or not e or not s:
|
||||
return '-3 参数获取失败'
|
||||
data2 = self.t(data, s, e)
|
||||
key = hashlib.md5(a.encode('utf-8')).hexdigest()[:16]
|
||||
iv = hashlib.md5(outer_sha1.encode('utf-8')).hexdigest()[:16]
|
||||
result = self.decrypt(data2, key, iv)
|
||||
if not result:
|
||||
return '-4 解密失败'
|
||||
try:
|
||||
dat3 = json.loads(result)
|
||||
except:
|
||||
return '-5 解密失败'
|
||||
key2 = dat3['key']
|
||||
iv2 = dat3['iv']
|
||||
self.data_key = hashlib.md5(key2.encode('utf-8')).hexdigest()[:16]
|
||||
self.data_iv = hashlib.md5(iv2.encode('utf-8')).hexdigest()[:16]
|
||||
self.cms_host = dat3['HBqq']
|
||||
jx_api = dat3.get('HBrjjg','')
|
||||
if jx_api.startswith('http'):
|
||||
self.jx_api = jx_api
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
if not self.cms_host:
|
||||
return {'list': []}
|
||||
self.headers['app-time'] = str(int(time.time()))
|
||||
try:
|
||||
response = self.fetch(f'{self.cms_host}/api.php/v1.vod/types', headers=self.headers).text
|
||||
except Exception as e:
|
||||
return {"class": [], "filters": {}}
|
||||
|
||||
try:
|
||||
data = json.loads(response) or {}
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
data = json.loads(self.decrypt(response))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {"class": [], "filters": {}}
|
||||
filter_keys = {"class", "area", "lang", "year", "letter", "by", "sort"}
|
||||
filters = {}
|
||||
classes = []
|
||||
typelist = data.get('data', {}).get('typelist', [])
|
||||
for item in typelist:
|
||||
type_id = str(item["type_id"])
|
||||
classes.append({"type_name": item["type_name"], "type_id": type_id})
|
||||
extend = item.get("type_extend", {})
|
||||
type_filters = []
|
||||
for key, value_str in extend.items():
|
||||
if key not in filter_keys:
|
||||
continue
|
||||
stripped = value_str.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
values = [v.strip() for v in stripped.split(",") if v.strip()]
|
||||
if not values:
|
||||
continue
|
||||
type_filters.append({
|
||||
"key": key,
|
||||
"name": key,
|
||||
"value": [{"n": v, "v": v} for v in values]
|
||||
})
|
||||
if type_filters:
|
||||
filters[type_id] = type_filters
|
||||
return {"class": classes, "filters": filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
if not self.cms_host:
|
||||
return {'list': []}
|
||||
self.headers['app-time'] = str(int(time.time()))
|
||||
print(f'{self.cms_host}/api.php/v1.vod/HomeIndex?page=&limit=6')
|
||||
response = self.fetch(f'{self.cms_host}/api.php/v1.vod/HomeIndex?page=&limit=6', headers=self.headers).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
videos = []
|
||||
for i in data['data']:
|
||||
if i.get('vod_list'):
|
||||
vod_list = i['vod_list']
|
||||
for j in vod_list:
|
||||
pic = j.get('vod_pic')
|
||||
if pic:
|
||||
if not pic.startswith('http'):
|
||||
j['vod_pic'] = self.cms_host + pic
|
||||
videos.extend(vod_list)
|
||||
return {'list': videos}
|
||||
|
||||
def detailContent(self, ids):
|
||||
self.headers['app-time'] = str(int(time.time()))
|
||||
response = self.fetch(f'{self.cms_host}/api.php/v1.vod/detail?vod_id={ids[0]}', headers=self.headers).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
data = data['data']
|
||||
if data == '':
|
||||
return {'list': []}
|
||||
vod_play_url = ''
|
||||
show = ''
|
||||
for i,j in data['vod_play_list'].items():
|
||||
show += j['player_info']['show'] + '$$$'
|
||||
urls = j.get('urls',{})
|
||||
play_url = ''
|
||||
if isinstance(urls, dict):
|
||||
for i2,j2 in urls.items():
|
||||
play_url += f"{j2['name']}${j2['from']}@{j2['url']}#"
|
||||
play_url = play_url.rstrip('#')
|
||||
vod_play_url += play_url + '$$$'
|
||||
data['vod_play_from'] = show.rstrip('$$$')
|
||||
data['vod_play_url'] = vod_play_url.rstrip('$$$')
|
||||
data['vod_play_note'] = '$$$'
|
||||
data.pop('vod_play_list')
|
||||
data.pop('type')
|
||||
return {'list': [data]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
if not self.cms_host:
|
||||
return {'list': []}
|
||||
self.headers['app-time'] = str(int(time.time()))
|
||||
response = self.fetch(f'{self.cms_host}/api.php/v1.vod?wd={key}&limit=18&page={pg}', headers=self.headers).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
videos = data['data']['list']
|
||||
for item in data['data']['list']:
|
||||
item.pop('type', None)
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if not self.cms_host:
|
||||
return {'list': []}
|
||||
self.headers['app-time'] = str(int(time.time()))
|
||||
response = self.fetch(
|
||||
f"{self.cms_host}/api.php/v1.vod?type={tid}&class={extend.get('class', '')}&area={extend.get('area', '')}&year={extend.get('year', '')}&by=time&page={pg}&limit=18",
|
||||
headers=self.headers).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
videos = data['data']['list']
|
||||
for item in data['data']['list']:
|
||||
pic = item.get('vod_pic', '')
|
||||
if pic:
|
||||
if not pic.startswith('http'):
|
||||
item['vod_pic'] = self.cms_host + pic
|
||||
item.pop('type', None)
|
||||
print(videos)
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
play_from, raw_url = id.split('@')
|
||||
jx,url,playurl, = 1,raw_url,''
|
||||
try:
|
||||
if not self.playerinfo:
|
||||
res = self.fetch(f'{self.host}/api.php?action=playerinfo',headers=self.headers).text
|
||||
data = self.decrypt(res)
|
||||
playerinfo =json.loads(data).get('data',{}).get('playerinfo',[])
|
||||
if len(playerinfo) > 1:
|
||||
self.playerinfo = playerinfo
|
||||
if self.playerinfo:
|
||||
for i in self.playerinfo:
|
||||
play_jx = i.get('playerjiekou','')
|
||||
if i.get('playername') == play_from and play_jx.startswith('http'):
|
||||
response = self.fetch(f'{play_jx}{raw_url}&playerkey={play_from}',headers=self.headers,verify=False).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
if str(data.get('code','')) == '403':
|
||||
playurl = ''
|
||||
else:
|
||||
playurl = data['url']
|
||||
jx = 0
|
||||
except Exception:
|
||||
playurl = ''
|
||||
|
||||
if playurl.startswith('http'):
|
||||
url = playurl
|
||||
else:
|
||||
if re.search(r'^https?[^\s]*\.(m3u8|mp4|flv)', raw_url, re.I):
|
||||
url = raw_url
|
||||
jx = 0
|
||||
else:
|
||||
try:
|
||||
response = self.fetch(self.jx_api + raw_url,headers=self.headers,verify=False).text
|
||||
try:
|
||||
data = json.loads(response)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data_ = self.decrypt(response)
|
||||
data = json.loads(data_)
|
||||
playurl = data.get('url','')
|
||||
if playurl.startswith('http'):
|
||||
jx,url = 0,playurl
|
||||
else:
|
||||
jx,url = 1,raw_url
|
||||
except Exception as e:
|
||||
jx,url = 1,raw_url
|
||||
if url.startswith('NBY-'):
|
||||
jx,url = 0,''
|
||||
return {'jx': jx, 'parse': 0, 'url': url,'header': {'User-Agent': 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 MUOUAPP/10.8.4506.400'}}
|
||||
|
||||
def decrypt(self,data, key='', iv=''):
|
||||
if not(key or iv):
|
||||
key = self.data_key
|
||||
iv = self.data_iv
|
||||
key_bytes = key.encode('utf-8')
|
||||
iv_bytes = iv.encode('utf-8')
|
||||
encrypted_data = base64.b64decode(data)
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
|
||||
decrypted_padded = cipher.decrypt(encrypted_data)
|
||||
decrypted = unpad(decrypted_padded, AES.block_size)
|
||||
return decrypted.decode('utf-8')
|
||||
|
||||
def t(self, s, v, v1):
|
||||
if s is not None and s != '':
|
||||
n = len(s)
|
||||
if v < 0 or v1 < 0:
|
||||
raise ValueError("参数不能为负数")
|
||||
if v + v1 <= n:
|
||||
return s[v:n - v1]
|
||||
else:
|
||||
return ''
|
||||
return s
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
168
PY1/Appfox.py
Normal file
168
PY1/Appfox.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
|
||||
import re,sys,json,urllib3
|
||||
from base.spider import Spider
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
headers,host,froms,detail,custom_first,parses,custom_parses = {
|
||||
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36",
|
||||
'Connection': "Keep-Alive",
|
||||
'Accept-Encoding': "gzip",
|
||||
'Accept-Language': "zh-CN,zh;q=0.8",
|
||||
'Cache-Control': "no-cache"
|
||||
},'','','','',{},{}
|
||||
|
||||
def init(self, extend=''):
|
||||
ext = extend.strip()
|
||||
if ext.startswith('http'):
|
||||
host = ext
|
||||
else:
|
||||
arr = json.loads(ext)
|
||||
host = arr['host']
|
||||
self.froms = arr.get('from', '')
|
||||
self.custom_parses = arr.get('parse', {})
|
||||
self.custom_first = arr.get('custom_first',0)
|
||||
if not re.match(r'^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?(\/)?$', host):
|
||||
host = self.fetch(host, headers=self.headers, verify=False).json()['apiDomain']
|
||||
self.host = host.rstrip('/')
|
||||
|
||||
def homeContent(self, filter):
|
||||
if not self.host: return None
|
||||
response = self.fetch(f'{self.host}/api.php/Appfox/init', headers=self.headers, verify=False).json()
|
||||
classes = []
|
||||
for i in response['data']['type_list']:
|
||||
classes.append({'type_id': i['type_id'],'type_name': i['type_name']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
if not self.host: return None
|
||||
response = self.fetch(f'{self.host}/api.php/Appfox/index', headers=self.headers, verify=False).json()
|
||||
data = response['data']
|
||||
videos = []
|
||||
for i in data:
|
||||
for j in i.get('banner', []):
|
||||
videos.append(j)
|
||||
for k in i.get('categories', []):
|
||||
for l in k.get('videos',[]):
|
||||
videos.append(l)
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if not self.host: return None
|
||||
response = self.fetch(f"{self.host}/api.php/Appfox/vodList?type_id={tid}&class=全部&area=全部&lang=全部&year=全部&sort=最新&page={pg}", headers=self.headers, verify=False).json()
|
||||
videos = []
|
||||
for i in response['data']['recommend_list']:
|
||||
videos.append(i)
|
||||
return {'list': videos}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
if not self.host: return None
|
||||
path = f"{self.host}/api.php/Appfox/vod?ac=detail&wd={key}"
|
||||
if self.froms: path += '&from=' + self.froms
|
||||
response = self.fetch(path, headers=self.headers, verify=False, timeout=7).json()
|
||||
self.detail = response['list']
|
||||
return response
|
||||
|
||||
def detailContent(self, ids):
|
||||
video = next((i.copy() for i in self.detail if str(i['vod_id']) == str(ids[0])), None)
|
||||
if not video:
|
||||
detail_response = self.fetch(f"{self.host}/api.php/Appfox/vod?ac=detail&ids={ids[0]}",headers=self.headers,verify=False).json()
|
||||
video = detail_response.get('list')[0]
|
||||
if not video: return {'list': []}
|
||||
play_from = video['vod_play_from'].split('$$$')
|
||||
play_urls = video['vod_play_url'].split('$$$')
|
||||
try:
|
||||
config_response = self.fetch(f"{self.host}/api.php/Appfox/config",headers=self.headers,verify=False).json()
|
||||
player_list = config_response.get('data', {}).get('playerList', [])
|
||||
jiexi_data_list = config_response.get('data', {}).get('jiexiDataList', [])
|
||||
except Exception:
|
||||
return {'list': [video]}
|
||||
player_map = {player['playerCode']: player for player in player_list}
|
||||
processed_play_urls = []
|
||||
for idx, play_code in enumerate(play_from):
|
||||
if play_code in player_map:
|
||||
player_info = player_map[play_code]
|
||||
if player_info['playerCode'] != player_info['playerName']:
|
||||
play_from[idx] = f"{player_info['playerName']}\u2005({play_code})"
|
||||
if idx < len(play_urls):
|
||||
urls = play_urls[idx].split('#')
|
||||
processed_urls = []
|
||||
for url in urls:
|
||||
parts = url.split('$')
|
||||
if len(parts) >= 2:
|
||||
parts[1] = f"{play_code}@{parts[1]}"
|
||||
processed_urls.append('$'.join(parts))
|
||||
else:
|
||||
processed_urls.append(url)
|
||||
processed_play_urls.append('#'.join(processed_urls))
|
||||
video['vod_play_from'] = '$$$'.join(play_from)
|
||||
video['vod_play_url'] = '$$$'.join(processed_play_urls)
|
||||
self.parses = {p['playerCode']: p['url'] for p in jiexi_data_list if p.get('url', '').startswith('http')}
|
||||
return {'list': [video]}
|
||||
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
play_from, raw_url = id.split('@', 1)
|
||||
jx, parse, parsed = 0, 0, 0
|
||||
url = raw_url
|
||||
parses_main = []
|
||||
if self.custom_first == 1:
|
||||
parses_main.append(self.custom_parses)
|
||||
parses_main.append(self.parses)
|
||||
else:
|
||||
parses_main.append(self.parses)
|
||||
parses_main.append(self.custom_parses)
|
||||
print(parses_main)
|
||||
for parses2 in parses_main:
|
||||
if not parsed and not re.match(r'https?://.*\.(m3u8|mp4|flv|mkv)', url):
|
||||
for key, parsers in parses2.items():
|
||||
if play_from not in key:
|
||||
continue
|
||||
if isinstance(parsers, list):
|
||||
for parser in parsers:
|
||||
if parser.startswith('parse:'):
|
||||
url, jx, parse = parser.split('parse:')[1] + raw_url, 0, 1
|
||||
break
|
||||
try:
|
||||
response = self.fetch(f"{parser}{raw_url}", headers=self.headers, verify=False).json()
|
||||
if response.get('url', '').startswith('http'):
|
||||
url, parsed = response['url'], 1
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
if parsers.startswith('parse:'):
|
||||
url, jx, parse = parsers.split('parse:')[1] + raw_url, 0, 1
|
||||
break
|
||||
try:
|
||||
response = self.fetch(f"{parsers}{raw_url}", headers=self.headers, verify=False).json()
|
||||
if response.get('url', '').startswith('http'):
|
||||
url, parsed = response['url'], 1
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if parsed or parse:
|
||||
break
|
||||
if parsed or parse:
|
||||
break
|
||||
if not (re.match(r'https?:\/\/.*\.(m3u8|mp4|flv|mkv)', url) or parsed == 1):
|
||||
jx = 1
|
||||
return {'jx': jx, 'parse': parse, 'url': url, 'header': {'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'}}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
187
PY1/ApptoV5无加密.py
Normal file
187
PY1/ApptoV5无加密.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习及爬虫技术交流。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
"""
|
||||
{
|
||||
"key": "xxx",
|
||||
"name": "xxx",
|
||||
"type": 3,
|
||||
"api": "./ApptoV5无加密.py",
|
||||
"ext": "http://domain.com"
|
||||
}
|
||||
"""
|
||||
|
||||
import re,sys,uuid
|
||||
from base.spider import Spider
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
host,config,local_uuid,parsing_config = '','','',[]
|
||||
headers = {
|
||||
'User-Agent': "Dart/2.19 (dart:io)",
|
||||
'Accept-Encoding': "gzip",
|
||||
'appto-local-uuid': local_uuid
|
||||
}
|
||||
|
||||
def init(self, extend=''):
|
||||
try:
|
||||
host = extend.strip()
|
||||
if not host.startswith('http'):
|
||||
return {}
|
||||
if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host):
|
||||
host_=self.fetch(host).json()
|
||||
self.host = host_['domain']
|
||||
else:
|
||||
self.host = host
|
||||
self.local_uuid = str(uuid.uuid4())
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/config/get?p=android&__platform=android', headers=self.headers).json()
|
||||
config = response['data']
|
||||
self.config = config
|
||||
parsing_conf = config['get_parsing']['lists']
|
||||
parsing_config = {}
|
||||
for i in parsing_conf:
|
||||
if len(i['config']) != 0:
|
||||
label = []
|
||||
for j in i['config']:
|
||||
if j['type'] == 'json':
|
||||
label.append(j['label'])
|
||||
parsing_config.update({i['key']:label})
|
||||
self.parsing_config = parsing_config
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f'初始化异常:{e}')
|
||||
return {}
|
||||
|
||||
def detailContent(self, ids):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/getVod?id={ids[0]}",headers=self.headers).json()
|
||||
data3 = response['data']
|
||||
videos = []
|
||||
vod_play_url = ''
|
||||
vod_play_from = ''
|
||||
for i in data3['vod_play_list']:
|
||||
play_url = ''
|
||||
for j in i['urls']:
|
||||
play_url += f"{j['name']}${i['player_info']['from']}@{j['url']}#"
|
||||
vod_play_from += i['player_info']['show'] + '$$$'
|
||||
vod_play_url += play_url.rstrip('#') + '$$$'
|
||||
vod_play_url = vod_play_url.rstrip('$$$')
|
||||
vod_play_from = vod_play_from.rstrip('$$$')
|
||||
videos.append({
|
||||
'vod_id': data3.get('vod_id'),
|
||||
'vod_name': data3.get('vod_name'),
|
||||
'vod_content': data3.get('vod_content'),
|
||||
'vod_remarks': data3.get('vod_remarks'),
|
||||
'vod_director': data3.get('vod_director'),
|
||||
'vod_actor': data3.get('vod_actor'),
|
||||
'vod_year': data3.get('vod_year'),
|
||||
'vod_area': data3.get('vod_area'),
|
||||
'vod_play_from': vod_play_from,
|
||||
'vod_play_url': vod_play_url
|
||||
})
|
||||
return {'list': videos}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = f"{self.host}/apptov5/v1/search/lists?wd={key}&page={pg}&type=&__platform=android"
|
||||
response = self.fetch(url, headers=self.headers).json()
|
||||
data = response['data']['data']
|
||||
for i in data:
|
||||
if i.get('vod_pic').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data, 'page': pg, 'total': response['data']['total']}
|
||||
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
default_ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
parsing_config = self.parsing_config
|
||||
parts = id.split('@')
|
||||
if len(parts) != 2:
|
||||
return {'parse': 0, 'url': id, 'header': {'User-Agent': default_ua}}
|
||||
playfrom, rawurl = parts
|
||||
label_list = parsing_config.get(playfrom)
|
||||
if not label_list:
|
||||
return {'parse': 0, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
result = {'parse': 1, 'url': rawurl, 'header': {'User-Agent': default_ua}}
|
||||
for label in label_list:
|
||||
payload = {
|
||||
'play_url': rawurl,
|
||||
'label': label,
|
||||
'key': playfrom
|
||||
}
|
||||
try:
|
||||
response = self.post(
|
||||
f"{self.host}/apptov5/v1/parsing/proxy?__platform=android",
|
||||
data=payload,
|
||||
headers=self.headers
|
||||
).json()
|
||||
except Exception as e:
|
||||
print(f"请求异常: {e}")
|
||||
continue
|
||||
if not isinstance(response, dict):
|
||||
continue
|
||||
if response.get('code') == 422:
|
||||
continue
|
||||
data = response.get('data')
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
url = data.get('url')
|
||||
if not url:
|
||||
continue
|
||||
ua = data.get('UA') or data.get('UserAgent') or default_ua
|
||||
result = {
|
||||
'parse': 0,
|
||||
'url': url,
|
||||
'header': {'User-Agent': ua}
|
||||
}
|
||||
break
|
||||
return result
|
||||
|
||||
def homeContent(self, filter):
|
||||
config = self.config
|
||||
if not config:
|
||||
return {}
|
||||
home_cate = config['get_home_cate']
|
||||
classes = []
|
||||
for i in home_cate:
|
||||
if isinstance(i.get('extend', []),dict):
|
||||
classes.append({'type_id': i['cate'], 'type_name': i['title']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
response = self.fetch(f'{self.host}/apptov5/v1/home/data?id=1&mold=1&__platform=android',headers=self.headers).json()
|
||||
data = response['data']
|
||||
vod_list = []
|
||||
for i in data['sections']:
|
||||
for j in i['items']:
|
||||
vod_pic = j.get('vod_pic')
|
||||
if vod_pic.startswith('mac://'):
|
||||
vod_pic = vod_pic.replace('mac://', 'http://', 1)
|
||||
vod_list.append({
|
||||
"vod_id": j.get('vod_id'),
|
||||
"vod_name": j.get('vod_name'),
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": j.get('vod_remarks')
|
||||
})
|
||||
return {'list': vod_list}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
response = self.fetch(f"{self.host}/apptov5/v1/vod/lists?area={extend.get('area','')}&lang={extend.get('lang','')}&year={extend.get('year','')}&order={extend.get('sort','time')}&type_id={tid}&type_name=&page={pg}&pageSize=21&__platform=android", headers=self.headers).json()
|
||||
data = response['data']
|
||||
data2 = data['data']
|
||||
for i in data['data']:
|
||||
if i.get('vod_pic','').startswith('mac://'):
|
||||
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
|
||||
return {'list': data2, 'page': pg, 'total': data['total']}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
47
PY1/Chaturbate.py
Normal file
47
PY1/Chaturbate.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re, json, requests
|
||||
from base.spider import Spider
|
||||
class Spider(Spider):
|
||||
def getName(self): return "Chaturbate 直播"
|
||||
def init(self, extend=""):
|
||||
self.base, self.headers = "https://chaturbate.com", {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
||||
return self
|
||||
def homeContent(self, filter):
|
||||
return {"class": [{"type_id": "f", "type_name": "女性"}, {"type_id": "m", "type_name": "男性"}, {"type_id": "c", "type_name": "情侣"}]}
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if pg else 0
|
||||
gender = {"f": "f", "m": "m", "c": "c"}.get(tid, "")
|
||||
url = f"{self.base}/api/ts/roomlist/room-list/?enable_recommendations=false&genders={gender}&limit=90&offset={pg*90}"
|
||||
data = self.fetch(url, headers=self.headers).json()
|
||||
total = data.get("total_count", 0)
|
||||
videos = [
|
||||
{"vod_id": r["username"], "vod_pic": r.get("img", ""), "vod_remarks": "",
|
||||
"vod_name": f'{r["username"]}{" (" + str(r["display_age"]) + ")" if r.get("display_age") else ""}'}
|
||||
for r in data.get("rooms", [])
|
||||
]
|
||||
return {"list": videos, "page": pg + 1, "pagecount": (total + 89) // 90, "limit": 90, "total": total}
|
||||
def detailContent(self, ids):
|
||||
room_slug = ids[0]
|
||||
return {"list": [{"vod_id": room_slug, "vod_name": f"Chaturbate - {room_slug}", "vod_pic": "",
|
||||
"vod_play_from": "Chaturbate", "vod_play_url": f"直播源${room_slug}"}]}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url, data, headers = "https://chaturbate.com/get_edge_hls_url_ajax/", {"room_slug": id}, {"X-Requested-With": "XMLHttpRequest"}
|
||||
try:
|
||||
play_url = requests.post(url, data=data, headers=headers).json().get("url", id)
|
||||
except (requests.RequestException, json.JSONDecodeError):
|
||||
play_url = id
|
||||
return {"parse": 0, "playUrl": "", "url": play_url, "header": headers}
|
||||
|
||||
def searchContent(self, key, quick, pg="0"):
|
||||
pg = int(pg) if pg else 0
|
||||
url_api = f"{self.base}/api/ts/roomlist/room-list/?enable_recommendations=false&limit=90&offset={pg*90}&query={key}"
|
||||
data = self.fetch(url_api, headers=self.headers).json()
|
||||
videos = [
|
||||
{"vod_id": r["username"], "vod_pic": r.get("img", ""), "vod_remarks": "",
|
||||
"vod_name": f'{r["username"]}{" (" + str(r["display_age"]) + ")" if r.get("display_age") else ""}'}
|
||||
for r in data.get("rooms", [])
|
||||
]
|
||||
return {"list": videos}
|
||||
|
||||
def isVideoFormat(self, url): return ".m3u8" in url
|
||||
def manualVideoCheck(self): return True
|
||||
165
PY1/DSYS.py
Normal file
165
PY1/DSYS.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import time
|
||||
import uuid
|
||||
from base64 import b64decode, b64encode
|
||||
import json
|
||||
import sys
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Util.Padding import unpad, pad
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "https://api.230110.xyz"
|
||||
|
||||
phost = "https://cdn.230110.xyz"
|
||||
|
||||
headers = {
|
||||
'origin': host,
|
||||
'referer': f'{host}/',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data='9XSPkyFMrOOG34JSg//ZosMof45cyBo9hwZMZ5rvI6Yz/ZZlXWIf8/644OzwW+FNIOdJ61R/Lxjy1tqN+ZzokxtiVzb8LjYAkh6GFudwAUXFt9yS1ZjAxC3tDKrQsJQLk3nym0s00DBBzLBntRBDFz7nbba+OOBuQOZpL3CESGL42l4opdoViQLhO/dIizY1kIOk2NxxpDC9Z751gPl1ctHWuLWhuLG/QWgNWi/iHScjKrMHJKcC9GQHst/4Q3dgZ03eQIIVB6jvoV1XXoBCz6fjM/jM3BXpzSttT4Stglwy93gWuNWuZiKypHK2Q0lO10oM0ceRW2a0fPGId+rNYMRO3cR/C0ZueD4cmTAVOuxVr9ZZSP8/nhD0bHyAPONXtchIDJb0O/kdFHk2KTJfQ5q4fHOyzezczc4iQDV/R0S8cGZKM14MF+wytA/iljfj43H0UYqq5pM+MCUGRTdYEtuxCp0+A+DiOhNZwY/Km/TgBoGZQWGbpljJ2LAVnWhxX+ickLH7zuR/FeIwP/R8zOuR+8C8UlT9eHTqtvfNzaGdFxt316atHy8TNjRO7J5a177mqsHs3ziG0toDDzLDCbhRUjFgVA3ktahhXiWaaCo/ZGSJAA8TDO5DYqnJ0JDaX0ILPj8QB5zxrHYmRE8PboIr3RBAjz1sREbaHfjrUjoh29ePhlolLV00EvgoxP5knaqt5Ws/sq5IG57qKCAPgqXzblPLHToJGBtukKhLp8jbGJrkb6PVn4/jysks0NGE'
|
||||
return {'class':self.aes(data,False)}
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data = {"q": "", "filter": [f"type_id = {tid}"], "offset": (int(pg)-1) * 24, "limit": 24, "sort": ["video_time:desc"],"lang": "zh-cn", "route": "/videos/search"}
|
||||
result = {}
|
||||
if 'skey_' in tid:return self.searchContent(tid.split('_')[-1], True, pg)
|
||||
result['list'] = self.getl(self.getdata(data))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data={"limit":1,"filter":[f"video_id = {ids[0]}"],"lang":"zh-cn","route":"/videos/search"}
|
||||
res = self.getdata(data)[0]
|
||||
purl=urlunparse(urlparse(self.phost)._replace(path=urlparse(res.get('video_url')).path))
|
||||
vod = {
|
||||
'vod_play_from': 'dsysav',
|
||||
'vod_play_url': f"{res.get('video_duration')}${purl}"
|
||||
}
|
||||
if res.get('video_tag'):
|
||||
clist = []
|
||||
tags=res['video_tag'].split(',')
|
||||
for k in tags:
|
||||
clist.append('[a=cr:' + json.dumps({'id': f'skey_{k}', 'name': k}) + '/]' + k + '[/a]')
|
||||
vod['vod_content'] = ' '.join(clist)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data={"q":key,"filter":[],"offset":(int(pg)-1) * 24,"limit":24,"sort":["video_time:desc"],"lang":"zh-cn","route":"/videos/search"}
|
||||
return {'list':self.getl(self.getdata(data)),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if id.endswith('.mpd'):
|
||||
id=f"{self.getProxyUrl()}&url={self.e64(id)}&type=mpd"
|
||||
return {'parse': 0, 'url': id, 'header':self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
if param.get('type') and param['type']=='mpd':
|
||||
url = self.d64(param.get('url'))
|
||||
ids=url.split('/')
|
||||
id=f"{ids[-3]}/{ids[-2]}/"
|
||||
xpu = f"{self.getProxyUrl()}&path=".replace('&', '&')
|
||||
data = self.fetch(url, headers=self.headers).text
|
||||
data = data.replace('initialization="', f'initialization="{xpu}{id}').replace('media="',f'media="{xpu}{id}')
|
||||
return [200,'application/octet-stream',data]
|
||||
else:
|
||||
hsign=self.md5(f"AjPuom638LmWfWyeM5YueKuJ9PuWLdRn/mpd/{param.get('path')}1767196800")
|
||||
bytes_data = bytes.fromhex(hsign)
|
||||
sign = b64encode(bytes_data).decode('utf-8').replace('=','').replace('+','-').replace('/','_')
|
||||
url=f"{self.phost}/mpd/{param.get('path')}?sign={sign}&expire=1767196800"
|
||||
return [302,'text/plain',None,{'Location':url}]
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def aes(self, text, operation=True):
|
||||
key = b'OPQT123412FRANME'
|
||||
iv = b'MRDCQP12QPM13412'
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
if operation:
|
||||
ct_bytes = cipher.encrypt(pad(json.dumps(text).encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return json.loads(pt.decode("utf-8"))
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getl(self,data):
|
||||
videos = []
|
||||
for i in data:
|
||||
img = i.get('video_cover')
|
||||
if img and 'http' in img:img = urlunparse(urlparse(self.phost)._replace(path=urlparse(img).path))
|
||||
videos.append({
|
||||
'vod_id': i.get('video_id'),
|
||||
'vod_name': i.get('video_title'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i.get('video_duration'),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return videos
|
||||
|
||||
def getdata(self,data):
|
||||
uid = str(uuid.uuid4())
|
||||
t = int(time.time())
|
||||
json_data = {
|
||||
'sign': self.md5(f"{self.e64(json.dumps(data))}{uid}{t}AjPuom638LmWfWyeM5YueKuJ9PuWLdRn"),
|
||||
'nonce': uid,
|
||||
'timestamp': t,
|
||||
'data': self.aes(data),
|
||||
}
|
||||
res = self.post(f"{self.host}/v1", json=json_data, headers=self.headers).json()
|
||||
res = self.aes(res['data'], False)
|
||||
return res
|
||||
599
PY1/E佬通用视频 (3).py
Normal file
599
PY1/E佬通用视频 (3).py
Normal file
@@ -0,0 +1,599 @@
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import hashlib
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse, urljoin, urlencode, quote
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
# 图片缓存,避免重复解密
|
||||
img_cache = {}
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
def init(self, extend=""):
|
||||
"""初始化,支持 extend='{"host":"https://example.com", "proxies":{...}}'"""
|
||||
try:
|
||||
cfg = json.loads(extend) if isinstance(extend, str) else extend or {}
|
||||
self.proxies = cfg.get('proxies', {})
|
||||
self.host = (cfg.get('host', '') or '').strip()
|
||||
if not self.host:
|
||||
self.host = self.get_working_host()
|
||||
except:
|
||||
self.proxies = {}
|
||||
self.host = self.get_working_host()
|
||||
|
||||
self.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,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1'
|
||||
}
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
print(f"[Spider] 使用站点: {self.host}")
|
||||
|
||||
def getName(self):
|
||||
return "🌈 通用视频解析器|Pro增强版"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(ext in (url or '').lower() for ext in ['.m3u8', '.mp4', '.ts', '.flv', '.mkv', '.avi', '.webm'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
global img_cache
|
||||
img_cache.clear()
|
||||
|
||||
def get_working_host(self):
|
||||
"""尝试多个host,找到可用的"""
|
||||
dynamic_urls = [
|
||||
'https://wanwuu.com/'
|
||||
]
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=3)
|
||||
if response.status_code == 200:
|
||||
print(f"[Spider] 检测到可用host: {url}")
|
||||
return url.rstrip('/')
|
||||
except Exception as e:
|
||||
continue
|
||||
return dynamic_urls[0].rstrip('/') if dynamic_urls else 'https://jszyapi.com'
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""首页:动态分类 + 视频列表 + filters"""
|
||||
try:
|
||||
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code != 200:
|
||||
return {'class': [], 'filters': {}, 'list': []}
|
||||
|
||||
response.encoding = response.apparent_encoding
|
||||
data = self.getpq(response.text)
|
||||
|
||||
classes = []
|
||||
# 多选择器:导航链接
|
||||
nav_items = data('nav a, .menu a, .nav a, #header a, .header a, ul.navbar-nav a, .category-list a, .scroll-content a, .module-menu a, .module-tab-item')
|
||||
seen_hrefs = set()
|
||||
bad_words = ['登录', '注册', '搜索', '首页', 'Home', 'Login', 'Search', '联系', '关于', '留言', 'RSS', '推特', 'TG', 'Q群', '合作', '公告', 'APP', '下载']
|
||||
|
||||
for k in nav_items.items():
|
||||
href = (k.attr('href') or '').strip()
|
||||
name = k.text().strip()
|
||||
if not href or href in ['#', '/', ''] or 'javascript' in href:
|
||||
continue
|
||||
if not name or len(name) < 2 or len(name) > 12:
|
||||
continue
|
||||
if any(bw in name for bw in bad_words):
|
||||
continue
|
||||
if href in seen_hrefs:
|
||||
continue
|
||||
|
||||
# 规范化href
|
||||
if not href.startswith('http'):
|
||||
href = urljoin(self.host, href)
|
||||
|
||||
classes.append({'type_name': name, 'type_id': href})
|
||||
seen_hrefs.add(href)
|
||||
if len(classes) >= 25:
|
||||
break
|
||||
|
||||
if not classes:
|
||||
classes = [
|
||||
{'type_name': '最新', 'type_id': '/latest/'},
|
||||
{'type_name': '热门', 'type_id': '/hot/'},
|
||||
{'type_name': '推荐', 'type_id': '/recommend/'}
|
||||
]
|
||||
|
||||
# 视频列表
|
||||
videos = self.getlist(data, '#content article, #main article, .posts article, .container .row article, article, .video-list .video-item, .module-poster-item, .avdata-outer, .search-result, .card')
|
||||
|
||||
# filters
|
||||
filters = {
|
||||
'class': [{'n': '全部', 'v': ''}, {'n': '高清', 'v': 'HD'}, {'n': '4K', 'v': '4K'}],
|
||||
'area': [{'n': '全部', 'v': ''}, {'n': '日本', 'v': 'jp'}, {'n': '欧美', 'v': 'us'}, {'n': '国产', 'v': 'cn'}],
|
||||
'year': [{'n': '全部', 'v': ''}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}, {'n': '2022', 'v': '2022'}],
|
||||
'lang': [{'n': '全部', 'v': ''}, {'n': '中文', 'v': 'zh'}, {'n': '日语', 'v': 'jp'}, {'n': '英文', 'v': 'en'}]
|
||||
}
|
||||
|
||||
return {'class': classes, 'filters': filters, 'list': videos}
|
||||
except Exception as e:
|
||||
print(f"[homeContent] Error: {e}")
|
||||
return {'class': [], 'filters': {}, 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""首页视频内容 (复用homeContent)"""
|
||||
res = self.homeContent(None)
|
||||
return {'list': res.get('list', [])}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容"""
|
||||
try:
|
||||
if '@folder' in tid:
|
||||
v = self.getfod(tid.replace('@folder', ''))
|
||||
return {'list': v, 'page': 1, 'pagecount': 1, 'limit': 90, 'total': len(v)}
|
||||
|
||||
pg = int(pg) if pg else 1
|
||||
url = tid if tid.startswith('http') else f"{self.host}{tid if tid.startswith('/') else '/' + tid}"
|
||||
url = url.rstrip('/')
|
||||
|
||||
# 构造分页URL
|
||||
real_url = f"{url}/" if pg == 1 else f"{url}/{pg}/"
|
||||
if '?page=' in url or '?pg=' in url:
|
||||
real_url = url.replace('{pg}', str(pg))
|
||||
|
||||
# extend参数添加filter
|
||||
if isinstance(extend, dict):
|
||||
params = []
|
||||
for key in ['class', 'area', 'year', 'lang', 'letter', 'by']:
|
||||
if extend.get(key):
|
||||
params.append(f"{key}={quote(str(extend[key]))}")
|
||||
if params:
|
||||
sep = '&' if '?' in real_url else '?'
|
||||
real_url = real_url + sep + '&'.join(params)
|
||||
|
||||
print(f"[categoryContent] 请求URL: {real_url}")
|
||||
response = requests.get(real_url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code != 200:
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
|
||||
response.encoding = response.apparent_encoding
|
||||
data = self.getpq(response.text)
|
||||
|
||||
# 【关键修复】分类页面用更全面的选择器
|
||||
videos = self.getlist(
|
||||
data,
|
||||
'.module-item, .module-poster-item, .video-item, article, .card, li.vodlist_item, .stui-vodlist__box, a.module-item-pic, .myui-vodlist__box',
|
||||
tid
|
||||
)
|
||||
|
||||
print(f"[categoryContent] 提取到 {len(videos)} 个视频")
|
||||
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
except Exception as e:
|
||||
print(f"[categoryContent] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页:提取视频源(增强版)"""
|
||||
try:
|
||||
url = ids[0] if ids[0].startswith('http') else f"{self.host}{ids[0]}"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
response.encoding = response.apparent_encoding
|
||||
html_text = response.text
|
||||
data = self.getpq(html_text)
|
||||
|
||||
plist = []
|
||||
unique_urls = set()
|
||||
|
||||
def add_play_url(name, u):
|
||||
if not u or u in unique_urls:
|
||||
return
|
||||
if not u.startswith('http'):
|
||||
u = urljoin(self.host, u)
|
||||
unique_urls.add(u)
|
||||
plist.append(f"{name}${u}")
|
||||
|
||||
# 1. Script 中的 m3u8/mp4 (优先级最高)
|
||||
scripts = data('script')
|
||||
for s in scripts.items():
|
||||
txt = s.text()
|
||||
if 'url' in txt and ('.m3u8' in txt or '.mp4' in txt):
|
||||
urls = re.findall(r'["\']+(http[^"\']+\.(?:m3u8|mp4)[^\'"]*)["\']', txt)
|
||||
for u in urls:
|
||||
add_play_url("脚本源", u)
|
||||
break
|
||||
|
||||
# 2. DPlayer 配置
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
try:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
if video_url:
|
||||
add_play_url(f"DPlayer{c}", video_url)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. Video 标签 (HTML5)
|
||||
for v in data('video').items():
|
||||
src = v.attr('src')
|
||||
if src:
|
||||
add_play_url("HTML5视频", src)
|
||||
for src_tag in v('source').items():
|
||||
add_play_url("HTML5源", src_tag.attr('src'))
|
||||
|
||||
# 4. Iframe 嗅探
|
||||
for iframe in data('iframe').items():
|
||||
src = iframe.attr('src') or iframe.attr('data-src')
|
||||
if src and any(x in src for x in ['.m3u8', '.mp4', 'upload', 'cloud', 'player', 'embed']):
|
||||
if not any(x in src for x in ['google', 'facebook', 'disqus']):
|
||||
add_play_url("Iframe源", src)
|
||||
|
||||
# 5. 通用变量/JSON 正则 (核心增强)
|
||||
common_patterns = [
|
||||
r'var\s+main\s*=\s*["\']([^"\']+)["\']',
|
||||
r'url\s*:\s*["\']([^"\']+\.(?:m3u8|mp4))["\']',
|
||||
r'vurl\s*=\s*["\']([^"\']+)["\']',
|
||||
r'vid\s*:\s*["\']([^"\']+\.(?:m3u8|mp4))["\']',
|
||||
r'"url"\s*:\s*"([^"]+)"',
|
||||
r'video_url\s*=\s*[\'"]([^\'"]+)[\'"]',
|
||||
r'var\s+videoUrl\s*=\s*["\']([^"\']+)["\']',
|
||||
r'playurl\s*=\s*["\']([^"\']+)["\']',
|
||||
r'"playUrl"\s*:\s*"([^"]+)"',
|
||||
r'src="([^"]*\.(?:m3u8|mp4)[^"]*)"',
|
||||
r'data-src="([^"]*\.(?:m3u8|mp4)[^"]*)"',
|
||||
r'mp4Url\s*=\s*["\']([^"\']+)["\']',
|
||||
r'm3u8Url\s*=\s*["\']([^"\']+)["\']',
|
||||
]
|
||||
for pat in common_patterns:
|
||||
matches = re.finditer(pat, html_text, re.IGNORECASE)
|
||||
for match in matches:
|
||||
u = match.group(1)
|
||||
if any(ext in u for ext in ['.m3u8', '.mp4', '.flv', '.m4v']):
|
||||
add_play_url("正则源", u)
|
||||
|
||||
# 6. Script JSON embed (ThePorn/porn87风格)
|
||||
try:
|
||||
json_matches = re.findall(r'<script[^>]*type="text/javascript"[^>]*>(.*?)</script>', html_text, re.DOTALL)
|
||||
for json_str in json_matches:
|
||||
try:
|
||||
obj = json.loads(json_str)
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str) and ('.m3u8' in v or '.mp4' in v):
|
||||
add_play_url(f"JSON-{k}", v)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# 7. 兜底:文本链接
|
||||
if not plist:
|
||||
content_area = data('.post-content, article, .content, .video-info, .module-info-introduction')
|
||||
for i, link in enumerate(content_area('a').items(), start=1):
|
||||
link_text = link.text().strip()
|
||||
link_href = link.attr('href')
|
||||
if link_href and any(kw in link_text for kw in ['点击观看', '观看', '播放', '视频', '第一弹', '线路', 'Play', '播放器']):
|
||||
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
|
||||
if not ep_name:
|
||||
ep_name = f"线路{i}"
|
||||
add_play_url(ep_name, link_href)
|
||||
|
||||
play_url = '#'.join(plist) if plist else f"无视频源,请尝试网页播放${url}"
|
||||
|
||||
# 标题
|
||||
vod_title = data('h1').text().strip()
|
||||
if not vod_title:
|
||||
vod_title = data('.post-title, .module-info-heading, .video-title').text().strip()
|
||||
if not vod_title:
|
||||
vod_title = data('title').text().split('|')[0].strip()
|
||||
|
||||
# 描述
|
||||
vod_content = data('.post-content, article, .module-info-introduction-content, .video-desc').text().strip()
|
||||
|
||||
return {'list': [{'vod_play_from': '通用解析', 'vod_play_url': play_url, 'vod_content': vod_content or vod_title}]}
|
||||
except Exception as e:
|
||||
print(f"[detailContent] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'list': [{'vod_play_from': '通用解析', 'vod_play_url': '获取失败'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索"""
|
||||
try:
|
||||
pg = int(pg) if pg else 1
|
||||
url = f"{self.host}/?s={quote(key)}"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
response.encoding = response.apparent_encoding
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data, 'article, .search-result, .post, .video-item, .module-poster-item, .avdata-outer, .card, .module-item')
|
||||
return {'list': videos, 'page': pg, 'pagecount': 9999}
|
||||
except Exception as e:
|
||||
print(f"[searchContent] Error: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放器"""
|
||||
if 'html' in id or 'php' in id or 'embed' in id or 'player' in id:
|
||||
parse = 1 # 需要解析
|
||||
elif self.isVideoFormat(id):
|
||||
parse = 0 # 直接播放
|
||||
else:
|
||||
parse = 1
|
||||
|
||||
url = self.proxy(id) if '.m3u8' in id else id
|
||||
return {'parse': parse, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
"""本地代理:处理m3u8/ts/图片解密"""
|
||||
try:
|
||||
type_ = param.get('type')
|
||||
url = param.get('url')
|
||||
|
||||
if type_ == 'cache':
|
||||
key = param.get('key')
|
||||
if content := img_cache.get(key):
|
||||
return [200, 'image/jpeg', content]
|
||||
return [404, 'text/plain', b'Expired']
|
||||
|
||||
elif type_ == 'img':
|
||||
real_url = self.d64(url) if not url.startswith('http') else url
|
||||
res = requests.get(real_url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
content = self.aesimg(res.content)
|
||||
return [200, 'image/jpeg', content]
|
||||
|
||||
elif type_ == 'm3u8':
|
||||
return self.m3Proxy(url)
|
||||
|
||||
else: # ts
|
||||
return self.tsProxy(url)
|
||||
except Exception as e:
|
||||
print(f"[localProxy] Error: {e}")
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
"""生成代理URL"""
|
||||
if data and self.proxies:
|
||||
return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
"""m3u8代理"""
|
||||
try:
|
||||
url = self.d64(url)
|
||||
res = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
res.encoding = res.apparent_encoding
|
||||
data = res.text
|
||||
base = res.url.rsplit('/', 1)[0]
|
||||
lines = []
|
||||
|
||||
for line in data.split('\n'):
|
||||
if '#EXT' not in line and line.strip():
|
||||
if not line.startswith('http'):
|
||||
if line.startswith('/'):
|
||||
host_base = '/'.join(res.url.split('/')[:3])
|
||||
line = f"{host_base}{line}"
|
||||
else:
|
||||
line = f"{base}/{line}"
|
||||
lines.append(self.proxy(line, 'ts'))
|
||||
else:
|
||||
lines.append(line)
|
||||
|
||||
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
|
||||
except Exception as e:
|
||||
print(f"[m3Proxy] Error: {e}")
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def tsProxy(self, url):
|
||||
"""ts代理"""
|
||||
try:
|
||||
content = requests.get(self.d64(url), headers=self.headers, proxies=self.proxies, timeout=10).content
|
||||
return [200, 'video/mp2t', content]
|
||||
except:
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def e64(self, text):
|
||||
"""base64编码"""
|
||||
return b64encode(str(text).encode()).decode()
|
||||
|
||||
def d64(self, text):
|
||||
"""base64解码"""
|
||||
return b64decode(str(text).encode()).decode()
|
||||
|
||||
def aesimg(self, data):
|
||||
"""AES解密图片"""
|
||||
if len(data) < 16:
|
||||
return data
|
||||
|
||||
# 多密钥尝试 (从成品提取)
|
||||
keys = [
|
||||
(b'f5d965df75336270', b'97b60394abc2fbe1'),
|
||||
(b'75336270f5d965df', b'abc2fbe197b60394'),
|
||||
]
|
||||
|
||||
for k, v in keys:
|
||||
try:
|
||||
dec = unpad(AES.new(k, AES.MODE_CBC, v).decrypt(data), 16)
|
||||
if dec.startswith(b'\xff\xd8') or dec.startswith(b'\x89PNG') or dec.startswith(b'GIF8'):
|
||||
return dec
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
dec = unpad(AES.new(k, AES.MODE_ECB).decrypt(data), 16)
|
||||
if dec.startswith(b'\xff\xd8'):
|
||||
return dec
|
||||
except:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
def getlist(self, data_pq, selector, tid=''):
|
||||
"""解析视频列表"""
|
||||
videos = []
|
||||
is_folder = '/mrdg' in (tid or '')
|
||||
|
||||
items = data_pq(selector)
|
||||
if len(items) == 0:
|
||||
items = data_pq('a:has(img)')
|
||||
|
||||
print(f"[getlist] 找到 {len(items)} 个候选项")
|
||||
|
||||
seen_ids = set()
|
||||
ad_keywords = ['娱乐', '棋牌', '澳门', '葡京', '太阳城', '彩票', 'AV', '直播', '充值', '下载', '回家']
|
||||
|
||||
for k in items.items():
|
||||
# 【关键修复】更灵活的链接提取
|
||||
if k.is_('a'):
|
||||
a = k
|
||||
container = k.parent()
|
||||
else:
|
||||
# 优先查找带href的a标签
|
||||
a = k('a[href]').eq(0)
|
||||
if not a or not a.attr('href'):
|
||||
a = k('a').eq(0)
|
||||
container = k
|
||||
|
||||
href = a.attr('href')
|
||||
if not href:
|
||||
continue
|
||||
|
||||
if any(x in href for x in ['/category/', '/tag/', '/feed/', '/page/', '/author/', 'gitlub']):
|
||||
continue
|
||||
if href in ['/', '#']:
|
||||
continue
|
||||
|
||||
# 【优化】标题提取
|
||||
title = container.find('h2, h3, h4, .title, .video-title, .module-poster-item-title, .module-item-title').text()
|
||||
if not title:
|
||||
title = a.attr('title') or a.attr('data-title')
|
||||
if not title:
|
||||
title = a.find('img').attr('alt')
|
||||
if not title:
|
||||
title = container.find('.video-name, .vodlist_title').text()
|
||||
if not title:
|
||||
title = a.text()
|
||||
|
||||
if not title or len(title.strip()) < 2:
|
||||
continue
|
||||
if any(ad in title for ad in ad_keywords):
|
||||
continue
|
||||
|
||||
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
|
||||
script_text = k('script').text()
|
||||
img = self.getimg(script_text, k, card_html)
|
||||
|
||||
if not img:
|
||||
continue
|
||||
if '.gif' in img.lower():
|
||||
continue
|
||||
|
||||
if href in seen_ids:
|
||||
continue
|
||||
|
||||
if not href.startswith('http'):
|
||||
href = urljoin(self.host, href)
|
||||
|
||||
seen_ids.add(href)
|
||||
remark = container.find('time, .date, .meta, .views, .video-duration, .module-item-note, .pic-text').text() or ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': f"{href}{'@folder' if is_folder else ''}",
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': remark,
|
||||
'vod_tag': 'folder' if is_folder else '',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
|
||||
return videos
|
||||
|
||||
def getimg(self, text, elem=None, html_content=None):
|
||||
"""提取图片URL"""
|
||||
# 1. var img_url (吃瓜网特色)
|
||||
if m := re.search(r'var\s+img_url\s*=\s*[\'"]([^\'"]+)[\'"]', text or ''):
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
# 2. loadBannerDirect
|
||||
if m := re.search(r"loadBannerDirect\('([^']+)'", text or ''):
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
if html_content is None and elem is not None:
|
||||
html_content = elem.outer_html() if hasattr(elem, 'outer_html') else str(elem)
|
||||
if not html_content:
|
||||
return ''
|
||||
|
||||
html_content = html_content.replace('"', '"').replace(''', "'").replace('&', '&')
|
||||
|
||||
# 3. data-src / data-original
|
||||
if m := re.search(r'data-src\s*=\s*["\']([^"\']+)["\']', html_content, re.I):
|
||||
return self._proc_url(m.group(1))
|
||||
if m := re.search(r'data-original\s*=\s*["\']([^"\']+)["\']', html_content, re.I):
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
# 4. http链接
|
||||
if m := re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp|gif))', html_content, re.I):
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
# 5. url()
|
||||
if 'url(' in html_content:
|
||||
m = re.search(r'url\s*\(\s*[\'"]?([^\"\'\)]+)[\'"]?\s*\)', html_content, re.I)
|
||||
if m:
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
# 6. src属性
|
||||
if m := re.search(r'src\s*=\s*["\']([^"\']+\.(?:jpg|png|jpeg|webp|gif))["\']', html_content, re.I):
|
||||
return self._proc_url(m.group(1))
|
||||
|
||||
return ''
|
||||
|
||||
def _proc_url(self, url):
|
||||
"""处理URL:转义、代理、AES"""
|
||||
if not url:
|
||||
return ''
|
||||
url = url.strip('\'" ')
|
||||
|
||||
if url.startswith('data:'):
|
||||
try:
|
||||
_, b64_str = url.split(',', 1)
|
||||
raw = b64decode(b64_str)
|
||||
if not (raw.startswith(b'\xff\xd8') or raw.startswith(b'\x89PNG') or raw.startswith(b'GIF8')):
|
||||
raw = self.aesimg(raw)
|
||||
key = hashlib.md5(raw).hexdigest()
|
||||
img_cache[key] = raw
|
||||
return f"{self.getProxyUrl()}&type=cache&key={key}"
|
||||
except:
|
||||
return ""
|
||||
|
||||
if not url.startswith('http'):
|
||||
url = urljoin(self.host, url)
|
||||
|
||||
# 所有图片走代理解密
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
|
||||
|
||||
def getfod(self, id):
|
||||
"""文件夹处理"""
|
||||
return []
|
||||
|
||||
def getpq(self, data):
|
||||
"""获取pq对象"""
|
||||
try:
|
||||
return pq(data)
|
||||
except:
|
||||
return pq(data.encode('utf-8'))
|
||||
298
PY1/E佬通用视频.py
Normal file
298
PY1/E佬通用视频.py
Normal file
@@ -0,0 +1,298 @@
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "美人吹箫"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = []
|
||||
try:
|
||||
rsp = self.fetch("https://www.mrcx2.yachts/cn/home/web/")
|
||||
if rsp and rsp.text:
|
||||
doc = pq(rsp.text)
|
||||
|
||||
|
||||
items = doc('.stui-header__menu li a')
|
||||
for item in items.items():
|
||||
name = item.text()
|
||||
href = item.attr('href')
|
||||
if name and href and name != "首页":
|
||||
|
||||
match = re.search(r'/type/id/(\d+)\.html', href)
|
||||
if match:
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': match.group(1)
|
||||
})
|
||||
|
||||
|
||||
if not classes:
|
||||
items = doc('.dropdown.type li a')
|
||||
for item in items.items():
|
||||
name = item.text()
|
||||
href = item.attr('href')
|
||||
if name and href and name != "首页":
|
||||
match = re.search(r'/type/id/(\d+)\.html', href)
|
||||
if match:
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': match.group(1)
|
||||
})
|
||||
|
||||
|
||||
seen = set()
|
||||
unique_classes = []
|
||||
for cls in classes:
|
||||
if cls['type_id'] not in seen:
|
||||
seen.add(cls['type_id'])
|
||||
unique_classes.append(cls)
|
||||
classes = unique_classes
|
||||
|
||||
except Exception as e:
|
||||
print(f"homeContent error: {e}")
|
||||
|
||||
result['class'] = classes
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {}
|
||||
try:
|
||||
videos = []
|
||||
url = "https://www.mrcx2.yachts/cn/home/web/"
|
||||
rsp = self.fetch(url)
|
||||
if rsp and rsp.text:
|
||||
doc = pq(rsp.text)
|
||||
items = doc('.stui-vodlist li.index')
|
||||
for item in items.items():
|
||||
a = item.find('.stui-vodlist__thumb')
|
||||
href = a.attr('href')
|
||||
title = a.attr('title') or item.find('.title a').attr('title')
|
||||
img = a.attr('data-original') or a.attr('style')
|
||||
|
||||
|
||||
if img and 'background-image:' in img:
|
||||
match = re.search(r'url\(["\']?(.*?)["\']?\)', img)
|
||||
if match:
|
||||
img = match.group(1)
|
||||
|
||||
if not title or not href:
|
||||
continue
|
||||
|
||||
|
||||
play_count = item.find('.text').text() or '播放0次'
|
||||
score = item.find('.score').text() or '0.0 分'
|
||||
|
||||
videos.append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': f"{score} | {play_count}"
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
except Exception as e:
|
||||
print(f"homeVideoContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
videos = []
|
||||
try:
|
||||
url = f"https://www.mrcx2.yachts/cn/home/web/index.php/vod/type/id/{tid}/page/{pg}.html"
|
||||
rsp = self.fetch(url)
|
||||
if rsp and rsp.text:
|
||||
doc = pq(rsp.text)
|
||||
items = doc('.stui-vodlist li')
|
||||
for item in items.items():
|
||||
a = item.find('.stui-vodlist__thumb')
|
||||
href = a.attr('href')
|
||||
title = a.attr('title') or item.find('.title a').attr('title')
|
||||
img = a.attr('data-original') or a.attr('style')
|
||||
|
||||
|
||||
if img and 'background-image:' in img:
|
||||
match = re.search(r'url\(["\']?(.*?)["\']?\)', img)
|
||||
if match:
|
||||
img = match.group(1)
|
||||
|
||||
if not title or not href:
|
||||
continue
|
||||
|
||||
|
||||
play_count = item.find('.text').text() or '播放0次'
|
||||
score = item.find('.score').text() or '0.0 分'
|
||||
|
||||
videos.append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': f"{score} | {play_count}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"categoryContent error: {e}")
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, array):
|
||||
result = {}
|
||||
if not array or not array[0]:
|
||||
return result
|
||||
|
||||
try:
|
||||
aid = array[0]
|
||||
|
||||
if aid.startswith('/'):
|
||||
play_url = "https://www.mrcx2.yachts" + aid
|
||||
else:
|
||||
play_url = "https://www.mrcx2.yachts/cn/home/web/" + aid
|
||||
|
||||
rsp = self.fetch(play_url)
|
||||
if not rsp or not rsp.text:
|
||||
return result
|
||||
|
||||
html = rsp.text
|
||||
doc = pq(html)
|
||||
|
||||
|
||||
vod = {
|
||||
'vod_id': aid,
|
||||
'vod_name': doc('title').text() or '',
|
||||
'vod_pic': '',
|
||||
'vod_remarks': '',
|
||||
'vod_content': '',
|
||||
'vod_play_from': 'E佬通用视频',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
|
||||
img = doc('.stui-vodlist__thumb').attr('data-original') or doc('.stui-vodlist__thumb').attr('style')
|
||||
if img and 'background-image:' in img:
|
||||
match = re.search(r'url\(["\']?(.*?)["\']?\)', img)
|
||||
if match:
|
||||
vod['vod_pic'] = match.group(1)
|
||||
|
||||
|
||||
if not vod['vod_pic']:
|
||||
img = doc('img').filter(lambda i, this: pq(this).attr('src') and 'cover' in pq(this).attr('src').lower()).attr('src')
|
||||
if img:
|
||||
vod['vod_pic'] = img
|
||||
|
||||
|
||||
description = doc('.stui-vodlist__detail .text').text()
|
||||
if description:
|
||||
vod['vod_remarks'] = description
|
||||
|
||||
|
||||
content = doc('.content').text() or doc('.detail').text() or doc('.stui-content__detail').text()
|
||||
if content:
|
||||
vod['vod_content'] = content
|
||||
|
||||
|
||||
|
||||
|
||||
vod['vod_play_url'] = f'正片${play_url}'
|
||||
|
||||
result['list'] = [vod]
|
||||
except Exception as e:
|
||||
print(f"detailContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
result = {}
|
||||
videos = []
|
||||
try:
|
||||
if not key:
|
||||
return result
|
||||
|
||||
url = f"https://www.mrcx2.yachts/cn/home/web/index.php/vod/search/wd/{urllib.parse.quote(key)}/page/{page}.html"
|
||||
rsp = self.fetch(url)
|
||||
if rsp and rsp.text:
|
||||
doc = pq(rsp.text)
|
||||
items = doc('.stui-vodlist li')
|
||||
for item in items.items():
|
||||
a = item.find('.stui-vodlist__thumb')
|
||||
href = a.attr('href')
|
||||
title = a.attr('title') or item.find('.title a').attr('title')
|
||||
img = a.attr('data-original') or a.attr('style')
|
||||
|
||||
|
||||
if img and 'background-image:' in img:
|
||||
match = re.search(r'url\(["\']?(.*?)["\']?\)', img)
|
||||
if match:
|
||||
img = match.group(1)
|
||||
|
||||
if not title or not href:
|
||||
continue
|
||||
|
||||
|
||||
play_count = item.find('.text').text() or '播放0次'
|
||||
score = item.find('.score').text() or '0.0 分'
|
||||
|
||||
videos.append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': f"{score} | {play_count}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"searchContent error: {e}")
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
result = {}
|
||||
try:
|
||||
if not id:
|
||||
return result
|
||||
|
||||
|
||||
if id.startswith('http'):
|
||||
play_url = id
|
||||
else:
|
||||
|
||||
if id.startswith('/'):
|
||||
play_url = "https://www.mrcx2.yachts" + id
|
||||
else:
|
||||
play_url = "https://www.mrcx2.yachts/cn/home/web/" + id
|
||||
|
||||
|
||||
result["parse"] = 1
|
||||
result["playUrl"] = ''
|
||||
result["url"] = play_url
|
||||
result["header"] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Referer': 'https://www.mrcx2.yachts/'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"playerContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def localProxy(self, param):
|
||||
return {}
|
||||
102
PY1/FeiApp.py
Normal file
102
PY1/FeiApp.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
|
||||
import re,sys,urllib3
|
||||
from base.spider import Spider
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
headers,host = {
|
||||
'User-Agent': "okhttp/4.9.3",
|
||||
'Accept-Encoding': "gzip"
|
||||
},''
|
||||
|
||||
def init(self, extend=''):
|
||||
host = extend.strip()
|
||||
if host.startswith('http'):
|
||||
self.host = host.rstrip('/')
|
||||
|
||||
def homeContent(self, filter):
|
||||
response = self.fetch(f'{self.host}/api.php?type=getsort', headers=self.headers,verify=False).json()
|
||||
classes = []
|
||||
for i in response['list']:
|
||||
classes.append({'type_id':i['type_id'],'type_name':i['type_name']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
response = self.fetch(f'{self.host}/api.php?type=getHome', headers=self.headers,verify=False).json()
|
||||
videos = []
|
||||
for i, j in response.items():
|
||||
if not isinstance(j, dict):
|
||||
continue
|
||||
lis = j.get('list')
|
||||
if isinstance(lis, list):
|
||||
videos.extend(lis)
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
response = self.fetch(f"{self.host}/api.php?type=getvod&type_id={tid}&page={pg}&tag={extend.get('class','全部')}&year={extend.get('year','全部')}", headers=self.headers,verify=False).json()
|
||||
return {'list': response['list'], 'page': pg}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
response = self.fetch(f'{self.host}/api.php?type=getsearch&text={key}', headers=self.headers,verify=False).json()
|
||||
for i in response['list']:
|
||||
if not i.get('vod_content'):
|
||||
i['vod_content'] = i['vod_blurb']
|
||||
return {'list': response['list'], 'page': pg}
|
||||
|
||||
def detailContent(self, ids):
|
||||
response = self.fetch(f'{self.host}/api.php?type=getVodinfo&id={ids[0]}', headers=self.headers,verify=False).json()
|
||||
show = ''
|
||||
vod_play_url = ''
|
||||
for i in response['vod_player']['list']:
|
||||
show += i.get('from','') + '$$$'
|
||||
play_url = i.get('url','')
|
||||
vod_play_url += '#'.join(f"{item}@{ids[0]}" for item in play_url.split('#')) + '$$$'
|
||||
videos = [{
|
||||
'vod_name': response.get('vod_name'),
|
||||
'vod_pic': response.get('vod_pic'),
|
||||
'vod_id': response.get('vod_id'),
|
||||
'vod_class': response.get('vod_class'),
|
||||
'vod_actor': response.get('vod_actor'),
|
||||
'vod_blurb': response.get('vod_blurb'),
|
||||
'vod_content': response.get('vod_blurb'),
|
||||
'vod_remarks': response.get('vod_remarks'),
|
||||
'vod_lang': response.get('vod_lang'),
|
||||
'vod_play_from': show.rstrip('$$$'),
|
||||
'vod_play_url': vod_play_url.rstrip('$$$')
|
||||
}]
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
jx, ua = 0, 'Dalvik/2.1.0 (Linux; U; Android 14; Xiaomi 15 Build/SQ3A.220705.004)'
|
||||
ua2 = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
|
||||
vodurl, vodid = id.split('@')
|
||||
if re.match(r'https?:\/\/.*\.(m3u8|mp4|flv)', vodurl):
|
||||
url = vodurl
|
||||
else:
|
||||
try:
|
||||
response = self.fetch(f'{self.host}/api.php?type=jx&vodurl={vodurl}&vodid={vodid}', headers=self.headers,verify=False).json()
|
||||
url = response['url']
|
||||
if not url.startswith('http') or url == vodurl:
|
||||
jx, url, ua = 1, vodurl, ua2
|
||||
except Exception:
|
||||
jx, url, ua = 1, vodurl, ua2
|
||||
return {'jx': jx, 'parse': 0, 'url': url,'header': {'User-Agent': ua}}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
251
PY1/JAV目录.py
Normal file
251
PY1/JAV目录.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
from base64 import b64decode, b64encode
|
||||
from requests import Session
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
self.headers['referer'] = f'{self.host}/'
|
||||
self.session = Session()
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def getName(self):
|
||||
return "JAV目录大全"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "https://javmenu.com"
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
'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',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-full-version': '"133.0.6943.98"',
|
||||
'sec-ch-ua-arch': '"x86"',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua-platform-version': '"19.0.0"',
|
||||
'sec-ch-ua-model': '""',
|
||||
'sec-ch-ua-full-version-list': '"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="133.0.6943.98", "Chromium";v="133.0.6943.98"',
|
||||
'dnt': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=0, i'
|
||||
}
|
||||
|
||||
# -------------------- 业务接口 --------------------
|
||||
def homeContent(self, filter):
|
||||
cateManual = {
|
||||
"FC2在线": "/zh/fc2/online",
|
||||
"成人动画": "/zh/hanime/online",
|
||||
"国产在线": "/zh/chinese/online",
|
||||
"有码在线": "/zh/censored/online",
|
||||
"无码在线": "/zh/uncensored/online",
|
||||
"欧美在线": "/zh/western/online"
|
||||
}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq("/zh")
|
||||
return {'list': self.getlist(data(".video-list-item"))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
url = f"{self.host}{tid}" if pg == '1' else f"{self.host}{tid}?page={pg}"
|
||||
data = self.getpq(url)
|
||||
return {
|
||||
'list': self.getlist(data(".video-list-item")),
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vod_id = ids[0]
|
||||
if not vod_id.startswith('http'):
|
||||
url = f"{self.host}{vod_id}"
|
||||
else:
|
||||
url = vod_id
|
||||
vod_id = vod_id.replace(self.host, '')
|
||||
data = self.getpq(url)
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': data('h1').text() or data('title').text().split(' - ')[0],
|
||||
'vod_pic': self.getCover(data),
|
||||
'vod_content': data('.card-text').text() or '',
|
||||
'vod_director': '',
|
||||
'vod_actor': self.getActors(data),
|
||||
'vod_area': '日本',
|
||||
'vod_year': self.getYear(data('.text-muted').text()),
|
||||
'vod_remarks': self.getRemarks(data),
|
||||
'vod_play_from': 'JAV在线',
|
||||
'vod_play_url': self.getPlaylist(data, url)
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f"{self.host}/zh/search?wd={key}&page={pg}"
|
||||
data = self.getpq(url)
|
||||
return {'list': self.getlist(data(".video-list-item"))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'parse': 0, 'url': self.d64(id), 'header': self.headers}
|
||||
|
||||
# -------------------- 私有工具 --------------------
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for item in data.items():
|
||||
link = item('a').attr('href')
|
||||
if not link or '/zh/' not in link:
|
||||
continue
|
||||
link = link.replace(self.host, '') if link.startswith(self.host) else link
|
||||
name = item('.card-title').text() or item('img').attr('alt') or ''
|
||||
if not name:
|
||||
continue
|
||||
vlist.append({
|
||||
'vod_id': link,
|
||||
'vod_name': name.split(' - ')[0].strip(),
|
||||
'vod_pic': self.getListPicture(item),
|
||||
'vod_remarks': (item('.text-muted').text() or '').strip(),
|
||||
'style': {'ratio': 1.5, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
# ******** 修复版本:支持LazyLoad和正确过滤 ********
|
||||
def getListPicture(self, item):
|
||||
"""
|
||||
获取列表中的图片
|
||||
支持LazyLoad延迟加载机制
|
||||
过滤水印、占位符和无预览图
|
||||
"""
|
||||
# 获取所有img标签
|
||||
imgs = item('img')
|
||||
|
||||
for img in imgs.items():
|
||||
# 优先级:先从data-src获取(LazyLoad属性),再从src获取
|
||||
pic = img.attr('data-src') or img.attr('src')
|
||||
|
||||
# 过滤条件:排除水印、占位符、加载图片
|
||||
if pic and not any(keyword in pic for keyword in ['button_logo', 'no_preview', 'loading.gif', 'loading.png']):
|
||||
return pic
|
||||
|
||||
return ''
|
||||
|
||||
def getCover(self, data):
|
||||
"""
|
||||
获取详情页的图片
|
||||
支持LazyLoad延迟加载机制
|
||||
过滤水印、占位符和无预览图
|
||||
"""
|
||||
# 获取所有img标签
|
||||
imgs = data('img')
|
||||
|
||||
for img in imgs.items():
|
||||
# 优先级:先从data-src获取(LazyLoad属性),再从src获取
|
||||
pic = img.attr('data-src') or img.attr('src')
|
||||
|
||||
# 过滤条件:排除水印、占位符、加载图片
|
||||
if pic and not any(keyword in pic for keyword in ['button_logo', 'no_preview', 'loading.gif', 'loading.png', 'website_building']):
|
||||
return pic
|
||||
|
||||
return ''
|
||||
|
||||
# **********************************
|
||||
def getActors(self, data):
|
||||
"""获取演员信息"""
|
||||
actors = []
|
||||
h1_text = data('h1').text()
|
||||
if h1_text:
|
||||
actors.extend(h1_text.strip().split()[1:])
|
||||
actor_links = data('a[href*="/actor/"]')
|
||||
for actor_link in actor_links.items():
|
||||
actor_text = actor_link.text()
|
||||
if actor_text and actor_text not in actors:
|
||||
actors.append(actor_text)
|
||||
return ','.join(actors) if actors else '未知'
|
||||
|
||||
def getYear(self, date_str):
|
||||
"""从日期字符串中提取年份"""
|
||||
m = re.search(r'(\d{4})-\d{2}-\d{2}', date_str or '')
|
||||
return m.group(1) if m else ''
|
||||
|
||||
def getRemarks(self, data):
|
||||
"""获取备注信息(标签)"""
|
||||
tags = [tag.text() for tag in data('.badge').items() if tag.text()]
|
||||
return ' '.join(set(tags)) if tags else ''
|
||||
|
||||
def getPlaylist(self, data, url):
|
||||
"""
|
||||
获取播放列表
|
||||
从source、video标签和脚本中提取m3u8链接
|
||||
"""
|
||||
play_urls, seen = [], set()
|
||||
|
||||
# 从source标签获取
|
||||
for src in data('source').items():
|
||||
u = src.attr('src')
|
||||
if u and u not in seen:
|
||||
play_urls.append(f"源{len(play_urls)+1}${self.e64(u)}")
|
||||
seen.add(u)
|
||||
|
||||
# 从video标签获取
|
||||
for u in data('video').items():
|
||||
u = u.attr('src')
|
||||
if u and u not in seen:
|
||||
play_urls.append(f"线路{len(play_urls)+1}${self.e64(u)}")
|
||||
seen.add(u)
|
||||
|
||||
# 从脚本中提取m3u8链接
|
||||
for m in re.findall(r'https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*', data('script').text()):
|
||||
if m not in seen:
|
||||
play_urls.append(f"线路{len(play_urls)+1}${self.e64(m)}")
|
||||
seen.add(m)
|
||||
|
||||
# 如果没有找到播放链接,使用页面URL
|
||||
if not play_urls:
|
||||
play_urls.append(f"在线播放${self.e64(url)}")
|
||||
|
||||
return '#'.join(play_urls)
|
||||
|
||||
def getpq(self, path=''):
|
||||
"""获取页面内容并返回PyQuery对象"""
|
||||
url = path if path.startswith('http') else f'{self.host}{path}'
|
||||
try:
|
||||
rsp = self.session.get(url, timeout=20)
|
||||
rsp.encoding = 'utf-8'
|
||||
return pq(rsp.text)
|
||||
except Exception as e:
|
||||
print(f"getpq error: {e}")
|
||||
return pq('')
|
||||
|
||||
def e64(self, text):
|
||||
"""Base64编码"""
|
||||
try:
|
||||
return b64encode(text.encode('utf-8')).decode('utf-8')
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def d64(self, encoded_text):
|
||||
"""Base64解码"""
|
||||
try:
|
||||
return b64decode(encoded_text.encode('utf-8')).decode('utf-8')
|
||||
except Exception:
|
||||
return ''
|
||||
301
PY1/Phb.py
Normal file
301
PY1/Phb.py
Normal file
@@ -0,0 +1,301 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
'''
|
||||
内置代理配置:真心jar为例
|
||||
{
|
||||
"key": "Phb",
|
||||
"name": "Phb",
|
||||
"type": 3,
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"api": "./py/Phb.py",
|
||||
"ext": {
|
||||
"http": "http://127.0.0.1:1072",
|
||||
"https": "http://127.0.0.1:1072"
|
||||
}
|
||||
},
|
||||
注:http(s)代理都是http
|
||||
'''
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
self.host = self.gethost()
|
||||
self.headers.update({'referer': f'{self.host}/', 'origin': self.host})
|
||||
self.session = Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"视频": "/video",
|
||||
"片单": "/playlists",
|
||||
"频道": "/channels",
|
||||
"分类": "/categories",
|
||||
"明星": "/pornstars"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq('/recommended')
|
||||
vhtml = data("#recommendedListings .pcVideoListItem .phimage")
|
||||
return {'list': self.getlist(vhtml)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid == '/video' or '_this_video' in tid:
|
||||
pagestr = f'&' if '?' in tid else f'?'
|
||||
tid = tid.split('_this_video')[0]
|
||||
data = self.getpq(f'{tid}{pagestr}page={pg}')
|
||||
vdata = self.getlist(data('#videoCategory .pcVideoListItem'))
|
||||
elif tid == '/playlists':
|
||||
data = self.getpq(f'{tid}?page={pg}')
|
||||
vhtml = data('#playListSection li')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'playlists_click_' + i('.thumbnail-info-wrapper .display-block a').attr('href'),
|
||||
'vod_name': i('.thumbnail-info-wrapper .display-block a').attr('title'),
|
||||
'vod_pic': self.proxy(i('.largeThumb').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.playlist-videos .number').text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/channels':
|
||||
data = self.getpq(f'{tid}?o=rk&page={pg}')
|
||||
vhtml = data('#filterChannelsSection li .description')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'director_click_' + i('.avatar a').attr('href'),
|
||||
'vod_name': i('.avatar img').attr('alt'),
|
||||
'vod_pic': self.proxy(i('.avatar img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.descriptionContainer ul li').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/categories' and pg == '1':
|
||||
result['pagecount'] = 1
|
||||
data = self.getpq(f'{tid}')
|
||||
vhtml = data('.categoriesListSection li .relativeWrapper')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': i('a').attr('href') + '_this_video',
|
||||
'vod_name': i('a').attr('alt'),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/pornstars':
|
||||
data = self.getpq(f'{tid}?o=t&page={pg}')
|
||||
vhtml = data('#popularPornstars .performerCard .wrap')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'pornstars_click_' + i('a').attr('href'),
|
||||
'vod_name': i('.performerCardName').text(),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_year': i('.performerVideosViewsCount span').eq(0).text(),
|
||||
'vod_remarks': i('.performerVideosViewsCount span').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif 'playlists_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
if pg == '1':
|
||||
hdata = self.getpq(tid)
|
||||
self.token = hdata('#searchInput').attr('data-token')
|
||||
vdata = self.getlist(hdata('#videoPlaylist .pcVideoListItem .phimage'))
|
||||
else:
|
||||
tid = tid.split('playlist/')[-1]
|
||||
data = self.getpq(f'/playlist/viewChunked?id={tid}&token={self.token}&page={pg}')
|
||||
vdata = self.getlist(data('.pcVideoListItem .phimage'))
|
||||
elif 'director_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#showAllChanelVideos .pcVideoListItem .phimage'))
|
||||
elif 'pornstars_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#mostRecentVideosSection .pcVideoListItem .phimage'))
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = f"{self.host}{ids[0]}"
|
||||
data = self.getpq(ids[0])
|
||||
vn = data('meta[property="og:title"]').attr('content')
|
||||
dtext = data('.userInfo .usernameWrap a')
|
||||
pdtitle = '[a=cr:' + json.dumps(
|
||||
{'id': 'director_click_' + dtext.attr('href'), 'name': dtext.text()}) + '/]' + dtext.text() + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director': pdtitle,
|
||||
'vod_remarks': (data('.userInfo').text() + ' / ' + data('.ratingInfo').text()).replace('\n', ' / '),
|
||||
'vod_play_from': 'Pornhub',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
js_content = data("#player script").eq(0).text()
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{url}')}"]
|
||||
try:
|
||||
pattern = r'"mediaDefinitions":\s*(\[.*?\]),\s*"isVertical"'
|
||||
match = re.search(pattern, js_content, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
udata = json.loads(json_str)
|
||||
plist = [
|
||||
f"{media['height']}${self.e64(f'{0}@@@@{url}')}"
|
||||
for media in udata[:-1]
|
||||
if (url := media.get('videoUrl'))
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"提取mediaDefinitions失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(f'/video/search?search={key}&page={pg}')
|
||||
return {'list': self.getlist(data('#videoSearchResult .pcVideoListItem .phimage'))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = self.d64(id).split('@@@@')
|
||||
if '.m3u8' in ids[1]: ids[1] = self.proxy(ids[1], 'm3u8')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
url = self.d64(param.get('url'))
|
||||
if param.get('type') == 'm3u8':
|
||||
return self.m3Proxy(url)
|
||||
else:
|
||||
return self.tsProxy(url)
|
||||
|
||||
def m3Proxy(self, url):
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain = last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def tsProxy(self, url):
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers['Content-Type'], data.content]
|
||||
|
||||
def gethost(self):
|
||||
try:
|
||||
response = requests.get('https://www.pornhub.com', headers=self.headers, proxies=self.proxies,
|
||||
allow_redirects=False)
|
||||
return response.headers['Location'][:-1]
|
||||
except Exception as e:
|
||||
print(f"获取主页失败: {str(e)}")
|
||||
return "https://www.pornhub.com"
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
vlist.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': self.proxy(i('img').attr('src')),
|
||||
'vod_remarks': i('.bgShadeEffect').text() or i('.duration').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path):
|
||||
try:
|
||||
response = self.session.get(f'{self.host}{path}').text
|
||||
return pq(response.encode('utf-8'))
|
||||
except Exception as e:
|
||||
print(f"请求失败: , {str(e)}")
|
||||
return None
|
||||
|
||||
def proxy(self, data, type='img'):
|
||||
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:return data
|
||||
301
PY1/Pornhub.py
Normal file
301
PY1/Pornhub.py
Normal file
@@ -0,0 +1,301 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
'''
|
||||
内置代理配置:真心jar为例
|
||||
{
|
||||
"key": "Phb",
|
||||
"name": "Phb",
|
||||
"type": 3,
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"filterable": 1,
|
||||
"api": "./py/Phb.py",
|
||||
"ext": {
|
||||
"http": "http://127.0.0.1:1072",
|
||||
"https": "http://127.0.0.1:1072"
|
||||
}
|
||||
},
|
||||
注:http(s)代理都是http
|
||||
'''
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
self.host = self.gethost()
|
||||
self.headers.update({'referer': f'{self.host}/', 'origin': self.host})
|
||||
self.session = Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"视频": "/video",
|
||||
"片单": "/playlists",
|
||||
"频道": "/channels",
|
||||
"分类": "/categories",
|
||||
"明星": "/pornstars"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq('/recommended')
|
||||
vhtml = data("#recommendedListings .pcVideoListItem .phimage")
|
||||
return {'list': self.getlist(vhtml)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid == '/video' or '_this_video' in tid:
|
||||
pagestr = f'&' if '?' in tid else f'?'
|
||||
tid = tid.split('_this_video')[0]
|
||||
data = self.getpq(f'{tid}{pagestr}page={pg}')
|
||||
vdata = self.getlist(data('#videoCategory .pcVideoListItem'))
|
||||
elif tid == '/playlists':
|
||||
data = self.getpq(f'{tid}?page={pg}')
|
||||
vhtml = data('#playListSection li')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'playlists_click_' + i('.thumbnail-info-wrapper .display-block a').attr('href'),
|
||||
'vod_name': i('.thumbnail-info-wrapper .display-block a').attr('title'),
|
||||
'vod_pic': self.proxy(i('.largeThumb').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.playlist-videos .number').text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/channels':
|
||||
data = self.getpq(f'{tid}?o=rk&page={pg}')
|
||||
vhtml = data('#filterChannelsSection li .description')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'director_click_' + i('.avatar a').attr('href'),
|
||||
'vod_name': i('.avatar img').attr('alt'),
|
||||
'vod_pic': self.proxy(i('.avatar img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.descriptionContainer ul li').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/categories' and pg == '1':
|
||||
result['pagecount'] = 1
|
||||
data = self.getpq(f'{tid}')
|
||||
vhtml = data('.categoriesListSection li .relativeWrapper')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': i('a').attr('href') + '_this_video',
|
||||
'vod_name': i('a').attr('alt'),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif tid == '/pornstars':
|
||||
data = self.getpq(f'{tid}?o=t&page={pg}')
|
||||
vhtml = data('#popularPornstars .performerCard .wrap')
|
||||
vdata = []
|
||||
for i in vhtml.items():
|
||||
vdata.append({
|
||||
'vod_id': 'pornstars_click_' + i('a').attr('href'),
|
||||
'vod_name': i('.performerCardName').text(),
|
||||
'vod_pic': self.proxy(i('a img').attr('src')),
|
||||
'vod_tag': 'folder',
|
||||
'vod_year': i('.performerVideosViewsCount span').eq(0).text(),
|
||||
'vod_remarks': i('.performerVideosViewsCount span').eq(-1).text(),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
elif 'playlists_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
if pg == '1':
|
||||
hdata = self.getpq(tid)
|
||||
self.token = hdata('#searchInput').attr('data-token')
|
||||
vdata = self.getlist(hdata('#videoPlaylist .pcVideoListItem .phimage'))
|
||||
else:
|
||||
tid = tid.split('playlist/')[-1]
|
||||
data = self.getpq(f'/playlist/viewChunked?id={tid}&token={self.token}&page={pg}')
|
||||
vdata = self.getlist(data('.pcVideoListItem .phimage'))
|
||||
elif 'director_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#showAllChanelVideos .pcVideoListItem .phimage'))
|
||||
elif 'pornstars_click' in tid:
|
||||
tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}/videos?page={pg}')
|
||||
vdata = self.getlist(data('#mostRecentVideosSection .pcVideoListItem .phimage'))
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = f"{self.host}{ids[0]}"
|
||||
data = self.getpq(ids[0])
|
||||
vn = data('meta[property="og:title"]').attr('content')
|
||||
dtext = data('.userInfo .usernameWrap a')
|
||||
pdtitle = '[a=cr:' + json.dumps(
|
||||
{'id': 'director_click_' + dtext.attr('href'), 'name': dtext.text()}) + '/]' + dtext.text() + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director': pdtitle,
|
||||
'vod_remarks': (data('.userInfo').text() + ' / ' + data('.ratingInfo').text()).replace('\n', ' / '),
|
||||
'vod_play_from': '老僧酿酒',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
js_content = data("#player script").eq(0).text()
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{url}')}"]
|
||||
try:
|
||||
pattern = r'"mediaDefinitions":\s*(\[.*?\]),\s*"isVertical"'
|
||||
match = re.search(pattern, js_content, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
udata = json.loads(json_str)
|
||||
plist = [
|
||||
f"{media['height']}${self.e64(f'{0}@@@@{url}')}"
|
||||
for media in udata[:-1]
|
||||
if (url := media.get('videoUrl'))
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"提取mediaDefinitions失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(f'/video/search?search={key}&page={pg}')
|
||||
return {'list': self.getlist(data('#videoSearchResult .pcVideoListItem .phimage'))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = self.d64(id).split('@@@@')
|
||||
if '.m3u8' in ids[1]: ids[1] = self.proxy(ids[1], 'm3u8')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
url = self.d64(param.get('url'))
|
||||
if param.get('type') == 'm3u8':
|
||||
return self.m3Proxy(url)
|
||||
else:
|
||||
return self.tsProxy(url)
|
||||
|
||||
def m3Proxy(self, url):
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain = last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def tsProxy(self, url):
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers['Content-Type'], data.content]
|
||||
|
||||
def gethost(self):
|
||||
try:
|
||||
response = requests.get('https://www.pornhub.com', headers=self.headers, proxies=self.proxies,
|
||||
allow_redirects=False)
|
||||
return response.headers['Location'][:-1]
|
||||
except Exception as e:
|
||||
print(f"获取主页失败: {str(e)}")
|
||||
return "https://www.pornhub.com"
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
vlist.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': self.proxy(i('img').attr('src')),
|
||||
'vod_remarks': i('.bgShadeEffect').text() or i('.duration').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path):
|
||||
try:
|
||||
response = self.session.get(f'{self.host}{path}').text
|
||||
return pq(response.encode('utf-8'))
|
||||
except Exception as e:
|
||||
print(f"请求失败: , {str(e)}")
|
||||
return None
|
||||
|
||||
def proxy(self, data, type='img'):
|
||||
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:return data
|
||||
196
PY1/ThePorn.py
Normal file
196
PY1/ThePorn.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "ThePorn"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
try:
|
||||
rsp = self.fetch("https://theporn.cc/categories")
|
||||
root = pq(rsp.text)
|
||||
classes = []
|
||||
|
||||
main_categories = {
|
||||
"影片": "categories/all", "日本AV": "jav", "欧美": "eu", "VR": "vr",
|
||||
"最佳影片": "video/best", "日本无码": "jav/uncensored"
|
||||
}
|
||||
|
||||
for name, cid in main_categories.items():
|
||||
classes.append({'type_name': name, 'type_id': cid})
|
||||
|
||||
for item in root('.categories_list .categorie_item').items():
|
||||
a_tag = item.find('a')
|
||||
if a_tag:
|
||||
name = a_tag.text().strip()
|
||||
href = a_tag.attr('href')
|
||||
if href and name:
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': f"categories/{href.split('/')[-1]}"
|
||||
})
|
||||
|
||||
seen = set()
|
||||
result['class'] = [cls for cls in classes if not (key := (cls['type_name'], cls['type_id'])) in seen and not seen.add(key)]
|
||||
|
||||
except Exception as e:
|
||||
cateManual = {
|
||||
"影片": "categories/all", "日本AV": "jav", "欧美": "eu", "VR": "vr",
|
||||
"最佳影片": "video/best", "日本无码": "jav/uncensored"
|
||||
}
|
||||
result['class'] = [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
rsp = self.fetch("https://theporn.cc/")
|
||||
return {'list': [self.parseVideoItem(item) for item in pq(rsp.text)('.video-list .avdata-outer').items()]}
|
||||
|
||||
def parseVideoItem(self, item):
|
||||
try:
|
||||
a_tag = item.find('a.av-link')
|
||||
href = a_tag.attr('href')
|
||||
img_tag = item.find('img.cover-img')
|
||||
cover = img_tag.attr('data-src') or img_tag.attr('src')
|
||||
return {
|
||||
"vod_id": href.split('/')[-1],
|
||||
"vod_name": item.find('.av_data_title').text(),
|
||||
"vod_pic": cover if cover.startswith('http') else "https://theporn.cc" + cover,
|
||||
"vod_remarks": item.find('.duration').text()
|
||||
} if href else None
|
||||
except:
|
||||
return None
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
# 修复翻页逻辑
|
||||
if int(pg) == 1:
|
||||
url = f"https://theporn.cc/{tid}"
|
||||
else:
|
||||
url = f"https://theporn.cc/{tid}/{pg}"
|
||||
|
||||
rsp = self.fetch(url)
|
||||
root = pq(rsp.text)
|
||||
|
||||
videos = []
|
||||
for selector in ['.video-list .avdata-outer', '.avdata-outer', '.video-list .card']:
|
||||
video_list = root(selector)
|
||||
if video_list.length > 0:
|
||||
videos = [self.parseVideoItem(item) for item in video_list.items()]
|
||||
break
|
||||
|
||||
# 尝试获取总页数
|
||||
pagecount = 9999
|
||||
try:
|
||||
pagination = root('.pagination')
|
||||
if pagination.length > 0:
|
||||
page_links = pagination.find('a')
|
||||
if page_links.length > 0:
|
||||
last_page = 1
|
||||
for link in page_links.items():
|
||||
href = link.attr('href')
|
||||
if href:
|
||||
# 从URL中提取页码
|
||||
match = re.search(r'/(\d+)$', href)
|
||||
if match:
|
||||
page_num = int(match.group(1))
|
||||
last_page = max(last_page, page_num)
|
||||
pagecount = last_page
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': pagecount,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
def detailContent(self, array):
|
||||
tid = array[0]
|
||||
rsp = self.fetch(f"https://theporn.cc/video/{tid}")
|
||||
root = pq(rsp.text)
|
||||
|
||||
title = root('title').text().replace(' - ThePorn', '') or root('.av-big-title .inner-title').text()
|
||||
cover = root('.cover-img').attr('src')
|
||||
if not cover:
|
||||
match = re.search(r'background-image:\s*url\("([^"]+)"\)', root('.vjs-poster').attr('style') or '')
|
||||
cover = match.group(1) if match else ''
|
||||
|
||||
return {'list': [{
|
||||
'vod_id': tid,
|
||||
'vod_name': title,
|
||||
'vod_pic': cover,
|
||||
'vod_content': title,
|
||||
'vod_actor': '未知演员',
|
||||
'vod_director': '未知导演',
|
||||
'vod_area': '未知地区',
|
||||
'vod_year': '2024',
|
||||
'vod_remarks': root('.duration').text() or '未知时长',
|
||||
'vod_play_from': 'ThePorn',
|
||||
'vod_play_url': f'第1集${tid}'
|
||||
}]}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
# 搜索页面的翻页逻辑
|
||||
if int(page) == 1:
|
||||
url = f"https://theporn.cc/search/{key}"
|
||||
else:
|
||||
url = f"https://theporn.cc/search/{key}/{page}"
|
||||
|
||||
rsp = self.fetch(url)
|
||||
root = pq(rsp.text)
|
||||
|
||||
for selector in ['.search-results .video-item', '.video-list .avdata-outer', '.avdata-outer']:
|
||||
video_list = root(selector)
|
||||
if video_list.length > 0:
|
||||
return {'list': [self.parseVideoItem(item) for item in video_list.items()]}
|
||||
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
rsp = self.fetch(f"https://theporn.cc/video/{id}")
|
||||
root = pq(rsp.text)
|
||||
|
||||
hash_id = ""
|
||||
for script in root('script').items():
|
||||
script_text = script.text()
|
||||
if 'hash_id' in script_text:
|
||||
match = re.search(r'"hash_id":\s*"([a-f0-9]+)"', script_text)
|
||||
if match:
|
||||
hash_id = match.group(1)
|
||||
break
|
||||
|
||||
if hash_id:
|
||||
src = f"https://b2.bttss.cc/videos/{hash_id}/g.m3u8?h=3121efe8979c635"
|
||||
else:
|
||||
video_tag = root('video#video-player_html5_api')
|
||||
src = video_tag.attr('src') if video_tag else ""
|
||||
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": src,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": "https://theporn.cc/",
|
||||
"Origin": "https://theporn.cc"
|
||||
}
|
||||
}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(fmt in url.lower() for fmt in ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.webm']) if url else False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
270
PY1/Xhm.py
Normal file
270
PY1/Xhm.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
self.headers['referer'] = f'{self.host}/'
|
||||
self.session = Session()
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
'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',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-full-version': '"133.0.6943.98"',
|
||||
'sec-ch-ua-arch': '"x86"',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua-platform-version': '"19.0.0"',
|
||||
'sec-ch-ua-model': '""',
|
||||
'sec-ch-ua-full-version-list': '"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="133.0.6943.98", "Chromium";v="133.0.6943.98"',
|
||||
'dnt': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=0, i'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"4K": "/4k",
|
||||
"国产": "two_click_/categories/chinese",
|
||||
"最新": "/newest",
|
||||
"最佳": "/best",
|
||||
"频道": "/channels",
|
||||
"类别": "/categories",
|
||||
"明星": "/pornstars"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
if k !='4K':filters[cateManual[k]]=[{'key':'type','name':'类型','value':[{'n':'4K','v':'/4k'}]}]
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq()
|
||||
return {'list': self.getlist(data(".thumb-list--sidebar .thumb-list__item"))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid in ['/4k', '/newest', '/best'] or 'two_click_' in tid:
|
||||
if 'two_click_' in tid: tid = tid.split('click_')[-1]
|
||||
data = self.getpq(f'{tid}{extend.get("type","")}/{pg}')
|
||||
vdata = self.getlist(data(".thumb-list--sidebar .thumb-list__item"))
|
||||
elif tid == '/channels':
|
||||
data = self.getpq(f'{tid}/{pg}')
|
||||
jsdata = self.getjsdata(data)
|
||||
for i in jsdata['channels']:
|
||||
vdata.append({
|
||||
'vod_id': f"two_click_" + i.get('channelURL'),
|
||||
'vod_name': i.get('channelName'),
|
||||
'vod_pic': i.get('siteLogoURL'),
|
||||
'vod_year': f'videos:{i.get("videoCount")}',
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': f'subscribers:{i["subscriptionModel"].get("subscribers")}',
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif tid == '/categories':
|
||||
result['pagecount'] = pg
|
||||
data = self.getpq(tid)
|
||||
self.cdata = self.getjsdata(data)
|
||||
for i in self.cdata['layoutPage']['store']['popular']['assignable']:
|
||||
vdata.append({
|
||||
'vod_id': "one_click_" + i.get('id'),
|
||||
'vod_name': i.get('name'),
|
||||
'vod_pic': '',
|
||||
'vod_tag': 'folder',
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif tid == '/pornstars':
|
||||
data = self.getpq(f'{tid}/{pg}')
|
||||
pdata = self.getjsdata(data)
|
||||
for i in pdata['pagesPornstarsComponent']['pornstarListProps']['pornstars']:
|
||||
vdata.append({
|
||||
'vod_id': f"two_click_" + i.get('pageURL'),
|
||||
'vod_name': i.get('name'),
|
||||
'vod_pic': i.get('imageThumbUrl'),
|
||||
'vod_remarks': i.get('translatedCountryName'),
|
||||
'vod_tag': 'folder',
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif 'one_click' in tid:
|
||||
result['pagecount'] = pg
|
||||
tid = tid.split('click_')[-1]
|
||||
for i in self.cdata['layoutPage']['store']['popular']['assignable']:
|
||||
if i.get('id') == tid:
|
||||
for j in i['items']:
|
||||
vdata.append({
|
||||
'vod_id': f"two_click_" + j.get('url'),
|
||||
'vod_name': j.get('name'),
|
||||
'vod_pic': j.get('thumb'),
|
||||
'vod_tag': 'folder',
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(ids[0])
|
||||
djs = self.getjsdata(data)
|
||||
vn = data('meta[property="og:title"]').attr('content')
|
||||
dtext = data('#video-tags-list-container')
|
||||
href = dtext('a').attr('href')
|
||||
title = dtext('span[class*="body-bold-"]').eq(0).text()
|
||||
pdtitle = ''
|
||||
if href:
|
||||
pdtitle = '[a=cr:' + json.dumps({'id': 'two_click_' + href, 'name': title}) + '/]' + title + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director': pdtitle,
|
||||
'vod_remarks': data('.rb-new__info').text(),
|
||||
'vod_play_from': 'Xhamster',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
try:
|
||||
plist = []
|
||||
d = djs['xplayerSettings']['sources']
|
||||
f = d.get('standard')
|
||||
def custom_sort_key(url):
|
||||
quality = url.split('$')[0]
|
||||
number = ''.join(filter(str.isdigit, quality))
|
||||
number = int(number) if number else 0
|
||||
return -number, quality
|
||||
|
||||
if f:
|
||||
for key, value in f.items():
|
||||
if isinstance(value, list):
|
||||
for info in value:
|
||||
id = self.e64(f'{0}@@@@{info.get("url") or info.get("fallback")}')
|
||||
plist.append(f"{info.get('label') or info.get('quality')}${id}")
|
||||
plist.sort(key=custom_sort_key)
|
||||
if d.get('hls'):
|
||||
for format_type, info in d['hls'].items():
|
||||
if url := info.get('url'):
|
||||
encoded = self.e64(f'{0}@@@@{url}')
|
||||
plist.append(f"{format_type}${encoded}")
|
||||
|
||||
except Exception as e:
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{ids[0]}')}"]
|
||||
print(f"获取视频信息失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(f'/search/{key}?page={pg}')
|
||||
return {'list': self.getlist(data(".thumb-list--sidebar .thumb-list__item")), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'origin': self.host,
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'referer': f'{self.host}/',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
ids = self.d64(id).split('@@@@')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def gethost(self):
|
||||
try:
|
||||
response = self.fetch('https://xhamster.com', headers=self.headers, allow_redirects=False)
|
||||
return response.headers['Location']
|
||||
except Exception as e:
|
||||
print(f"获取主页失败: {str(e)}")
|
||||
return "https://zn.xhamster.com"
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
vlist.append({
|
||||
'vod_id': i('.role-pop').attr('href'),
|
||||
'vod_name': i('.video-thumb-info a').text(),
|
||||
'vod_pic': i('.role-pop img').attr('src'),
|
||||
'vod_year': i('.video-thumb-info .video-thumb-views').text().split(' ')[0],
|
||||
'vod_remarks': i('.role-pop div[data-role="video-duration"]').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path=''):
|
||||
h = '' if path.startswith('http') else self.host
|
||||
response = self.session.get(f'{h}{path}').text
|
||||
try:
|
||||
return pq(response)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(response.encode('utf-8'))
|
||||
|
||||
def getjsdata(self, data):
|
||||
vhtml = data("script[id='initials-script']").text()
|
||||
jst = json.loads(vhtml.split('initials=')[-1][:-1])
|
||||
return jst
|
||||
|
||||
276
PY1/Xvd.py
Normal file
276
PY1/Xvd.py
Normal file
@@ -0,0 +1,276 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
from base64 import b64decode, b64encode
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.session = Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "https://www.xvideos.com"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36",
|
||||
"pragma": "no-cache",
|
||||
"cache-control": "no-cache",
|
||||
"sec-ch-ua-platform": "\"Windows\"",
|
||||
"sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
|
||||
"dnt": "1",
|
||||
"origin":host,
|
||||
'referer':f'{host}/',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-dest": "empty",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"priority": "u=1, i"
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"最新": "/new",
|
||||
"最佳": "/best",
|
||||
"频道": "/channels-index",
|
||||
"标签": "/tags",
|
||||
"明星": "/pornstars-index"
|
||||
}
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq()
|
||||
return {'list':self.getlist(data(".mozaique .frame-block"))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
page = f"/{int(pg) - 1}" if pg != '1' else ''
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid=='/new' or 'tags_click' in tid:
|
||||
if 'tags_click' in tid:tid=tid.split('click_')[-1]
|
||||
data=self.getpq(f'{tid}/{pg}')
|
||||
vdata=self.getlist(data(".mozaique .frame-block"))
|
||||
elif tid=='/best':
|
||||
if pg=='1':
|
||||
self.path=self.session.get(f'{self.host}{tid}',allow_redirects=False).headers['Location']
|
||||
data=self.getpq(f'{self.path}{page}')
|
||||
vdata=self.getlist(data(".mozaique .frame-block"))
|
||||
elif tid=='/channels-index' or tid=='/pornstars-index':
|
||||
data = self.getpq(f'{tid}{page}')
|
||||
vhtml=data(".mozaique .thumb-block")
|
||||
for i in vhtml.items():
|
||||
a = i('.thumb-inside .thumb a')
|
||||
match = re.search(r'src="([^"]+)"', a('script').text())
|
||||
img=''
|
||||
if match:
|
||||
img = match.group(1).strip()
|
||||
vdata.append({
|
||||
'vod_id': f"channels_click_{'/channels'if tid=='/channels-index' else ''}"+a.attr('href'),
|
||||
'vod_name': a('.profile-name').text() or i('.profile-name').text().replace('\xa0','/'),
|
||||
'vod_pic': self.proxy(img),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.thumb-under .profile-counts').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif tid=='/tags':
|
||||
result['pagecount'] = pg
|
||||
vhtml = self.getpq(tid)
|
||||
vhtml = vhtml('.tags-list')
|
||||
for d in vhtml.items():
|
||||
for i in d('li a').items():
|
||||
vdata.append({
|
||||
'vod_id': "tags_click_"+i.attr('href'),
|
||||
'vod_name': i.attr('title') or i('b').text(),
|
||||
'vod_pic': '',
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.navbadge').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif 'channels_click' in tid:
|
||||
tid=tid.split('click_')[-1]
|
||||
vhtml=self.session.post(f'{self.host}{tid}/videos/best/{int(pg)-1}').json()
|
||||
for i in vhtml['videos']:
|
||||
vdata.append({
|
||||
'vod_id': i.get('u'),
|
||||
'vod_name': i.get('tf'),
|
||||
'vod_pic': self.proxy(i.get('il')),
|
||||
'vod_year': i.get('n'),
|
||||
'vod_remarks': i.get('d'),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = f"{self.host}{ids[0]}"
|
||||
data = self.getpq(ids[0])
|
||||
vn=data('meta[property="og:title"]').attr('content')
|
||||
dtext=data('.main-uploader a')
|
||||
href=dtext.attr('href')
|
||||
pdtitle=''
|
||||
if href and href.count('/') < 2:
|
||||
href=f'/channels{href}'
|
||||
pdtitle = '[a=cr:' + json.dumps({'id': 'channels_click_'+href, 'name': dtext('.name').text()}) + '/]' + dtext('.name').text() + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director':pdtitle,
|
||||
'vod_remarks': data('.page-title').text().replace(vn,''),
|
||||
'vod_play_from': 'Xvideos',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
js_content = data("#video-player-bg script")
|
||||
jstr=''
|
||||
for script in js_content.items():
|
||||
content = script.text()
|
||||
if 'setVideoUrlLow' in content and 'html5player' in content:
|
||||
jstr = content
|
||||
break
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{url}')}"]
|
||||
def extract_video_urls(js_content):
|
||||
try:
|
||||
low = re.search(r'setVideoUrlLow\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
high = re.search(r'setVideoUrlHigh\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
hls = re.search(r'setVideoHLS\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
|
||||
return {
|
||||
'hls': hls.group(1) if hls else None,
|
||||
'high': high.group(1) if high else None,
|
||||
'low': low.group(1) if low else None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"提取视频URL失败: {str(e)}")
|
||||
return {}
|
||||
if jstr:
|
||||
try:
|
||||
urls = extract_video_urls(jstr)
|
||||
plist = [
|
||||
f"{quality}${self.e64(f'{0}@@@@{url}')}"
|
||||
for quality, url in urls.items()
|
||||
if url
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"提取url失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.getpq(f'/?k={key}&p={int(pg)-1}')
|
||||
return {'list':self.getlist(data(".mozaique .frame-block")),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids=self.d64(id).split('@@@@')
|
||||
if '.m3u8' in ids[1]: ids[1] = self.proxy(ids[1], 'm3u8')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
url=self.d64(param['url'])
|
||||
if param.get('type') == 'm3u8':
|
||||
return self.m3Proxy(url)
|
||||
else:
|
||||
return self.tsProxy(url)
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist=[]
|
||||
for i in data.items():
|
||||
a=i('.thumb-inside .thumb a')
|
||||
b=i('.thumb-under .title a')
|
||||
vlist.append({
|
||||
'vod_id': a.attr('href'),
|
||||
'vod_name': b('a').attr('title'),
|
||||
'vod_pic': self.proxy(a('img').attr('data-src')),
|
||||
'vod_year': a('.video-hd-mark').text(),
|
||||
'vod_remarks': b('.duration').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path=''):
|
||||
response = self.session.get(f'{self.host}{path}').text
|
||||
try:
|
||||
return pq(response)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(response.encode('utf-8'))
|
||||
|
||||
def m3Proxy(self, url):
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain=last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def tsProxy(self, url):
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers['Content-Type'], data.content]
|
||||
|
||||
def proxy(self, data, type='img'):
|
||||
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:return data
|
||||
260
PY1/Xvideos.py
Normal file
260
PY1/Xvideos.py
Normal file
@@ -0,0 +1,260 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
from base64 import b64decode, b64encode
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
self.headers['referer']=f'{self.host}/'
|
||||
self.session = Session()
|
||||
self.session.headers.update(self.headers)
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "https://www.xvideos.com"
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
'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',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-full-version': '"133.0.6943.98"',
|
||||
'sec-ch-ua-arch': '"x86"',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua-platform-version': '"19.0.0"',
|
||||
'sec-ch-ua-model': '""',
|
||||
'sec-ch-ua-full-version-list': '"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="133.0.6943.98", "Chromium";v="133.0.6943.98"',
|
||||
'dnt': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=0, i'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"最新": "/new",
|
||||
"最佳": "/best",
|
||||
"频道": "/channels-index",
|
||||
"标签": "/tags",
|
||||
"明星": "/pornstars-index"
|
||||
}
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq()
|
||||
return {'list':self.getlist(data(".mozaique .frame-block"))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
page = f"/{int(pg) - 1}" if pg != '1' else ''
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid=='/new' or 'tags_click' in tid:
|
||||
if 'tags_click' in tid:tid=tid.split('click_')[-1]
|
||||
data=self.getpq(f'{tid}/{pg}')
|
||||
vdata=self.getlist(data(".mozaique .frame-block"))
|
||||
elif tid=='/best':
|
||||
if pg=='1':
|
||||
self.path=self.session.get(f'{self.host}{tid}',headers=self.headers,allow_redirects=False).headers['Location']
|
||||
data=self.getpq(f'{self.path}{page}')
|
||||
vdata=self.getlist(data(".mozaique .frame-block"))
|
||||
elif tid=='/channels-index' or tid=='/pornstars-index':
|
||||
data = self.getpq(f'{tid}{page}')
|
||||
vhtml=data(".mozaique .thumb-block")
|
||||
for i in vhtml.items():
|
||||
a = i('.thumb-inside .thumb a')
|
||||
match = re.search(r'src="([^"]+)"', a('script').text())
|
||||
img=''
|
||||
if match:
|
||||
img = match.group(1).strip()
|
||||
vdata.append({
|
||||
'vod_id': f"channels_click_{'/channels'if tid=='/channels-index' else ''}"+a.attr('href'),
|
||||
'vod_name': a('.profile-name').text() or i('.profile-name').text().replace('\xa0','/'),
|
||||
'vod_pic': img,
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.thumb-under .profile-counts').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif tid=='/tags':
|
||||
result['pagecount'] = pg
|
||||
vhtml = self.getpq(tid)
|
||||
vhtml = vhtml('.tags-list')
|
||||
for d in vhtml.items():
|
||||
for i in d('li a').items():
|
||||
vdata.append({
|
||||
'vod_id': "tags_click_"+i.attr('href'),
|
||||
'vod_name': i.attr('title') or i('b').text(),
|
||||
'vod_pic': '',
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': i('.navbadge').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
elif 'channels_click' in tid:
|
||||
tid=tid.split('click_')[-1]
|
||||
headers=self.session.headers.copy()
|
||||
headers.update({'Accept': 'application/json, text/javascript, */*; q=0.01'})
|
||||
vhtml=self.post(f'{self.host}{tid}/videos/best/{int(pg)-1}',headers=headers).json()
|
||||
for i in vhtml['videos']:
|
||||
vdata.append({
|
||||
'vod_id': i.get('u'),
|
||||
'vod_name': i.get('tf'),
|
||||
'vod_pic': i.get('il'),
|
||||
'vod_year': i.get('n'),
|
||||
'vod_remarks': i.get('d'),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = f"{self.host}{ids[0]}"
|
||||
data = self.getpq(ids[0])
|
||||
vn=data('meta[property="og:title"]').attr('content')
|
||||
dtext=data('.main-uploader a')
|
||||
href=dtext.attr('href')
|
||||
pdtitle=''
|
||||
if href and href.count('/') < 2:
|
||||
href=f'/channels{href}'
|
||||
pdtitle = '[a=cr:' + json.dumps({'id': 'channels_click_'+href, 'name': dtext('.name').text()}) + '/]' + dtext('.name').text() + '[/a]'
|
||||
vod = {
|
||||
'vod_name': vn,
|
||||
'vod_director':pdtitle,
|
||||
'vod_remarks': data('.page-title').text().replace(vn,''),
|
||||
'vod_play_from': '老僧酿酒',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
js_content = data("#video-player-bg script")
|
||||
jstr=''
|
||||
for script in js_content.items():
|
||||
content = script.text()
|
||||
if 'setVideoUrlLow' in content and 'html5player' in content:
|
||||
jstr = content
|
||||
break
|
||||
plist = [f"{vn}${self.e64(f'{1}@@@@{url}')}"]
|
||||
def extract_video_urls(js_content):
|
||||
try:
|
||||
low = re.search(r'setVideoUrlLow\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
high = re.search(r'setVideoUrlHigh\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
hls = re.search(r'setVideoHLS\([\'"]([^\'"]+)[\'"]\)', js_content)
|
||||
|
||||
return {
|
||||
'hls': hls.group(1) if hls else None,
|
||||
'high': high.group(1) if high else None,
|
||||
'low': low.group(1) if low else None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"提取视频URL失败: {str(e)}")
|
||||
return {}
|
||||
if jstr:
|
||||
try:
|
||||
urls = extract_video_urls(jstr)
|
||||
plist = [
|
||||
f"{quality}${self.e64(f'{0}@@@@{url}')}"
|
||||
for quality, url in urls.items()
|
||||
if url
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"提取url失败: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.getpq(f'/?k={key}&p={int(pg)-1}')
|
||||
return {'list':self.getlist(data(".mozaique .frame-block")),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'dnt': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'origin': self.host,
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'referer': f'{self.host}/',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=1, i',
|
||||
}
|
||||
ids=self.d64(id).split('@@@@')
|
||||
return {'parse': int(ids[0]), 'url': ids[1], 'header': headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist=[]
|
||||
for i in data.items():
|
||||
a=i('.thumb-inside .thumb a')
|
||||
b=i('.thumb-under .title a')
|
||||
vlist.append({
|
||||
'vod_id': a.attr('href'),
|
||||
'vod_name': b('a').attr('title'),
|
||||
'vod_pic': a('img').attr('data-src'),
|
||||
'vod_year': a('.video-hd-mark').text(),
|
||||
'vod_remarks': b('.duration').text(),
|
||||
'style': {'ratio': 1.33, 'type': 'rect'}
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path=''):
|
||||
response = self.session.get(f'{self.host}{path}').text
|
||||
try:
|
||||
return pq(response)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(response.encode('utf-8'))
|
||||
BIN
PY1/__pycache__/103.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/103.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/18.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/18.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/4k.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/4k.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/51吸瓜动态版.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/51吸瓜动态版.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/91.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/91.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/911.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/911.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/91RB1.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/91RB1.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/91rb.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/91rb.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Chaturbate.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Chaturbate.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/DSYS.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/DSYS.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/E佬通用视频 (3).cpython-311.pyc
Normal file
BIN
PY1/__pycache__/E佬通用视频 (3).cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/E佬通用视频.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/E佬通用视频.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/NU.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/NU.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Phb.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Phb.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/ThePorn.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/ThePorn.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Xhamster.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Xhamster.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Xhm.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Xhm.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Xvd.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Xvd.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/Xvideos.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/Xvideos.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/boyfriend.show.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/boyfriend.show.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/cam4.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/cam4.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/epo.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/epo.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/fullhd.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/fullhd.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/gay.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/gay.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/kemon.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/kemon.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/lavAPP.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/lavAPP.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/onlyfans gv.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/onlyfans gv.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/pornhub.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/pornhub.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/py_mrav.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/py_mrav.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/py_stgay.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/py_stgay.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/py_stripchat.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/py_stripchat.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/py_色播聚合.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/py_色播聚合.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/rou.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/rou.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/tube8.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/tube8.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/uaa有声.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/uaa有声.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/y_139fm多分类.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/y_139fm多分类.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/yyds直播.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/yyds直播.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/一个区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/一个区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/七区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/七区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/七哥大洋马.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/七哥大洋马.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/中文X直播.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/中文X直播.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/乐哥欧美.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/乐哥欧美.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/乐哥玩物社区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/乐哥玩物社区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/九个区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/九个区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/今日看料.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/今日看料.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/四虎视频.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/四虎视频.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/好色tv.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/好色tv.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/妲己.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/妲己.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/小红薯APP.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/小红薯APP.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/小黄书[密].cpython-311.pyc
Normal file
BIN
PY1/__pycache__/小黄书[密].cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/推特APP.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/推特APP.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/有声小说.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/有声小说.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/玩具社区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/玩具社区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/瑟瑟.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/瑟瑟.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/神秘影院.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/神秘影院.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/禁片天堂.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/禁片天堂.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/菠萝七区.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/菠萝七区.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/韩国色情电影.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/韩国色情电影.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/香蕉.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/香蕉.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/黄色仓库.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/黄色仓库.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/黑料.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/黑料.cpython-311.pyc
Normal file
Binary file not shown.
BIN
PY1/__pycache__/黑料弹幕版.cpython-311.pyc
Normal file
BIN
PY1/__pycache__/黑料弹幕版.cpython-311.pyc
Normal file
Binary file not shown.
392
PY1/avjoy.py
Normal file
392
PY1/avjoy.py
Normal file
@@ -0,0 +1,392 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
host = 'https://cn.avjoy.me'
|
||||
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,*/*;q=0.8',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'referer': 'https://cn.avjoy.me/',
|
||||
'origin': 'https://cn.avjoy.me',
|
||||
}
|
||||
|
||||
def init(self, extend=''):
|
||||
self.proxies = json.loads(extend).get('proxy', {}) if extend else {}
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def getName(self):
|
||||
return "hohoj"
|
||||
|
||||
def fetch(self, url, params=None):
|
||||
try:
|
||||
resp = self.session.get(url, headers=self.session.headers, params=params,
|
||||
proxies=self.proxies, timeout=10, allow_redirects=True)
|
||||
return resp.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def fetch_resp(self, url, params=None, extra_headers=None, stream=False):
|
||||
try:
|
||||
hdrs = self.session.headers.copy()
|
||||
if extra_headers:
|
||||
hdrs.update(extra_headers)
|
||||
return self.session.get(url, headers=hdrs, params=params,
|
||||
proxies=self.proxies, timeout=10,
|
||||
allow_redirects=True, stream=stream)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
html = self.fetch(self.host)
|
||||
return {
|
||||
'class': [
|
||||
{'type_name': '最新上传视频', 'type_id': 'videos'},
|
||||
{'type_name': '视频', 'type_id': 'videos'},
|
||||
{'type_name': '类别', 'type_id': 'categories'},
|
||||
{'type_name': '标签', 'type_id': 'tags'}
|
||||
],
|
||||
'filters': self.get_filters(),
|
||||
'list': self.parse_videos_from_list_html(pq(html))
|
||||
}
|
||||
|
||||
def get_filters(self):
|
||||
return {}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
norm = tid.lstrip('/') if not tid.startswith('http') else tid
|
||||
if '?' in norm and not norm.startswith('http'):
|
||||
norm = norm.split('?', 1)[0]
|
||||
url = f"{self.host}/{norm}" if not norm.startswith('http') else norm
|
||||
params = (extend or {}).copy()
|
||||
try:
|
||||
if int(pg) > 1:
|
||||
params['page'] = pg
|
||||
except:
|
||||
pass
|
||||
params.pop('o', None)
|
||||
html = self.fetch(url, params)
|
||||
doc = pq(html)
|
||||
m_cur = re.search(r"current_url\s*=\s*\"([^\"]+)\"", html)
|
||||
if m_cur:
|
||||
base_path = m_cur.group(1)
|
||||
if base_path.startswith('/videos/') or base_path.startswith('/search/videos/'):
|
||||
url = f"{self.host}{base_path}"
|
||||
html = self.fetch(url, params)
|
||||
doc = pq(html)
|
||||
|
||||
def uniq_append(items, entry):
|
||||
key = (entry.get('vod_id'), entry.get('vod_name'))
|
||||
if key and key not in {(i.get('vod_id'), i.get('vod_name')) for i in items}:
|
||||
items.append(entry)
|
||||
if tid == 'categories':
|
||||
items = []
|
||||
for card in doc('div.content-left .row.content-row > div').items():
|
||||
a = card.find('a').eq(0)
|
||||
href = (a.attr('href') or '').strip()
|
||||
name = card.find('.category-title .title-truncate').text().strip()
|
||||
pic = (card.find('.thumb-overlay img').attr('src') or '').strip()
|
||||
if href and name and href.startswith('/videos/'):
|
||||
cat_id = href.lstrip('/')
|
||||
if pic and pic.startswith('/'):
|
||||
pic = f"{self.host}{pic}"
|
||||
uniq_append(items, {
|
||||
'vod_id': cat_id,
|
||||
'vod_name': name,
|
||||
'vod_pic': pic,
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.1}
|
||||
})
|
||||
for a in doc('.dropdown-menu.multi-column-dropdown a').items():
|
||||
href = (a.attr('href') or '').strip()
|
||||
name = a.text().strip()
|
||||
if href.startswith('/videos/') and name:
|
||||
uniq_append(items, {
|
||||
'vod_id': href.lstrip('/'),
|
||||
'vod_name': name,
|
||||
'vod_pic': '',
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.1}
|
||||
})
|
||||
return {
|
||||
'list': items,
|
||||
'page': '1',
|
||||
'pagecount': 1,
|
||||
'limit': 90,
|
||||
'total': len(items)
|
||||
}
|
||||
if tid == 'tags':
|
||||
items = []
|
||||
for a in doc('.popular-tag a').items():
|
||||
name = a.text().strip()
|
||||
href = (a.attr('href') or '').strip()
|
||||
if href.startswith('/search/videos/') and name:
|
||||
uniq_append(items, {
|
||||
'vod_id': href.lstrip('/'),
|
||||
'vod_name': name,
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.0}
|
||||
})
|
||||
for a in doc('.trending-searches a').items():
|
||||
name = a.text().strip()
|
||||
href = (a.attr('href') or '').strip()
|
||||
if href.startswith('/search/videos/') and name:
|
||||
uniq_append(items, {
|
||||
'vod_id': href.lstrip('/'),
|
||||
'vod_name': name,
|
||||
'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.0}
|
||||
})
|
||||
return {
|
||||
'list': items,
|
||||
'page': '1',
|
||||
'pagecount': 1,
|
||||
'limit': 90,
|
||||
'total': len(items)
|
||||
}
|
||||
videos = self.parse_videos_from_list_html(doc)
|
||||
if not videos:
|
||||
fallback = []
|
||||
for a in doc('a[href^="/video/"]').items():
|
||||
href = a.attr('href')
|
||||
title = a.text().strip()
|
||||
img = a.parents().find('img').eq(0).attr('src')
|
||||
if href and title:
|
||||
uniq_append(fallback, {
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'style': {"type": "rect", "ratio": 1.5}
|
||||
})
|
||||
videos = fallback
|
||||
pagecount = 1
|
||||
try:
|
||||
pagecount = doc('.pagination a').length or 1
|
||||
except:
|
||||
pagecount = 1
|
||||
return {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': pagecount,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]
|
||||
url = f"{self.host}{vid}" if vid.startswith('/') else f"{self.host}/{vid}"
|
||||
html = self.fetch(url)
|
||||
data = pq(html)
|
||||
title = data('h1').text() or data('title').text() or ''
|
||||
title = re.sub(r'\s*HoHoJ.*$', '', title)
|
||||
title = re.sub(r'\s*\|.*$', '', title)
|
||||
title = title.strip()
|
||||
poster = data('video#video').attr('poster') or data('meta[property="og:image"]').attr('content')
|
||||
vod_year = data('.info span').eq(-1).text()
|
||||
m_vid = re.search(r"video_id\s*=\s*\"(\d+)\"", html)
|
||||
video_id = m_vid.group(1) if m_vid else ''
|
||||
if not video_id:
|
||||
m_url_id = re.search(r"/video/(\d+)", url) or re.search(r"/video/(\d+)", html)
|
||||
video_id = m_url_id.group(1) if m_url_id else ''
|
||||
m_vkey = re.search(r"/embed/([a-zA-Z0-9]+)", html)
|
||||
vkey = m_vkey.group(1) if m_vkey else ''
|
||||
play_id = video_id or vkey
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_play_from': '撸出血',
|
||||
'vod_play_url': f"{title}${play_id or ''}",
|
||||
'vod_pic': poster,
|
||||
'vod_year': vod_year,
|
||||
}
|
||||
tags = []
|
||||
for tag in data('a.tag').items():
|
||||
name = tag.text().strip()
|
||||
href = tag.attr('href')
|
||||
if name and href:
|
||||
tags.append(f'[a=cr:{json.dumps({"id": href, "name": name})}/]{name}[/a]')
|
||||
if tags:
|
||||
vod['vod_content'] = ' '.join(tags)
|
||||
director_name = data('a[href^="/user/"]').text().strip()
|
||||
if director_name:
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
director_href = f"/search/videos/{quote(director_name)}"
|
||||
except:
|
||||
director_href = f"/search/videos/{director_name}"
|
||||
director_link = f"[a=cr:{json.dumps({'id': director_href, 'name': director_name})}/]{director_name}[/a]"
|
||||
vod['vod_content'] = (vod.get('vod_content', '') + ('\n' if vod.get('vod_content') else '') + '导演:' + director_link)
|
||||
intro = (data('section.video-description').text() or '').strip()
|
||||
if not intro:
|
||||
intro = (data('meta[name="description"]').attr('content') or '').strip()
|
||||
if intro:
|
||||
vod['vod_content'] = (vod.get('vod_content', '') + ('\n' if vod.get('vod_content') else '') + '影片介绍:' + intro)
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
params = {}
|
||||
try:
|
||||
if int(pg) > 1:
|
||||
params['page'] = pg
|
||||
except:
|
||||
pass
|
||||
url = f"{self.host}/search/videos/{requests.utils.quote(key)}"
|
||||
html = self.fetch(url, params)
|
||||
if not html:
|
||||
html = self.fetch(f"{self.host}/search", {'text': key, **params})
|
||||
return {'list': self.parse_videos_from_list_html(pq(html)), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
def pick_best_source(html_text):
|
||||
sources = []
|
||||
for m in re.finditer(r"<source[^>]+src=\"([^\"]+)\"[^>]*>", html_text):
|
||||
frag = html_text[m.start():m.end()]
|
||||
src = m.group(1)
|
||||
res_m = re.search(r"res=\'?(\d+)\'?", frag)
|
||||
label_m = re.search(r"label=\'([^\']+)\'", frag)
|
||||
res = int(res_m.group(1)) if res_m else 0
|
||||
label = label_m.group(1) if label_m else ''
|
||||
sources.append((res, label, src))
|
||||
if sources:
|
||||
sources.sort(reverse=True)
|
||||
return sources[0][2]
|
||||
mv = re.search(r"<video[^>]+src=\"([^\"]+)\"", html_text)
|
||||
if mv:
|
||||
return mv.group(1)
|
||||
mv2 = re.search(r"var\s+videoSrc\s*=\s*[\"']([^\"']+)[\"']", html_text)
|
||||
if mv2:
|
||||
return mv2.group(1)
|
||||
doc = pq(html_text)
|
||||
return doc('video source').attr('src') or doc('video').attr('src') or ''
|
||||
|
||||
raw = str(id).strip()
|
||||
if re.match(r'^https?://', raw) and self.isVideoFormat(raw):
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': raw,
|
||||
'header': {
|
||||
'user-agent': self.headers['user-agent'],
|
||||
'referer': self.host,
|
||||
'origin': self.host,
|
||||
}
|
||||
}
|
||||
|
||||
m = re.search(r"/video/(\d+)", raw) or re.search(r"id=(\d+)", raw)
|
||||
if m:
|
||||
raw = m.group(1)
|
||||
is_numeric = re.match(r"^\d+$", raw) is not None
|
||||
|
||||
video_url = ''
|
||||
referer_used = ''
|
||||
if is_numeric:
|
||||
for path in [f"{self.host}/video/{raw}", f"{self.host}/video/{raw}/"]:
|
||||
self.session.headers['referer'] = path
|
||||
play_html = self.fetch(path)
|
||||
video_url = pick_best_source(play_html)
|
||||
if video_url:
|
||||
referer_used = path
|
||||
break
|
||||
m_dl = re.search(r"href=\"(/download\\.php\\?id=\\d+[^\"]*label=1080p)\"", play_html)
|
||||
if not m_dl:
|
||||
m_dl = re.search(r"href=\"(/download\\.php\\?id=\\d+[^\"]*)\"", play_html)
|
||||
if m_dl:
|
||||
dl_url = f"{self.host}{m_dl.group(1)}"
|
||||
resp = self.fetch_resp(dl_url, extra_headers={'referer': path}, stream=True)
|
||||
if resp and resp.ok:
|
||||
resp.close()
|
||||
video_url = resp.url
|
||||
referer_used = path
|
||||
break
|
||||
if not video_url:
|
||||
embed_url = f"{self.host}/embed/{raw}" if not is_numeric else f"{self.host}/embed?id={raw}"
|
||||
self.session.headers['referer'] = embed_url
|
||||
html = self.fetch(embed_url)
|
||||
v2 = pick_best_source(html)
|
||||
if v2:
|
||||
video_url = v2
|
||||
referer_used = embed_url
|
||||
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': video_url or '',
|
||||
'header': {
|
||||
'user-agent': self.headers['user-agent'],
|
||||
'referer': referer_used or self.host,
|
||||
'origin': self.host,
|
||||
}
|
||||
}
|
||||
def parse_videos_from_list_html(self, doc: pq):
|
||||
videos = []
|
||||
for item in doc('.row.content-row > div').items():
|
||||
link = item.find('a').eq(0).attr('href')
|
||||
img = item.find('.thumb-overlay img').eq(0).attr('src')
|
||||
info = item.find('.content-info').eq(0)
|
||||
title = info.find('.content-title').text().strip()
|
||||
duration = (item.find('.video-duration, .thumb-overlay .duration, .content-duration, .duration').eq(0).text() or '').strip()
|
||||
overlay_text = (item.find('.thumb-overlay').text() or '').strip()
|
||||
hd_flag = bool(item.find('.hd, .icon-hd, .hd-icon, .badge-hd, .label-hd').length) or ('HD' in overlay_text)
|
||||
if not link or not title:
|
||||
continue
|
||||
parts = []
|
||||
if hd_flag:
|
||||
parts.append('HD')
|
||||
if duration:
|
||||
parts.append(duration)
|
||||
remarks = ' • '.join(parts)
|
||||
videos.append({
|
||||
'vod_id': link,
|
||||
'vod_name': re.sub(r'\s*\|.*$', '', re.sub(r'\s*HoHoJ.*$', '', title)).strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': remarks or '',
|
||||
'vod_tag': '',
|
||||
'style': {"type": "rect", "ratio": 1.5}
|
||||
})
|
||||
if not videos:
|
||||
for info in doc('.content-info').items():
|
||||
a = info('a').eq(0)
|
||||
link = a.attr('href')
|
||||
title = info('.content-title').text().strip()
|
||||
if not link or not title:
|
||||
continue
|
||||
img = info.prev('a').find('img').attr('src') or info.prevAll('a').eq(0).find('img').attr('src')
|
||||
duration = (info.parents().find('.video-duration, .thumb-overlay .duration, .content-duration, .duration').eq(0).text() or '').strip()
|
||||
overlay_text = (info.parents().find('.thumb-overlay').text() or '').strip()
|
||||
hd_flag = bool(info.parents().find('.hd, .icon-hd, .hd-icon, .badge-hd, .label-hd').length) or ('HD' in overlay_text)
|
||||
parts = []
|
||||
if hd_flag:
|
||||
parts.append('HD')
|
||||
if duration:
|
||||
parts.append(duration)
|
||||
remarks = ' • '.join(parts)
|
||||
videos.append({
|
||||
'vod_id': link,
|
||||
'vod_name': re.sub(r'\s*\|.*$', '', re.sub(r'\s*HoHoJ.*$', '', title)).strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': remarks or '',
|
||||
'vod_tag': '',
|
||||
'style': {"type": "rect", "ratio": 1.5}
|
||||
})
|
||||
return videos
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return bool(url) and (url.lower().endswith('.mp4') or url.lower().endswith('.m3u8'))
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
def destroy(self):
|
||||
pass
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
179
PY1/badnews (1).py
Normal file
179
PY1/badnews (1).py
Normal file
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def __init__(self):
|
||||
self.name = 'Bad.news'
|
||||
self.host = 'https://bad.news'
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36',
|
||||
'Referer': self.host + '/',
|
||||
'Origin': self.host,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9'
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
# =========================
|
||||
# 首页分类
|
||||
# =========================
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{'type_id': '', 'type_name': '新出品'},
|
||||
{'type_id': '/dm', 'type_name': 'H动漫'},
|
||||
{'type_id': '/av/release', 'type_name': '日本AV'},
|
||||
{'type_id': '/tag/long-porn', 'type_name': '长视频'}
|
||||
]
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.categoryContent('', '1', False, {})
|
||||
|
||||
# =========================
|
||||
# 列表解析
|
||||
# =========================
|
||||
def parse_list(self, html):
|
||||
videos = []
|
||||
# 定义黑名单关键词
|
||||
black_list = ['热点', '招聘', '20k', '工作制', '双休', '远程', '月薪']
|
||||
|
||||
# 1. 解析瀑布流 (p1)
|
||||
p1 = re.findall(
|
||||
r'href="([^"]+)"[^>]*title="([^"]+)"[^>]*(?:data-echo-background|poster)="([^"]+)"',
|
||||
html, re.S
|
||||
)
|
||||
for path, title, pic in p1:
|
||||
# 过滤逻辑:检查标题是否包含黑名单中的任何词
|
||||
if any(word in title for word in black_list):
|
||||
continue
|
||||
|
||||
if path.startswith('/'):
|
||||
videos.append({
|
||||
'vod_id': path,
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': pic.split('?')[0],
|
||||
'vod_remarks': ''
|
||||
})
|
||||
|
||||
# 2. 解析 table 信息流 (p2)
|
||||
p2 = re.findall(r'<table.*?>(.*?)</table>', html, re.S)
|
||||
for block in p2:
|
||||
# 先提取标题进行预校验
|
||||
title_m = re.search(r'<h3.*?>(.*?)</h3>', block, re.S)
|
||||
raw_title = re.sub('<[^>]+>', '', title_m.group(1)).strip() if title_m else ''
|
||||
|
||||
# 如果标题为空或者是黑名单广告,直接跳过
|
||||
if not raw_title or any(word in raw_title for word in black_list):
|
||||
continue
|
||||
|
||||
link = re.search(r'href="([^"]+)"', block)
|
||||
if not link:
|
||||
continue
|
||||
path = link.group(1)
|
||||
|
||||
if not path.startswith('/') or any(v['vod_id'] == path for v in videos):
|
||||
continue
|
||||
|
||||
pic_m = re.search(r'poster="([^"]+)"', block)
|
||||
|
||||
videos.append({
|
||||
'vod_id': path,
|
||||
'vod_name': raw_title,
|
||||
'vod_pic': pic_m.group(1).split('?')[0] if pic_m else '',
|
||||
'vod_remarks': ''
|
||||
})
|
||||
|
||||
return videos
|
||||
|
||||
# =========================
|
||||
# 分类
|
||||
# =========================
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg)
|
||||
url = f'{self.host}{tid}/page-{pg}' if tid else (self.host if pg == 1 else f'{self.host}/page-{pg}')
|
||||
res = self.fetch(url, headers=self.headers)
|
||||
return {'list': self.parse_list(res.text), 'page': pg, 'pagecount': 999}
|
||||
|
||||
# =========================
|
||||
# 详情页(HTML + DM 分流)
|
||||
# =========================
|
||||
def detailContent(self, ids):
|
||||
path = ids[0]
|
||||
url = self.host + path
|
||||
html = self.fetch(url, headers=self.headers).text
|
||||
|
||||
title_m = re.search(r'<title>(.*?)</title>', html)
|
||||
title = title_m.group(1).split('-')[0].strip() if title_m else 'Bad.news'
|
||||
|
||||
# ===== DM(H动漫)=========
|
||||
if path.startswith('/dm'):
|
||||
iframe = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
play_url = iframe.group(1) if iframe else url
|
||||
if play_url.startswith('/'):
|
||||
play_url = self.host + play_url
|
||||
|
||||
return {'list': [{
|
||||
'vod_id': play_url,
|
||||
'vod_name': title,
|
||||
'vod_play_from': 'DM-Web',
|
||||
'vod_play_url': f'播放${play_url}'
|
||||
}]}
|
||||
|
||||
# ===== 普通 HTML 视频 =====
|
||||
m = re.search(r'<video[^>]+data-source="([^"]+)"', html)
|
||||
if m:
|
||||
return {'list': [{
|
||||
'vod_id': path,
|
||||
'vod_name': title,
|
||||
'vod_play_from': 'HTML',
|
||||
'vod_play_url': f'播放${m.group(1)}'
|
||||
}]}
|
||||
|
||||
return {'list': []}
|
||||
|
||||
# =========================
|
||||
# 播放器
|
||||
# =========================
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
headers = {
|
||||
'User-Agent': self.headers['User-Agent'],
|
||||
'Referer': self.host + '/',
|
||||
'Origin': self.host,
|
||||
'Range': 'bytes=0-'
|
||||
}
|
||||
|
||||
# DM 用 WebView 嗅探
|
||||
if flag == 'DM-Web':
|
||||
return {
|
||||
'parse': 1,
|
||||
'sniff': 1,
|
||||
'url': id,
|
||||
'header': headers,
|
||||
'sniff_include': ['.mp4', '.m3u8'],
|
||||
'sniff_exclude': [
|
||||
'.html', '.js', '.css',
|
||||
'.jpg', '.png', '.gif',
|
||||
'google', 'facebook',
|
||||
'doubleclick', 'analytics',
|
||||
'ads', 'tracker'
|
||||
]
|
||||
}
|
||||
|
||||
# HTML 直连
|
||||
return {'parse': 0, 'url': id}
|
||||
|
||||
# =========================
|
||||
# 搜索
|
||||
# =========================
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f'{self.host}/search/q-{key}'
|
||||
res = self.fetch(url, headers=self.headers)
|
||||
return {'list': self.parse_list(res.text)}
|
||||
271
PY1/boyfriend.show.py
Normal file
271
PY1/boyfriend.show.py
Normal file
@@ -0,0 +1,271 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
import sys, re, os
|
||||
import base64
|
||||
import hashlib
|
||||
import requests
|
||||
from typing import Tuple
|
||||
try:
|
||||
from base.spider import Spider
|
||||
except ModuleNotFoundError:
|
||||
# Ensure project root (..\..\) is on sys.path when executed directly
|
||||
CURRENT_FILE = os.path.abspath(__file__)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(CURRENT_FILE)))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
from base.spider import Spider
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote, unquote
|
||||
from urllib3.util.retry import Retry
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend="{}"): # noqa: N802 (match framework signature)
|
||||
origin = 'https://zh.boyfriend.show'
|
||||
self.host = origin
|
||||
self.headers = {
|
||||
'Origin': origin,
|
||||
'Referer': f"{origin}/",
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0'
|
||||
}
|
||||
self.stripchat_key = self.decode_key_compact()
|
||||
# 缓存字典
|
||||
self._hash_cache = {}
|
||||
self.create_session_with_retry()
|
||||
|
||||
def getName(self):
|
||||
return 'Boyfriend Show'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return bool(re.search(r'(m3u8|mp4)(?:\?|$)', url))
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
try:
|
||||
self.session.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
CLASSES = [
|
||||
{'type_name': '女主播g', 'type_id': 'girls'},
|
||||
{'type_name': '情侣c', 'type_id': 'couples'},
|
||||
{'type_name': '男主播m', 'type_id': 'men'},
|
||||
{'type_name': '跨性别t', 'type_id': 'trans'}
|
||||
]
|
||||
VALUE = (
|
||||
{'n': '中国', 'v': 'tagLanguageChinese'},
|
||||
{'n': '亚洲', 'v': 'ethnicityAsian'},
|
||||
{'n': '白人', 'v': 'ethnicityWhite'},
|
||||
{'n': '拉丁', 'v': 'ethnicityLatino'},
|
||||
{'n': '混血', 'v': 'ethnicityMultiracial'},
|
||||
{'n': '印度', 'v': 'ethnicityIndian'},
|
||||
{'n': '阿拉伯', 'v': 'ethnicityMiddleEastern'},
|
||||
{'n': '黑人', 'v': 'ethnicityEbony'}
|
||||
)
|
||||
VALUE_MEN = (
|
||||
{'n': '情侣', 'v': 'sexGayCouples'},
|
||||
{'n': '直男', 'v': 'orientationStraight'}
|
||||
)
|
||||
TIDS = ('girls', 'couples', 'men', 'trans')
|
||||
filters = {
|
||||
tid: [{'key': 'tag', 'value': VALUE_MEN + VALUE if tid == 'men' else VALUE}]
|
||||
for tid in TIDS
|
||||
}
|
||||
return {
|
||||
'class': CLASSES,
|
||||
'filters': filters
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return []
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
limit = 60
|
||||
offset = limit * (int(pg) - 1)
|
||||
url = f"{self.host}/api/front/models?improveTs=false&removeShows=false&limit={limit}&offset={offset}&primaryTag={tid}&sortBy=stripRanking&rcmGrp=A&rbCnGr=true&prxCnGr=false&nic=false"
|
||||
if 'tag' in extend:
|
||||
url += "&filterGroupTags=%5B%5B%22" + extend['tag'] + "%22%5D%5D"
|
||||
rsp = self.fetch(url).json()
|
||||
videos = [
|
||||
{
|
||||
"vod_id": str(vod['username']).strip(),
|
||||
"vod_name": f"{self.country_code_to_flag(str(vod['country']).strip())}{str(vod['username']).strip()}",
|
||||
# 使用与站点一致的org域名
|
||||
"vod_pic": f"https://img.doppiocdn.org/thumbs/{vod['snapshotTimestamp']}/{vod['id']}",
|
||||
"vod_remarks": "" if vod.get('status') == "public" else "🎫"
|
||||
}
|
||||
for vod in rsp.get('models', [])
|
||||
]
|
||||
total = int(rsp.get('filteredCount', 0))
|
||||
return {
|
||||
"list": videos,
|
||||
"page": pg,
|
||||
"pagecount": (total + limit - 1) // limit,
|
||||
"limit": limit,
|
||||
"total": total
|
||||
}
|
||||
|
||||
def detailContent(self, array):
|
||||
username = array[0]
|
||||
rsp = self.fetch(f"{self.host}/api/front/v2/models/username/{username}/cam").json()
|
||||
info = rsp['cam']
|
||||
user = rsp['user']['user']
|
||||
id = str(user['id'])
|
||||
country = str(user['country']).strip()
|
||||
isLive = "" if user['isLive'] else " 已下播"
|
||||
flag = self.country_code_to_flag(country)
|
||||
remark, startAt = '', ''
|
||||
if show := info.get('show'):
|
||||
startAt = show.get('createdAt')
|
||||
elif show := info.get('groupShowAnnouncement'):
|
||||
startAt = show.get('startAt')
|
||||
if startAt:
|
||||
BJtime = (datetime.strptime(startAt, "%Y-%m-%dT%H:%M:%SZ") + timedelta(hours=8)).strftime("%m月%d日 %H:%M")
|
||||
remark = f"🎫 始于 {BJtime}"
|
||||
vod = {
|
||||
"vod_id": id,
|
||||
"vod_name": str(info['topic']).strip(),
|
||||
"vod_pic": str(user['avatarUrl']),
|
||||
"vod_director": f"{flag}{username}{isLive}",
|
||||
"vod_remarks": remark,
|
||||
'vod_play_from': 'Boyfriend Show',
|
||||
'vod_play_url': f"{id}${id}"
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def process_key(self, key: str) -> Tuple[str, str]:
|
||||
tags = {'G': 'girls', 'C': 'couples', 'M': 'men', 'T': 'trans'}
|
||||
parts = key.split(maxsplit=1) # 仅分割第一个空格
|
||||
if len(parts) > 1 and (tag := tags.get(parts[0].upper())):
|
||||
return tag, parts[1].strip()
|
||||
return 'men', key.strip()
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
result = {}
|
||||
if int(pg) > 1:
|
||||
return result
|
||||
tag, key = self.process_key(key)
|
||||
url = f"{self.host}/api/front/v4/models/search/group/username?query={key}&limit=900&primaryTag={tag}"
|
||||
rsp = self.fetch(url).json()
|
||||
result['list'] = [
|
||||
{
|
||||
"vod_id": str(user['username']).strip(),
|
||||
"vod_name": f"{self.country_code_to_flag(str(user['country']).strip())}{user['username']}",
|
||||
"vod_pic": f"https://img.doppiocdn.org/thumbs/{user['snapshotTimestamp']}/{user['id']}",
|
||||
"vod_remarks": "" if user['status'] == "public" else "🎫"
|
||||
}
|
||||
for user in rsp.get('models', [])
|
||||
if user['isLive']
|
||||
]
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
# 主索引m3u8
|
||||
url = f"https://edge-hls.doppiocdn.net/hls/{id}/master/{id}_auto.m3u8?playlistType=lowLatency"
|
||||
rsp = self.fetch(url)
|
||||
lines = rsp.text.strip().split('\n')
|
||||
psch, pkey = '', ''
|
||||
urls = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#EXT-X-MOUFLON:'):
|
||||
if parts := line.split(':'):
|
||||
if len(parts) >= 4:
|
||||
psch, pkey = parts[2], parts[3]
|
||||
if '#EXT-X-STREAM-INF' in line:
|
||||
name_start = line.find('NAME="') + 6
|
||||
name_end = line.find('"', name_start)
|
||||
qn = line[name_start:name_end]
|
||||
# URL在下一行
|
||||
url_base = lines[i + 1]
|
||||
# 组合最终的URL,并加上psch和pkey参数
|
||||
full_url = f"{url_base}&psch={psch}&pkey={pkey}"
|
||||
proxy_url = f"{self.getProxyUrl()}&url={quote(full_url)}"
|
||||
# 将画质和URL添加到列表中
|
||||
urls.extend([qn, proxy_url])
|
||||
return {
|
||||
"url": urls,
|
||||
"parse": '0',
|
||||
"contentType": '',
|
||||
"header": self.headers
|
||||
}
|
||||
|
||||
def localProxy(self, param):
|
||||
url = unquote(param['url'])
|
||||
data = self.fetch(url)
|
||||
if data.status_code == 403:
|
||||
data = self.fetch(re.sub(r'\d+p\d*\.m3u8', '160p_blurred.m3u8', url))
|
||||
if data.status_code != 200:
|
||||
return [404, "text/plain", ""]
|
||||
data = data.text
|
||||
if "#EXT-X-MOUFLON:FILE" in data:
|
||||
data = self.process_m3u8_content_v2(data)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def process_m3u8_content_v2(self, m3u8_content):
|
||||
lines = m3u8_content.strip().split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if (line.startswith('#EXT-X-MOUFLON:FILE:') and 'media.mp4' in lines[i + 1]):
|
||||
encrypted_data = line.split(':', 2)[2].strip()
|
||||
try:
|
||||
decrypted_data = self.decrypt(encrypted_data, self.stripchat_key)
|
||||
except Exception:
|
||||
decrypted_data = self.decrypt(encrypted_data, "Zokee2OhPh9kugh4")
|
||||
lines[i + 1] = lines[i + 1].replace('media.mp4', decrypted_data)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def country_code_to_flag(self, country_code):
|
||||
if len(country_code) != 2 or not country_code.isalpha():
|
||||
return country_code
|
||||
flag_emoji = ''.join([chr(ord(c.upper()) - ord('A') + 0x1F1E6) for c in country_code])
|
||||
return flag_emoji
|
||||
|
||||
def decode_key_compact(self):
|
||||
base64_str = "NTEgNzUgNjUgNjEgNmUgMzQgNjMgNjEgNjkgMzkgNjIgNmYgNGEgNjEgMzUgNjE="
|
||||
decoded = base64.b64decode(base64_str).decode('utf-8')
|
||||
key_bytes = bytes(int(hex_str, 16) for hex_str in decoded.split(" "))
|
||||
return key_bytes.decode('utf-8')
|
||||
|
||||
def compute_hash(self, key: str) -> bytes:
|
||||
"""计算并缓存SHA-256哈希"""
|
||||
if key not in self._hash_cache:
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(key.encode('utf-8'))
|
||||
self._hash_cache[key] = sha256.digest()
|
||||
return self._hash_cache[key]
|
||||
|
||||
def decrypt(self, encrypted_b64: str, key: str) -> str:
|
||||
# 修复Base64填充
|
||||
padding = len(encrypted_b64) % 4
|
||||
if padding:
|
||||
encrypted_b64 += '=' * (4 - padding)
|
||||
|
||||
# 计算哈希并解密
|
||||
hash_bytes = self.compute_hash(key)
|
||||
encrypted_data = base64.b64decode(encrypted_b64)
|
||||
|
||||
# 异或解密
|
||||
decrypted_bytes = bytearray()
|
||||
for i, cipher_byte in enumerate(encrypted_data):
|
||||
key_byte = hash_bytes[i % len(hash_bytes)]
|
||||
decrypted_bytes.append(cipher_byte ^ key_byte)
|
||||
return decrypted_bytes.decode('utf-8')
|
||||
|
||||
def create_session_with_retry(self):
|
||||
self.session = requests.Session()
|
||||
retry_strategy = Retry(
|
||||
total=3,
|
||||
backoff_factor=0.3,
|
||||
status_forcelist=[429, 500, 502, 503, 504]
|
||||
)
|
||||
adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
|
||||
def fetch(self, url):
|
||||
return self.session.get(url, headers=self.headers, timeout=10)
|
||||
118
PY1/cam4.py
Normal file
118
PY1/cam4.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "Cam4直播"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.base = "https://zh.cam4.com"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
|
||||
}
|
||||
return self
|
||||
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
|
||||
{"type_id": "female", "type_name": "女性"},
|
||||
|
||||
{"type_id": "couples", "type_name": "情侣"},
|
||||
|
||||
]
|
||||
return {"class": classes}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if not pg:
|
||||
pg = 1
|
||||
params = f"?directoryJson=true&online=true&url=true&page={pg}"
|
||||
if tid == "female":
|
||||
params += "&gender=female"
|
||||
elif tid == "male":
|
||||
params += "&gender=male"
|
||||
elif tid == "couples":
|
||||
params += "&broadcastType=male_female_group"
|
||||
elif tid == "shemale":
|
||||
params += "&gender=shemale"
|
||||
|
||||
url = f"{self.base}/directoryCams{params}"
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
data = rsp.text
|
||||
try:
|
||||
jRoot = json.loads(data)
|
||||
except:
|
||||
return {"list": []}
|
||||
|
||||
videos = []
|
||||
for u in jRoot.get("users", []):
|
||||
title = f"{u.get('username')} ({u.get('countryCode', '')})"
|
||||
if "age" in u:
|
||||
title += f" - {u['age']}岁"
|
||||
if "resolution" in u:
|
||||
res = u["resolution"].split(":")[-1]
|
||||
title += f" [HD:{res}]"
|
||||
video = {
|
||||
"vod_id": u.get("hlsPreviewUrl"),
|
||||
"vod_name": title,
|
||||
"vod_pic": u.get("snapshotImageLink", ""),
|
||||
"vod_remarks": u.get("statusMessage", ""),
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {
|
||||
"list": videos,
|
||||
"page": int(pg),
|
||||
"pagecount": 9999,
|
||||
"limit": 90,
|
||||
"total": len(videos)
|
||||
}
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
id = ids[0]
|
||||
vod = {
|
||||
"vod_id": id,
|
||||
"vod_name": "Cam4直播",
|
||||
"vod_pic": "",
|
||||
"vod_play_from": "Cam4",
|
||||
"vod_play_url": f"直播源${id}",
|
||||
}
|
||||
return {"list": [vod]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
play_url = id
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": play_url,
|
||||
"header": self.headers
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f"{self.base}/directoryCams?directoryJson=true&online=true&url=true&showTag={key}&page={pg}"
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
data = rsp.text
|
||||
try:
|
||||
jRoot = json.loads(data)
|
||||
except:
|
||||
return {"list": []}
|
||||
|
||||
videos = []
|
||||
for u in jRoot.get("users", []):
|
||||
title = f"{u.get('username')} ({u.get('countryCode', '')})"
|
||||
video = {
|
||||
"vod_id": u.get("hlsPreviewUrl"),
|
||||
"vod_name": title,
|
||||
"vod_pic": u.get("snapshotImageLink", ""),
|
||||
"vod_remarks": u.get("statusMessage", ""),
|
||||
}
|
||||
videos.append(video)
|
||||
return {"list": videos}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return ".m3u8" in url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
255
PY1/epo.py
Normal file
255
PY1/epo.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#author Kyle
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
from base64 import b64decode, b64encode
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.eporner.com"
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Referer': self.host + '/',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
}
|
||||
self.timeout = 10
|
||||
self.retries = 2
|
||||
self.cookies = {"EPRNS": "1"}
|
||||
|
||||
def getName(self):
|
||||
return "EP涩"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return True
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result['class'] = [
|
||||
{"type_id": "/cat/all/", "type_name": "最新"},
|
||||
{"type_id": "/best-videos/", "type_name": "最佳视频"},
|
||||
{"type_id": "/top-rated/", "type_name": "最高评分"},
|
||||
{"type_id": "/cat/4k-porn/", "type_name": "4K"},
|
||||
]
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {}
|
||||
videos = self._api_search(query="all", page=1, order="latest", gay="0", per_page=20)
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
params = self._map_tid_to_api(tid)
|
||||
videos = self._api_search(page=int(pg), **params)
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = len(videos) or 20
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
result = {}
|
||||
videos = self._api_search(query=key or "all", page=int(pg), order="latest", gay="0")
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = len(videos) or 20
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {}
|
||||
url = ids[0]
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
r = self.fetch(url, headers=self.headers)
|
||||
root = self.html(r.text)
|
||||
og_title = root.xpath('//meta[@property="og:title"]/@content')
|
||||
if og_title:
|
||||
title = og_title[0].replace(' - EPORNER', '').strip()
|
||||
else:
|
||||
h1 = "".join(root.xpath('//h1//text()'))
|
||||
title = re.sub(r"\s*(\d+\s*min.*)$", "", h1).strip() or "爱看AV"
|
||||
img_elem = root.xpath('//meta[@property="og:image"]/@content') or root.xpath('//img[@id="mainvideoimg"]/@src')
|
||||
thumbnail = img_elem[0] if img_elem else ""
|
||||
meta_desc = (root.xpath('//meta[@name="description"]/@content') or root.xpath('//meta[@property="og:description"]/@content'))
|
||||
desc_text = meta_desc[0] if meta_desc else ""
|
||||
dur_match = re.search(r"Duration:\s*([0-9:]+)", desc_text)
|
||||
duration = dur_match.group(1) if dur_match else ""
|
||||
encoded_url = self.e64(url)
|
||||
play_url = f"播放${encoded_url}"
|
||||
vod = {
|
||||
"vod_id": url,
|
||||
"vod_name": title,
|
||||
"vod_pic": thumbnail,
|
||||
"vod_remarks": duration,
|
||||
"vod_content": desc_text.strip(),
|
||||
"vod_play_from": "🍑Play",
|
||||
"vod_play_url": play_url
|
||||
}
|
||||
result['list'] = [vod]
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
url = self.d64(id)
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
r = self.fetch(url, headers=self.headers)
|
||||
html_content = r.text
|
||||
pattern = r"vid\s*=\s*'([^']+)';\s*[\w*\.]+hash\s*=\s*['\"]([\da-f]{32})"
|
||||
match = re.search(pattern, html_content)
|
||||
if match:
|
||||
vid, hash_val = match.groups()
|
||||
hash_code = ''.join((self.encode_base_n(int(hash_val[i:i + 8], 16), 36) for i in range(0, 32, 8)))
|
||||
xhr_url = f"{self.host}/xhr/video/{vid}?hash={hash_code}&device=generic&domain=www.eporner.com&fallback=false&embed=false&supportedFormats=mp4"
|
||||
xhr_headers = {
|
||||
**self.headers,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01'
|
||||
}
|
||||
resp = self.fetch(xhr_url, headers=xhr_headers)
|
||||
data = json.loads(resp.text)
|
||||
if data.get("available", True):
|
||||
sources_block = data.get("sources", {})
|
||||
sources = sources_block.get("mp4", {})
|
||||
final_url = None
|
||||
if sources:
|
||||
quality_sorted = sorted(sources.keys(), key=lambda q: int(re.sub(r"\D", "", q) or 0), reverse=True)
|
||||
best_quality = quality_sorted[0]
|
||||
final_url = sources[best_quality].get("src")
|
||||
else:
|
||||
hls = sources_block.get("hls")
|
||||
if isinstance(hls, dict):
|
||||
final_url = hls.get("src")
|
||||
elif isinstance(hls, list) and hls:
|
||||
final_url = hls[0].get("src")
|
||||
best_quality = "hls"
|
||||
if final_url:
|
||||
result["parse"] = 0
|
||||
result["url"] = final_url
|
||||
result["header"] = {
|
||||
'User-Agent': self.headers['User-Agent'],
|
||||
'Referer': url,
|
||||
'Origin': self.host,
|
||||
'Accept': '*/*'
|
||||
}
|
||||
return result
|
||||
|
||||
def encode_base_n(self, num, n, table=None):
|
||||
FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
if not table:
|
||||
table = FULL_TABLE[:n]
|
||||
if n > len(table):
|
||||
raise ValueError('base %d exceeds table length %d' % (n, len(table)))
|
||||
if num == 0:
|
||||
return table[0]
|
||||
ret = ''
|
||||
while num:
|
||||
ret = table[num % n] + ret
|
||||
num = num // n
|
||||
return ret
|
||||
|
||||
def e64(self, text):
|
||||
return b64encode(text.encode()).decode()
|
||||
|
||||
def d64(self, encoded_text):
|
||||
return b64decode(encoded_text.encode()).decode()
|
||||
|
||||
def html(self, content):
|
||||
from lxml import etree
|
||||
return etree.HTML(content)
|
||||
|
||||
def fetch(self, url, headers=None, timeout=None):
|
||||
import requests, ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
for _ in range(self.retries + 1):
|
||||
resp = requests.get(url, headers=headers, timeout=timeout, verify=False, cookies=getattr(self, 'cookies', None))
|
||||
resp.encoding = 'utf-8'
|
||||
return resp
|
||||
time.sleep(1)
|
||||
raise Exception("Fetch failed")
|
||||
|
||||
def _parse_video_list(self, root):
|
||||
videos = []
|
||||
items = root.xpath('//div[contains(@class, "mb") and @data-id]')
|
||||
for item in items:
|
||||
link = item.xpath('.//a[contains(@href, "/video-")]/@href')
|
||||
if not link:
|
||||
continue
|
||||
vod_id = self.host + link[0]
|
||||
title = "".join(item.xpath('.//p[contains(@class, "mbtit")]//text()')).strip()
|
||||
img = item.xpath('.//img/@src')
|
||||
thumbnail = img[0] if img else ""
|
||||
duration = "".join(item.xpath('.//span[contains(@class,"mbtim")]/text()')).strip()
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": title or "爱看AV",
|
||||
"vod_pic": thumbnail,
|
||||
"vod_remarks": duration
|
||||
})
|
||||
return videos
|
||||
|
||||
def _map_tid_to_api(self, tid: str):
|
||||
params = {"query": "all", "order": "latest", "gay": "0", "per_page": 30}
|
||||
t = (tid or '').strip('/').lower()
|
||||
if t.startswith('best-videos'):
|
||||
params["order"] = "most-popular"
|
||||
elif t.startswith('top-rated'):
|
||||
params["order"] = "top-rated"
|
||||
elif t.startswith('cat/4k-porn'):
|
||||
params["query"] = "4k"
|
||||
elif t.startswith('cat/gay'):
|
||||
params["gay"] = "2"
|
||||
params["order"] = "latest"
|
||||
else:
|
||||
params["gay"] = "0"
|
||||
return params
|
||||
|
||||
def _api_search(self, query="all", page=1, order="latest", gay="0", per_page=30, thumbsize="medium"):
|
||||
base = f"{self.host}/api/v2/video/search/"
|
||||
q = {
|
||||
"query": query,
|
||||
"per_page": per_page,
|
||||
"page": page,
|
||||
"thumbsize": thumbsize,
|
||||
"order": order,
|
||||
"format": "json"
|
||||
}
|
||||
if gay is not None:
|
||||
q["gay"] = gay
|
||||
url = base + "?" + urllib.parse.urlencode(q)
|
||||
r = self.fetch(url, headers={**self.headers, 'X-Requested-With': 'XMLHttpRequest'})
|
||||
data = json.loads(r.text)
|
||||
return self._parse_api_list(data)
|
||||
|
||||
def _parse_api_list(self, data: dict):
|
||||
videos = []
|
||||
for v in (data or {}).get('videos', []):
|
||||
vurl = v.get('url') or ''
|
||||
title = v.get('title') or ''
|
||||
thumb = (v.get('default_thumb') or {}).get('src') or ''
|
||||
remarks = v.get('length_min') or ''
|
||||
videos.append({
|
||||
"vod_id": vurl if vurl.startswith('http') else (self.host + vurl),
|
||||
"vod_name": title or "爱看AV",
|
||||
"vod_pic": thumb,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
return videos
|
||||
379
PY1/fullhd.py
Normal file
379
PY1/fullhd.py
Normal file
@@ -0,0 +1,379 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from base.spider import Spider
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import urllib.parse
|
||||
from Crypto.Cipher import ARC4
|
||||
from Crypto.Util.Padding import unpad
|
||||
import binascii
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://www.fullhd.xxx/zh/"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
pm = ''
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{'📽️' + match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{'📽️' + match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'✨{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result = {"class": [
|
||||
{"type_id": "latest-updates", "type_name": "最新视频🌠"},
|
||||
{"type_id": "top-rated", "type_name": "最佳视频🌠"},
|
||||
{"type_id": "most-popular", "type_name": "热门影片🌠"},
|
||||
{"type_id": "networks/brazzers-com", "type_name": "Brazzers🌠"},
|
||||
{"type_id": "networks/tushy-com", "type_name": "Tushy🌠"},
|
||||
{"type_id": "networks/naughtyamerica-com", "type_name": "Naughtyamerica🌠"},
|
||||
{"type_id": "sites/sexmex", "type_name": "Sexmex🌠"},
|
||||
{"type_id": "sites/passion-hd", "type_name": "Passion-HD🌠"},
|
||||
{"type_id": "categories/animation", "type_name": "Animation🌠"},
|
||||
{"type_id": "categories/18-years-old", "type_name": "Teen🌠"},
|
||||
{"type_id": "categories/pawg", "type_name": "Pawg🌠"},
|
||||
{"type_id": "categories/thong", "type_name": "Thong🌠"},
|
||||
{"type_id": "categories/stockings", "type_name": "Stockings🌠"},
|
||||
{"type_id": "categories/jav-uncensored", "type_name": "JAV🌠"},
|
||||
{"type_id": "categories/pantyhose", "type_name": "Pantyhose🌠"}
|
||||
],
|
||||
}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
# Get videos from different sections
|
||||
sections = {
|
||||
"latest-updates": "最新视频",
|
||||
"top-rated": "最佳视频",
|
||||
"most-popular": "热门影片"
|
||||
}
|
||||
|
||||
for section_id, section_name in sections.items():
|
||||
section = doc.find('div', id=f"list_videos_videos_watched_right_now_items")
|
||||
if not section:
|
||||
continue
|
||||
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else section_name
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
# 获取图片 - 适配两种不同的img标签结构
|
||||
pic = ""
|
||||
# 第一种方式:查找带有data-src属性的img标签
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
if pics and pics.get('data-src'):
|
||||
pic = pics['data-src']
|
||||
# 第二种方式:查找带有src属性的img标签
|
||||
if not pic:
|
||||
pics = vod.find('img', class_="thumb_img")
|
||||
if pics and pics.get('src'):
|
||||
pic = pics['src']
|
||||
|
||||
# 如果找到了图片但URL不完整,添加基础URL
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Error in homeVideoContent: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
try:
|
||||
if pg and int(pg) > 1:
|
||||
url = f'{xurl}/{cid}/{pg}/'
|
||||
else:
|
||||
url = f'{xurl}/{cid}/'
|
||||
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
section = doc.find('div', class_="list-videos")
|
||||
if section:
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else ""
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
# 获取图片 - 适配两种不同的img标签结构
|
||||
pic = ""
|
||||
# 第一种方式:查找带有data-src属性的img标签
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
if pics and pics.get('data-src'):
|
||||
pic = pics['data-src']
|
||||
# 第二种方式:查找带有src属性的img标签
|
||||
if not pic:
|
||||
pics = vod.find('img', class_="thumb_img")
|
||||
if pics and pics.get('src'):
|
||||
pic = pics['src']
|
||||
|
||||
# 如果找到了图片但URL不完整,添加基础URL
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in categoryContent: {str(e)}")
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
global pm
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
playurl = ''
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
res1 = requests.get(url=did, headers=headerx)
|
||||
res1.encoding = "utf-8"
|
||||
res = res1.text
|
||||
|
||||
content = '👉' + self.extract_middle_text(res,'<h1>','</h1>', 0)
|
||||
|
||||
yanuan = self.extract_middle_text(res, '<span>Pornstars:</span>','</div>',1, 'href=".*?">(.*?)</a>')
|
||||
|
||||
bofang = did
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_actor": yanuan,
|
||||
"vod_director": '',
|
||||
"vod_content": content,
|
||||
"vod_play_from": '老僧酿酒',
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parts = id.split("http")
|
||||
xiutan = 0
|
||||
if xiutan == 0:
|
||||
if len(parts) > 1:
|
||||
before_https, after_https = parts[0], 'http' + parts[1]
|
||||
res = requests.get(url=after_https, headers=headerx)
|
||||
res = res.text
|
||||
|
||||
url2 = self.extract_middle_text(res, '<video', '</video>', 0).replace('\\', '')
|
||||
soup = BeautifulSoup(url2, 'html.parser')
|
||||
first_source = soup.find('source')
|
||||
src_value = first_source.get('src')
|
||||
|
||||
response = requests.head(src_value, allow_redirects=False)
|
||||
if response.status_code == 302:
|
||||
redirect_url = response.headers['Location']
|
||||
|
||||
response = requests.head(redirect_url, allow_redirects=False)
|
||||
if response.status_code == 302:
|
||||
redirect_url = response.headers['Location']
|
||||
|
||||
result = {}
|
||||
result["parse"] = xiutan
|
||||
result["playUrl"] = ''
|
||||
result["url"] = redirect_url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
result = {}
|
||||
videos = []
|
||||
if not page:
|
||||
page = '1'
|
||||
if page == '1':
|
||||
url = f'{xurl}/search/{key}/'
|
||||
else:
|
||||
url = f'{xurl}/search/{key}/{str(page)}/'
|
||||
|
||||
try:
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
section = doc.find('div', class_="list-videos")
|
||||
if section:
|
||||
vods = section.find_all('div', class_="item")
|
||||
for vod in vods:
|
||||
names = vod.find_all('a')
|
||||
name = names[0]['title'] if names and 'title' in names[0].attrs else ""
|
||||
|
||||
ids = vod.find_all('a')
|
||||
id = ids[0]['href'] if ids else ""
|
||||
|
||||
# 获取图片 - 适配两种不同的img标签结构
|
||||
pic = ""
|
||||
# 第一种方式:查找带有data-src属性的img标签
|
||||
pics = vod.find('img', class_="lazyload")
|
||||
if pics and pics.get('data-src'):
|
||||
pic = pics['data-src']
|
||||
# 第二种方式:查找带有src属性的img标签
|
||||
if not pic:
|
||||
pics = vod.find('img', class_="thumb_img")
|
||||
if pics and pics.get('src'):
|
||||
pic = pics['src']
|
||||
|
||||
# 如果找到了图片但URL不完整,添加基础URL
|
||||
if pic and 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="duration")
|
||||
remark = remarks.text.strip() if remarks else ""
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
except Exception as e:
|
||||
print(f"Error in searchContentPage: {str(e)}")
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': page,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
234
PY1/gay.py
Normal file
234
PY1/gay.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : Doubebly
|
||||
# @Time : 2025/9/9 20:36
|
||||
import sys
|
||||
import requests
|
||||
from lxml import etree
|
||||
from urllib.parse import quote, unquote
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return self.vod.name
|
||||
|
||||
def init(self, extend):
|
||||
self.vod = Vod(extend, self.getProxyUrl())
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
return self.vod.liveContent(url)
|
||||
|
||||
def homeContent(self, filter):
|
||||
return self.vod.homeContent(filter)
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.vod.homeVideoContent()
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
return self.vod.categoryContent(cid, page, filter, ext)
|
||||
|
||||
def detailContent(self, did):
|
||||
return self.vod.detailContent(did)
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
return self.vod.searchContent(key, quick, page)
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
return self.vod.playerContent(flag, pid, vipFlags)
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == 'img':
|
||||
try:
|
||||
content = self.vod.decrypt_img(unquote(params['url']))
|
||||
return [200, "application/octet-stream", content]
|
||||
except Exception as e:
|
||||
print(f"Image decryption failed: {e}")
|
||||
return [500, "text/plain", b"Image Error"]
|
||||
return [404, "text/plain", b""]
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
return '正在Destroy'
|
||||
|
||||
|
||||
class Vod:
|
||||
def __init__(self, extend='{}', proxy_url=""):
|
||||
self.debug = False
|
||||
self.getProxyUrl = proxy_url
|
||||
self.log(f'{proxy_url}')
|
||||
self.name = 'GAY'
|
||||
self.error_play_url = 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4'
|
||||
self.home_url = 'https://91nt.com'
|
||||
self.headers = {
|
||||
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
||||
'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",
|
||||
'cache-control': "no-cache",
|
||||
'pragma': "no-cache",
|
||||
'priority': "u=0, i",
|
||||
'referer': "https://91nt.com/",
|
||||
'sec-ch-ua': "\"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Google Chrome\";v=\"140\"",
|
||||
'sec-ch-ua-mobile': "?0",
|
||||
'sec-ch-ua-platform': "\"Windows\"",
|
||||
'sec-fetch-dest': "document",
|
||||
'sec-fetch-mode': "navigate",
|
||||
'sec-fetch-site': "same-origin",
|
||||
'sec-fetch-user': "?1",
|
||||
'upgrade-insecure-requests': "1",
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
self.log('执行了, homeContent')
|
||||
return {'class': [{'type_id': 'play', 'type_name': '精选G片'},
|
||||
{'type_id': 'xrbj', 'type_name': '鲜肉薄肌'},
|
||||
{'type_id': 'wtns', 'type_name': '无套内射'},
|
||||
{'type_id': 'zfyh', 'type_name': '制服诱惑'},
|
||||
{'type_id': 'dmfj', 'type_name': '耽美天菜'},
|
||||
{'type_id': 'jrmn', 'type_name': '肌肉猛男'},
|
||||
{'type_id': 'rhgv', 'type_name': '日韩GV'},
|
||||
{'type_id': 'omjd', 'type_name': '欧美巨屌'},
|
||||
{'type_id': 'drqp', 'type_name': '多人群交'},
|
||||
{'type_id': 'kjys', 'type_name': '口交颜射'},
|
||||
{'type_id': 'tjsm', 'type_name': '调教SM'}],
|
||||
'filters': {'play': [{'key': 'tag',
|
||||
'name': '分类',
|
||||
'value': [{'n': '当前最热', 'v': 'popular'},
|
||||
{'n': '最近更新', 'v': 'new'},
|
||||
{'n': '正在播放', 'v': 'watchings'},
|
||||
{'n': '小蓝原创', 'v': 'xiaolan'},
|
||||
{'n': '本月最热', 'v': 'mon'},
|
||||
{'n': '10分钟以上', 'v': '10min'},
|
||||
{'n': '20分钟以上', 'v': '20min'},
|
||||
{'n': '本月收藏', 'v': 'collect'},
|
||||
{'n': '高清', 'v': 'hd'},
|
||||
{'n': '每月最热', 'v': 'every'},
|
||||
{'n': '本月讨论', 'v': 'current'},
|
||||
{'n': '收藏最多', 'v': 'most'}]}]}}
|
||||
|
||||
def homeVideoContent(self):
|
||||
self.log('执行了, homeVideoContent')
|
||||
data_list = []
|
||||
url = f'{self.home_url}/videos/all/new'
|
||||
res = requests.get(url, headers=self.headers)
|
||||
root = etree.HTML(res.text.encode('utf-8'))
|
||||
data = root.xpath('//li/div[@class="video-item"]')
|
||||
for i in data:
|
||||
data_list.append(
|
||||
{
|
||||
'vod_id': i.xpath('./div[1]/a/@href')[0].strip(),
|
||||
'vod_name': i.xpath('./div[1]/a/text()')[0].strip(),
|
||||
'vod_pic': f"{self.getProxyUrl}&type=img&url={quote(i.xpath('.//img/@data-src')[0])}",
|
||||
'vod_remarks': i.xpath('.//div[contains(@class, "text-sm")]/text()')[0]
|
||||
}
|
||||
)
|
||||
return {'list': data_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
self.log('执行了, categoryContent')
|
||||
data_list = []
|
||||
if cid == 'play':
|
||||
url = f'{self.home_url}/videos/all/watchings/{page}'
|
||||
else:
|
||||
url = f'{self.home_url}/videos/category/{cid}/{page}'
|
||||
res = requests.get(url, headers=self.headers)
|
||||
root = etree.HTML(res.text.encode('utf-8'))
|
||||
data = root.xpath('//li/div[@class="video-item"]')
|
||||
for i in data:
|
||||
data_list.append(
|
||||
{
|
||||
'vod_id': i.xpath('./div[1]/a/@href')[0].strip(),
|
||||
'vod_name': i.xpath('./div[1]/a/text()')[0].strip(),
|
||||
'vod_pic': f"{self.getProxyUrl}&type=img&url={quote(i.xpath('.//img/@data-src')[0])}",
|
||||
'vod_remarks': i.xpath('.//div[contains(@class, "text-sm")]/text()')[0]
|
||||
}
|
||||
)
|
||||
return {'list': data_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def detailContent(self, did):
|
||||
ids = did[0]
|
||||
url = f'{self.home_url}{ids}'
|
||||
res = requests.get(url, headers=self.headers)
|
||||
root = etree.HTML(res.text.encode('utf-8'))
|
||||
play_url = root.xpath('//div[@class="player-container"]/div/@data-url')[0]
|
||||
play_name = root.xpath('//div[@class="player-container"]/div/@data-video')[0]
|
||||
video_list = []
|
||||
video_list.append(
|
||||
{
|
||||
'type_name': '',
|
||||
'vod_id': ids,
|
||||
'vod_name': '',
|
||||
'vod_remarks': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': '',
|
||||
'vod_play_from': self.name,
|
||||
'vod_play_url': f'{play_name}${play_url}'
|
||||
|
||||
}
|
||||
)
|
||||
return {"list": video_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
self.log('执行了, searchContent')
|
||||
data_list = []
|
||||
url = f'{self.home_url}/videos/search/{key}/{page}'
|
||||
res = requests.get(url, headers=self.headers)
|
||||
root = etree.HTML(res.text.encode('utf-8'))
|
||||
data = root.xpath('//li/div[@class="video-item"]')
|
||||
for i in data:
|
||||
# vod_id = i.xpath('./div[1]/a/@href')[0].strip()
|
||||
# vod_name = i.xpath('./div[1]/a/text()')[0].strip()
|
||||
# vod_pic = i.xpath('.//img/@data-src')[0]
|
||||
# vod_remarks = i.xpath('.//div[contains(@class, "text-sm")]/text()')[0]
|
||||
# self.log(f"{vod_id}, {vod_name}, {vod_pic}, {vod_remarks}")
|
||||
data_list.append(
|
||||
{
|
||||
'vod_id': i.xpath('./div[1]/a/@href')[0].strip(),
|
||||
'vod_name': i.xpath('./div[1]/a/text()')[0].strip().decode('utf-8'),
|
||||
'vod_pic': f"{self.getProxyUrl}&type=img&url={quote(i.xpath('.//img/@data-src')[0])}",
|
||||
'vod_remarks': i.xpath('.//div[contains(@class, "text-sm")]/text()')[0]
|
||||
}
|
||||
)
|
||||
return {'list': data_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
return {'url': pid, 'parse': 0, 'jx': 0}
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def decrypt_img(self, url):
|
||||
self.log('执行了, decrypt_img')
|
||||
key_str = 'f5d965df75336270'
|
||||
iv_str = '97b60394abc2fbe1'
|
||||
key = key_str.encode('utf-8')
|
||||
iv = iv_str.encode('utf-8')
|
||||
encrypted_bytes = requests.get(url, timeout=5).content
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted_bytes = cipher.decrypt(encrypted_bytes)
|
||||
final_data = unpad(decrypted_bytes, AES.block_size)
|
||||
return final_data
|
||||
|
||||
def log(self, msg):
|
||||
if self.debug:
|
||||
try:
|
||||
requests.post('http://192.168.31.12:5000/log', data=msg, timeout=1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
197
PY1/kemon.py
Normal file
197
PY1/kemon.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import sys, re, threading, json, requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
creators_cache = []; post_image_cache = {}
|
||||
img_pattern = re.compile(r'src=["\']([^"\']+)["\']')
|
||||
img_exts = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif'}
|
||||
vid_exts = {'mp4', 'mkv', 'mov', 'webm', 'm4v', 'avi', 'flv'}
|
||||
|
||||
def getName(self): return "Kemono"
|
||||
|
||||
def init(self, extend=""):
|
||||
super().init(extend)
|
||||
self.base_url = "https://kemono.cr"; self.api_url = "https://kemono.cr/api/v1"; self.img_base = "https://kemono.cr/data"
|
||||
self.session = requests.Session()
|
||||
retries = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
|
||||
adapter = HTTPAdapter(pool_connections=20, pool_maxsize=20, max_retries=retries)
|
||||
self.session.mount('https://', adapter); self.session.mount('http://', adapter)
|
||||
self.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': f"{self.base_url}/", 'Accept': 'text/css,*/*;q=0.1', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive'}
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def destroy(self):
|
||||
if hasattr(self, 'session'): self.session.close()
|
||||
|
||||
def fetch(self, url, headers=None, is_api=False):
|
||||
req_headers = headers or self.headers
|
||||
if is_api or '/api/' in url: req_headers['Accept'] = 'text/css'
|
||||
return self.session.get(url, headers=req_headers, timeout=30, verify=False)
|
||||
|
||||
def homeContent(self, filter):
|
||||
services = [("Patreon", "patreon"), ("Pixiv Fanbox", "fanbox"), ("Fantia", "fantia"), ("Discord", "discord"), ("Gumroad", "gumroad"), ("SubscribeStar", "subscribestar"), ("Boosty", "boosty"), ("DLsite", "dlsite")]
|
||||
result = [{"type_name": "Popular Recent", "type_id": "popular"}] + [{"type_name": n, "type_id": i} for n, i in services]
|
||||
if not self.creators_cache: threading.Thread(target=self._load_creators_task).start()
|
||||
return {'class': result}
|
||||
|
||||
def _load_creators_task(self):
|
||||
resp = self.fetch(f"{self.api_url}/creators", is_api=True)
|
||||
if resp and resp.status_code == 200: self.creators_cache = resp.json()
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg)
|
||||
handlers = {'popular': lambda: self._get_popular_posts(pg), 'similar###': lambda: self._get_similar_list(tid, pg), 'creator###': lambda: self._get_post_list(tid, pg), 'post###': lambda: self._handle_image_popup_safely(tid)}
|
||||
for prefix, handler in handlers.items():
|
||||
if tid.startswith(prefix): return handler()
|
||||
return self._get_creator_list(tid, pg)
|
||||
|
||||
def _get_creator_list(self, service_id, pg):
|
||||
if not self.creators_cache:
|
||||
self._load_creators_task()
|
||||
if not self.creators_cache: return {'list': [{'vod_id': 'err', 'vod_name': 'Loading...', 'vod_pic': ''}]}
|
||||
filtered = [c for c in self.creators_cache if c.get('service') == service_id]
|
||||
filtered.sort(key=lambda x: x.get('favorited', 0), reverse=True)
|
||||
limit = 20; total = len(filtered); start, end = (pg - 1) * limit, pg * limit
|
||||
style = {"type": "list", "ratio": "1.1"}; icon_prefix = f"https://img.kemono.cr/icons/{service_id}/"
|
||||
vlist = [{'vod_id': f"creator###{service_id}###{item['id']}", 'vod_name': item.get('name'), 'vod_pic': icon_prefix + item['id'], 'vod_tag': 'folder', 'vod_remarks': f"♥ {item.get('favorited', 0)}", "style": style} for item in filtered[start:end]]
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (total + limit - 1) // limit + 1, 'limit': limit, 'total': total}
|
||||
|
||||
def _get_similar_list(self, tid, pg):
|
||||
_, service, user_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/recommended", is_api=True)
|
||||
if not resp or resp.status_code != 200: return {'list': [{'vod_id': 'err', 'vod_name': 'API Error', 'vod_pic': ''}]}
|
||||
creators = resp.json()
|
||||
if not creators: return {'list': [{'vod_id': 'err', 'vod_name': 'No Similar Artists', 'vod_pic': ''}]}
|
||||
creators.sort(key=lambda x: x.get('score', 0), reverse=True)
|
||||
limit = 20; total = len(creators); start, end = (pg - 1) * limit, pg * limit
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
vlist = [{'vod_id': f"creator###{item['service']}###{item['id']}", 'vod_name': item.get('name'), 'vod_pic': f"https://img.kemono.cr/icons/{item['service']}/{item['id']}", 'vod_tag': 'folder', 'vod_remarks': f"R: {int(item.get('score', 0)*100)}", "style": style} for item in creators[start:end]]
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (total + limit - 1) // limit + 1, 'limit': limit, 'total': total}
|
||||
|
||||
def _get_post_list(self, tid, pg):
|
||||
_, service, user_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/posts?o={(pg - 1) * 50}", is_api=True)
|
||||
vlist = []
|
||||
if resp and resp.status_code == 200:
|
||||
for post in resp.json():
|
||||
files = [post.get('file', {})] + post.get('attachments', [])
|
||||
cover_path = next((f.get('path') for f in files if f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts), None)
|
||||
cover = f"https://img.kemono.cr/thumbnail/data{cover_path}" if cover_path else "https://kemono.cr/static/kemono-logo.svg"
|
||||
has_video = bool(post.get('videos')) or any(f.get('path', '').split('.')[-1].lower() in self.vid_exts for f in files)
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
vlist.append({'vod_id': f"{'video' if has_video else 'post'}###{service}###{user_id}###{post.get('id')}", 'vod_name': post.get('title', 'Untitled'), 'vod_pic': cover, 'vod_remarks': '▶ Video' if has_video else post.get('published', '')[:10], 'vod_tag': 'folder' if not has_video else '', 'style': style})
|
||||
return {'list': vlist, 'page': pg, 'pagecount': pg + 1 if len(vlist) >= 50 else pg, 'limit': 50, 'total': 99999}
|
||||
|
||||
def _get_popular_posts(self, pg):
|
||||
resp = self.fetch(f"{self.api_url}/posts/popular?period=recent&o={(pg - 1) * 50}", is_api=True)
|
||||
vlist = []; total_count = 99999
|
||||
if resp and resp.status_code == 200:
|
||||
data = resp.json(); total_count = data.get('props', {}).get('count', 99999)
|
||||
for post in data.get('posts', []):
|
||||
files = [post.get('file', {})] + post.get('attachments', [])
|
||||
cover_path = next((f.get('path') for f in files if f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts), None)
|
||||
cover = f"https://img.kemono.cr/thumbnail/data{cover_path}" if cover_path else "https://kemono.cr/static/kemono-logo.svg"
|
||||
has_video = bool(post.get('videos')) or any(f.get('path', '').split('.')[-1].lower() in self.vid_exts for f in files)
|
||||
style = {"type": "rect", "ratio": 1.33}
|
||||
vlist.append({'vod_id': f"{'video' if has_video else 'post'}###{post.get('service')}###{post.get('user')}###{post.get('id')}", 'vod_name': post.get('title', 'Untitled'), 'vod_pic': cover, 'vod_remarks': '▶ Video' if has_video else post.get('published', '')[:10], 'vod_tag': 'folder' if not has_video else '', 'style': style})
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (total_count + 49) // 50, 'limit': 50, 'total': total_count}
|
||||
|
||||
def detailContent(self, ids):
|
||||
tid = ids[0]
|
||||
if not tid.startswith('video###'): return {'list': []}
|
||||
_, service, user_id, post_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/post/{post_id}", is_api=True)
|
||||
if not resp or resp.status_code != 200: return {'list': []}
|
||||
data = resp.json(); post = data.get('post', data); play_list = []
|
||||
for v in post.get('videos', []):
|
||||
if v.get('path'):
|
||||
base = v.get('server', '') or ''; path = v.get('path', '')
|
||||
if not path.startswith('/data'): path = '/data' + path
|
||||
play_list.append(f"{v.get('name', 'Video')}${base}{path}")
|
||||
if not play_list:
|
||||
for idx, f in enumerate([post.get('file')] + post.get('attachments', [])):
|
||||
if f and f.get('path') and f['path'].split('.')[-1].lower() in self.vid_exts:
|
||||
full_url = self.img_base + f['path'] if not f['path'].startswith('http') else f['path']
|
||||
play_list.append(f"{f.get('name', f'Part {idx+1}')}${full_url}")
|
||||
files = [post.get('file', {})]
|
||||
first_img = next((f.get('path') for f in files if f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts), None)
|
||||
pic = f"https://img.kemono.cr/thumbnail/data{first_img}" if first_img else ""
|
||||
link_data = {'id': f"similar###{service}###{user_id}", 'name': '★ Similar Artists'}
|
||||
director_link = f"[a=cr:{json.dumps(link_data)}/]★ 查找相似画师 (Find Similar)[/a]"
|
||||
vod = {"vod_id": tid, "vod_name": post.get('title', 'Untitled'), "vod_pic": pic, "type_name": service, "vod_year": post.get('published', '')[:4], "vod_pubdate": post.get('published', '')[:10], "vod_area": "Kemono", "vod_remarks": f"♥ {post.get('favorited', 0)}", "vod_director": director_link, "vod_content": re.sub(r'<[^>]+>', '', post.get('content', '')).strip()[:500], "vod_play_from": "KemonoPlayer", "vod_play_url": "#".join(play_list) if play_list else ""}
|
||||
return {"list": [vod]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags): return {'parse': 0, 'url': id, 'header': {'User-Agent': self.headers['User-Agent']}}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
if not self.creators_cache: self._load_creators_task()
|
||||
pg, limit = int(pg), 20; key_lower = key.lower()
|
||||
results = [c for c in self.creators_cache if key_lower in c.get('name', '').lower()]
|
||||
results.sort(key=lambda x: x.get('favorited', 0), reverse=True)
|
||||
start, end = (pg - 1) * limit, pg * limit
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
vlist = [{'vod_id': f"creator###{item['service']}###{item['id']}", 'vod_name': item['name'], 'vod_pic': f"https://img.kemono.cr/icons/{item['service']}/{item['id']}", 'vod_tag': 'folder', 'vod_remarks': item['service'], "style": style} for item in results[start:end]]
|
||||
if pg == 1 and results:
|
||||
top_match = results[0]
|
||||
resp = self.fetch(f"{self.api_url}/{top_match['service']}/user/{top_match['id']}/recommended", is_api=True)
|
||||
if resp and resp.status_code == 200:
|
||||
rec_data = resp.json(); rec_data.sort(key=lambda x: x.get('score', 0), reverse=True)
|
||||
if rec_data:
|
||||
vlist.append({'vod_id': 'ignore', 'vod_name': '=== 相似画师 (Similar) ===', 'vod_pic': 'https://kemono.cr/static/kemono-logo.svg', 'vod_tag': '', 'vod_remarks': '', 'style': style})
|
||||
vlist.extend({'vod_id': f"creator###{item['service']}###{item['id']}", 'vod_name': item.get('name'), 'vod_pic': f"https://img.kemono.cr/icons/{item['service']}/{item['id']}", 'vod_tag': 'folder', 'vod_remarks': f"R: {int(item.get('score', 0)*100)}", "style": style} for item in rec_data)
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (len(results) + limit - 1) // limit + 1, 'limit': limit, 'total': len(results)}
|
||||
|
||||
def _handle_image_popup_safely(self, tid):
|
||||
def load_images_and_show():
|
||||
_, service, user_id, post_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/post/{post_id}", is_api=True)
|
||||
if not resp or resp.status_code != 200: return
|
||||
data = resp.json(); post = data.get('post', data); image_urls = []
|
||||
for f in [post.get('file')] + post.get('attachments', []):
|
||||
if f and f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts: image_urls.append(self.img_base + f['path'] if not f['path'].startswith('http') else f['path'])
|
||||
if post.get('content'):
|
||||
for img_url in self.img_pattern.findall(post['content']):
|
||||
if img_url.split('.')[-1].lower() in self.img_exts:
|
||||
if img_url.startswith('//'): img_url = 'https:' + img_url
|
||||
elif img_url.startswith('/'): img_url = self.base_url + img_url
|
||||
image_urls.append(img_url)
|
||||
if not image_urls: return
|
||||
cache_key = f"{service}_{user_id}_{post_id}"; self.post_image_cache[cache_key] = image_urls; self._show_popup_dialog(cache_key)
|
||||
threading.Thread(target=load_images_and_show).start()
|
||||
return {'list': [{'vod_id': 'ignore', 'vod_name': 'Gallery Loading...', 'vod_pic': '', 'vod_remarks': 'View'}], 'page': 1, 'pagecount': 1, 'limit': 1, 'total': 1}
|
||||
|
||||
def _show_popup_dialog(self, cache_key):
|
||||
def launch_popup():
|
||||
try:
|
||||
from java import jclass, dynamic_proxy
|
||||
from java.lang import Runnable
|
||||
JClass = jclass("java.lang.Class"); AT = JClass.forName("android.app.ActivityThread"); currentAT = AT.getMethod("currentActivityThread").invoke(None)
|
||||
mActivities = AT.getDeclaredField("mActivities"); mActivities.setAccessible(True); values = mActivities.get(currentAT).values()
|
||||
try: records = values.toArray()
|
||||
except: records = values.getClass().getMethod("toArray").invoke(values)
|
||||
act = None
|
||||
for record in records:
|
||||
try:
|
||||
recordClass = record.getClass(); pausedField = recordClass.getDeclaredField("paused"); pausedField.setAccessible(True)
|
||||
if not pausedField.getBoolean(record): activityField = recordClass.getDeclaredField("activity"); activityField.setAccessible(True); act = activityField.get(record); break
|
||||
except: continue
|
||||
if not act: return
|
||||
class UiRunner(dynamic_proxy(Runnable)):
|
||||
def __init__(self, func): super().__init__(); self.func = func
|
||||
def run(self):
|
||||
try: self.func()
|
||||
except: pass
|
||||
def show_dialog():
|
||||
try:
|
||||
Dialog = jclass("android.app.Dialog"); WebView = jclass("android.webkit.WebView"); ColorDrawable = jclass("android.graphics.drawable.ColorDrawable"); Color = jclass("android.graphics.Color")
|
||||
d = Dialog(act); d.requestWindowFeature(1); win = d.getWindow(); win.getDecorView().setPadding(0, 0, 0, 0); win.setBackgroundDrawable(ColorDrawable(Color.BLACK)); win.setLayout(-1, -1)
|
||||
w = WebView(act); ws = w.getSettings(); ws.setJavaScriptEnabled(True); ws.setDomStorageEnabled(True); w.setBackgroundColor(Color.BLACK)
|
||||
images = self.post_image_cache.get(cache_key, [])
|
||||
img_tags = [f'<div class="image-item"><img src="{url}" loading="lazy" referrerpolicy="no-referrer"></div>' for url in images]
|
||||
html = f"""<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="referrer" content="no-referrer"><style>body{{margin:0;padding:5px;background:#121212}}.image-grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(100%,1fr));gap:5px}}.image-item{{background:#222;border-radius:4px;overflow:hidden}}.image-item img{{width:100%;height:auto;display:block}}</style></head><body><div class="image-grid">{"".join(img_tags)}</div></body></html>"""
|
||||
w.loadDataWithBaseURL(None, html, "text/html", "utf-8", None); d.setContentView(w); d.show()
|
||||
except: pass
|
||||
act.getWindow().getDecorView().post(UiRunner(show_dialog))
|
||||
except: pass
|
||||
threading.Thread(target=launch_popup).start()
|
||||
246
PY1/kemono优化.py
Normal file
246
PY1/kemono优化.py
Normal file
@@ -0,0 +1,246 @@
|
||||
# coding: utf-8
|
||||
import sys, re, threading, socket, urllib.parse, http.server, socketserver
|
||||
import requests
|
||||
from base.spider import Spider
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
try:
|
||||
from requests.packages import urllib3
|
||||
urllib3.disable_warnings()
|
||||
except: pass
|
||||
|
||||
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
class SlideshowHandler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args): pass
|
||||
def log_error(self, format, *args): pass
|
||||
def do_GET(self):
|
||||
spider = getattr(self.server, 'spider', None)
|
||||
if not spider: return
|
||||
try:
|
||||
path_info = urllib.parse.urlparse(self.path)
|
||||
path, query = path_info.path, urllib.parse.parse_qs(path_info.query)
|
||||
if path == '/gallery.html': self._handle_gallery(spider, query)
|
||||
elif path == '/image': self._handle_image(spider, query)
|
||||
else: self.send_error(404)
|
||||
except: self.send_error(500)
|
||||
def _handle_gallery(self, spider, query):
|
||||
try:
|
||||
cache_key = query.get('id', [''])[0]
|
||||
images = spider.post_image_cache.get(cache_key, [])
|
||||
if not images:
|
||||
self.send_response(200); self.end_headers()
|
||||
self.wfile.write(b"<h2 style='color:#ccc;text-align:center;margin-top:20%'>Loading...</h2>")
|
||||
return
|
||||
img_tags = [f'<div class="image-item"><img src="{url}" loading="lazy" referrerpolicy="no-referrer"></div>' for url in images]
|
||||
html = f"""<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{{margin:0;padding:5px;background:#121212}}.image-grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(100%,1fr));gap:5px}}.image-item{{background:#222;border-radius:4px;overflow:hidden}}.image-item img{{width:100%;height:auto;display:block}}</style></head><body><div class="image-grid">{"".join(img_tags)}</div></body></html>"""
|
||||
self.send_response(200); self.send_header('Content-type', 'text/html; charset=utf-8'); self.end_headers()
|
||||
self.wfile.write(html.encode('utf-8'))
|
||||
except: pass
|
||||
def _handle_image(self, spider, query):
|
||||
try:
|
||||
target = query.get('url', [''])[0]
|
||||
if not target: return self.send_error(400)
|
||||
headers = {'User-Agent': spider.headers['User-Agent']}
|
||||
with requests.get(target, headers=headers, proxies=spider.proxies, stream=True, verify=False, timeout=30) as resp:
|
||||
if resp.status_code == 200:
|
||||
self.send_response(resp.status_code)
|
||||
self.send_header('Content-Type', resp.headers.get('Content-Type', 'image/jpeg'))
|
||||
self.send_header('Cache-Control', 'max-age=86400')
|
||||
self.end_headers()
|
||||
for chunk in resp.iter_content(chunk_size=65536):
|
||||
if chunk: self.wfile.write(chunk)
|
||||
else: self.send_error(resp.status_code)
|
||||
except: self.send_error(404)
|
||||
|
||||
class Spider(Spider):
|
||||
server = None
|
||||
server_port = 0
|
||||
creators_cache = []
|
||||
post_image_cache = {}
|
||||
img_pattern = re.compile(r'src=["\']([^"\']+)["\']')
|
||||
img_exts = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif'}
|
||||
vid_exts = {'mp4', 'mkv', 'mov', 'webm', 'm4v', 'avi', 'flv'}
|
||||
def getName(self): return "Kemono"
|
||||
def init(self, extend=""):
|
||||
try:
|
||||
super().init(extend)
|
||||
self.base_url = "https://kemono.cr"
|
||||
self.api_url = "https://kemono.cr/api/v1"
|
||||
self.img_base = "https://kemono.cr/data"
|
||||
self.proxies = {'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890'}
|
||||
self.session = requests.Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
retries = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
|
||||
adapter = HTTPAdapter(pool_connections=20, pool_maxsize=20, max_retries=retries)
|
||||
self.session.mount('https://', adapter); self.session.mount('http://', adapter)
|
||||
self.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': f"{self.base_url}/", 'Accept': 'text/css,*/*;q=0.1', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive'}
|
||||
self.session.headers.update(self.headers)
|
||||
self._start_server()
|
||||
except: pass
|
||||
def destroy(self):
|
||||
if self.server:
|
||||
try: self.server.shutdown(); self.server.server_close()
|
||||
except: pass
|
||||
self.server = None
|
||||
if hasattr(self, 'session'): self.session.close()
|
||||
def _start_server(self):
|
||||
if self.server: return
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(('0.0.0.0', 0)); self.server_port = sock.getsockname()[1]; sock.close()
|
||||
self.server = ThreadedHTTPServer(('0.0.0.0', self.server_port), SlideshowHandler)
|
||||
self.server.spider = self
|
||||
t = threading.Thread(target=self.server.serve_forever); t.daemon = True; t.start()
|
||||
except: pass
|
||||
def fetch(self, url, headers=None, is_api=False):
|
||||
try:
|
||||
req_headers = headers or self.headers
|
||||
if is_api or '/api/' in url: req_headers['Accept'] = 'text/css'
|
||||
return self.session.get(url, headers=req_headers, timeout=30, verify=False)
|
||||
except: return None
|
||||
def _proxify_url(self, target):
|
||||
if not target: return ""
|
||||
if target.startswith('//'): target = 'https:' + target
|
||||
return f"http://127.0.0.1:{self.server_port}/image?url={urllib.parse.quote(target)}"
|
||||
def homeContent(self, filter):
|
||||
services = [{"type_name": n, "type_id": i} for n, i in [("Patreon", "patreon"), ("Pixiv Fanbox", "fanbox"), ("Fantia", "fantia"), ("Discord", "discord"), ("Gumroad", "gumroad"), ("SubscribeStar", "subscribestar"), ("Afdian", "afdian"), ("Boosty", "boosty"), ("DLsite", "dlsite")]]
|
||||
if not self.creators_cache: threading.Thread(target=self._load_creators_task).start()
|
||||
return {'class': services}
|
||||
def _load_creators_task(self):
|
||||
try:
|
||||
resp = self.fetch(f"{self.api_url}/creators", is_api=True)
|
||||
if resp and resp.status_code == 200: self.creators_cache = resp.json()
|
||||
except: pass
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg)
|
||||
if tid.startswith('post###'): return self._handle_image_popup_safely(tid)
|
||||
elif tid.startswith('creator###'): return self._get_post_list(tid, pg)
|
||||
else: return self._get_creator_list(tid, pg)
|
||||
def _get_creator_list(self, service_id, pg):
|
||||
limit = 20
|
||||
if not self.creators_cache:
|
||||
self._load_creators_task()
|
||||
if not self.creators_cache: return {'list': [{'vod_id': 'err', 'vod_name': 'Loading...', 'vod_pic': ''}]}
|
||||
filtered = [c for c in self.creators_cache if c.get('service') == service_id]
|
||||
filtered.sort(key=lambda x: x.get('favorited', 0), reverse=True)
|
||||
total = len(filtered); start, end = (pg - 1) * limit, pg * limit
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
icon_prefix = f"https://img.kemono.cr/icons/{service_id}/"
|
||||
vlist = [{'vod_id': f"creator###{service_id}###{item['id']}", 'vod_name': item.get('name'), 'vod_pic': self._proxify_url(icon_prefix + item['id']), 'vod_tag': 'folder', 'vod_remarks': f"♥ {item.get('favorited', 0)}", "style": style} for item in filtered[start:end]]
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (total + limit - 1) // limit + 1, 'limit': limit, 'total': total}
|
||||
def _get_post_list(self, tid, pg):
|
||||
try:
|
||||
_, service, user_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/posts?o={(pg - 1) * 50}", is_api=True)
|
||||
vlist = []
|
||||
if resp and resp.status_code == 200:
|
||||
for post in resp.json():
|
||||
files = [post.get('file', {})] + post.get('attachments', [])
|
||||
cover_path = next((f.get('path') for f in files if f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts), None)
|
||||
cover = f"https://img.kemono.cr/thumbnail/data{cover_path}" if cover_path else "https://kemono.cr/static/kemono-logo.svg"
|
||||
has_video = bool(post.get('videos'))
|
||||
if not has_video: has_video = any(f.get('path', '').split('.')[-1].lower() in self.vid_exts for f in files)
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
vlist.append({'vod_id': f"{'video' if has_video else 'post'}###{service}###{user_id}###{post.get('id')}", 'vod_name': post.get('title', 'Untitled'), 'vod_pic': self._proxify_url(cover), 'vod_remarks': '▶ Video' if has_video else post.get('published', '')[:10], 'vod_tag': 'folder' if not has_video else '', 'style': style})
|
||||
return {'list': vlist, 'page': pg, 'pagecount': pg + 1 if len(vlist) >= 50 else pg, 'limit': 50, 'total': 99999}
|
||||
except: return {'list': []}
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
tid = ids[0]
|
||||
if not tid.startswith('video###'): return {'list': []}
|
||||
_, service, user_id, post_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/post/{post_id}", is_api=True)
|
||||
if not resp or resp.status_code != 200: return {'list': []}
|
||||
data = resp.json(); post = data.get('post', data); play_list = []
|
||||
if post.get('videos'):
|
||||
for v in post['videos']:
|
||||
if v.get('path'):
|
||||
base = v.get('server', '') or ''; path = v.get('path', '')
|
||||
if not path.startswith('/data'): path = '/data' + path
|
||||
play_list.append(f"{v.get('name', 'Video')}${base}{path}")
|
||||
if not play_list:
|
||||
for idx, f in enumerate([post.get('file')] + post.get('attachments', [])):
|
||||
if f and f.get('path') and f['path'].split('.')[-1].lower() in self.vid_exts:
|
||||
full_url = self.img_base + f['path'] if not f['path'].startswith('http') else f['path']
|
||||
play_list.append(f"{f.get('name', f'Part {idx+1}')}${full_url}")
|
||||
files = [post.get('file', {})]
|
||||
first_img = next((f.get('path') for f in files if f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts), None)
|
||||
pic = self._proxify_url(f"https://img.kemono.cr/thumbnail/data{first_img}") if first_img else ""
|
||||
vod = {"vod_id": tid, "vod_name": post.get('title', 'Untitled'), "vod_pic": pic, "type_name": service, "vod_year": post.get('published', '')[:4], "vod_pubdate": post.get('published', '')[:10], "vod_area": "Kemono", "vod_remarks": f"♥ {post.get('favorited', 0)}", "vod_content": re.sub(r'<[^>]+>', '', post.get('content', '')).strip()[:500], "vod_play_from": "KemonoPlayer", "vod_play_url": "#".join(play_list) if play_list else "No Video$http://127.0.0.1/"}
|
||||
return {"list": [vod]}
|
||||
except: return {'list': []}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'parse': 0, 'url': id, 'header': {'User-Agent': self.headers['User-Agent']}}
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
if not self.creators_cache: self._load_creators_task()
|
||||
pg, limit = int(pg), 20; key_lower = key.lower()
|
||||
results = [c for c in self.creators_cache if key_lower in c.get('name', '').lower()]
|
||||
results.sort(key=lambda x: x.get('favorited', 0), reverse=True)
|
||||
start, end = (pg - 1) * limit, pg * limit
|
||||
style = {"type": "list", "ratio": "1.1"}
|
||||
vlist = [{'vod_id': f"creator###{item['service']}###{item['id']}", 'vod_name': item['name'], 'vod_pic': self._proxify_url(f"https://img.kemono.cr/icons/{item['service']}/{item['id']}"), 'vod_tag': 'folder', 'vod_remarks': item['service'], "style": style} for item in results[start:end]]
|
||||
return {'list': vlist, 'page': pg, 'pagecount': (len(results) + limit - 1) // limit + 1, 'limit': limit, 'total': len(results)}
|
||||
def _handle_image_popup_safely(self, tid):
|
||||
def load_images_and_show():
|
||||
try:
|
||||
_, service, user_id, post_id = tid.split('###')
|
||||
resp = self.fetch(f"{self.api_url}/{service}/user/{user_id}/post/{post_id}", is_api=True)
|
||||
if not resp or resp.status_code != 200: return
|
||||
data = resp.json(); post = data.get('post', data); image_urls = []
|
||||
for f in [post.get('file')] + post.get('attachments', []):
|
||||
if f and f.get('path') and f['path'].split('.')[-1].lower() in self.img_exts:
|
||||
image_urls.append(self.img_base + f['path'] if not f['path'].startswith('http') else f['path'])
|
||||
if post.get('content'):
|
||||
for img_url in self.img_pattern.findall(post['content']):
|
||||
if img_url.split('.')[-1].lower() in self.img_exts:
|
||||
if img_url.startswith('//'): img_url = 'https:' + img_url
|
||||
elif img_url.startswith('/'): img_url = self.base_url + img_url
|
||||
image_urls.append(img_url)
|
||||
if not image_urls: return
|
||||
seen = set(); proxied_urls = []
|
||||
for u in image_urls:
|
||||
if u not in seen: proxied_urls.append(self._proxify_url(u)); seen.add(u)
|
||||
cache_key = f"{service}_{user_id}_{post_id}"
|
||||
self.post_image_cache[cache_key] = proxied_urls
|
||||
self._show_popup_dialog(cache_key)
|
||||
except: pass
|
||||
threading.Thread(target=load_images_and_show).start()
|
||||
return {'list': [{'vod_id': 'ignore', 'vod_name': 'Gallery Loading...', 'vod_pic': '', 'vod_remarks': 'View'}], 'page': 1, 'pagecount': 1, 'limit': 1, 'total': 1}
|
||||
def _show_popup_dialog(self, cache_key):
|
||||
def launch_popup():
|
||||
try:
|
||||
from java import jclass, dynamic_proxy
|
||||
from java.lang import Runnable
|
||||
JClass = jclass("java.lang.Class")
|
||||
AT = JClass.forName("android.app.ActivityThread")
|
||||
currentAT = AT.getMethod("currentActivityThread").invoke(None)
|
||||
mActivities = AT.getDeclaredField("mActivities"); mActivities.setAccessible(True)
|
||||
values = mActivities.get(currentAT).values()
|
||||
try: records = values.toArray()
|
||||
except: records = values.getClass().getMethod("toArray").invoke(values)
|
||||
act = None
|
||||
for record in records:
|
||||
try:
|
||||
recordClass = record.getClass(); pausedField = recordClass.getDeclaredField("paused"); pausedField.setAccessible(True)
|
||||
if not pausedField.getBoolean(record):
|
||||
activityField = recordClass.getDeclaredField("activity"); activityField.setAccessible(True); act = activityField.get(record); break
|
||||
except: continue
|
||||
if not act: return
|
||||
class UiRunner(dynamic_proxy(Runnable)):
|
||||
def __init__(self, func): super().__init__(); self.func = func
|
||||
def run(self):
|
||||
try: self.func()
|
||||
except: pass
|
||||
def show_dialog():
|
||||
try:
|
||||
Dialog = jclass("android.app.Dialog"); WebView = jclass("android.webkit.WebView"); ColorDrawable = jclass("android.graphics.drawable.ColorDrawable"); Color = jclass("android.graphics.Color")
|
||||
d = Dialog(act); d.requestWindowFeature(1); win = d.getWindow(); win.getDecorView().setPadding(0, 0, 0, 0); win.setBackgroundDrawable(ColorDrawable(Color.BLACK)); win.setLayout(-1, -1)
|
||||
w = WebView(act); ws = w.getSettings(); ws.setJavaScriptEnabled(True); ws.setDomStorageEnabled(True); w.setBackgroundColor(Color.BLACK)
|
||||
w.loadUrl(f"http://127.0.0.1:{self.server_port}/gallery.html?id={cache_key}"); d.setContentView(w); d.show()
|
||||
except: pass
|
||||
act.getWindow().getDecorView().post(UiRunner(show_dialog))
|
||||
except: pass
|
||||
threading.Thread(target=launch_popup).start()
|
||||
212
PY1/lavAPP.py
Normal file
212
PY1/lavAPP.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
from base64 import b64encode, b64decode
|
||||
from Crypto.Hash import MD5, SHA256
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "lav"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.id = self.ms(str(int(time.time() * 1000)))[:16]
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "http://sir_new.tiansexyl.tv"
|
||||
t = str(int(time.time() * 1000))
|
||||
headers = {'User-Agent': 'okhttp-okgo/jeasonlzy', 'Connection': 'Keep-Alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
def homeContent(self, filter):
|
||||
cateManual = {"演员": "actor", "分类": "avsearch", }
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({'type_name': k, 'type_id': cateManual[k]})
|
||||
j = {'code': 'homePage', 'mod': 'down', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id}
|
||||
|
||||
body = self.aes(j)
|
||||
data = self.post(f'{self.host}/api.php?t={str(int(time.time() * 1000))}', data=body, headers=self.headers).json()['data']
|
||||
data1 = self.aes(data, False)['data']
|
||||
self.r = data1['r']
|
||||
for i, d in enumerate(data1['avTag']):
|
||||
# if i == 4:
|
||||
# break
|
||||
classes.append({'type_name': d['name'], 'type_id': d['tag']})
|
||||
resutl = {}
|
||||
resutl["class"] = classes
|
||||
return resutl
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
id = tid.split("@@")
|
||||
result = {}
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
if id[0] == 'avsearch':
|
||||
if pg == '1':
|
||||
j = {'code': 'avsearch', 'mod': 'search', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id}
|
||||
if len(id) > 1:
|
||||
j = {'code': 'find', 'mod': 'tag', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id, 'type': 'av', 'dis': 'new', 'page': str(pg), 'tag': id[1]}
|
||||
elif id[0] == 'actor':
|
||||
j = {'mod': 'actor', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv', 'app_type': 'rn',
|
||||
'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn', 'oauth_id': self.id,
|
||||
'page': str(pg), 'filter': ''}
|
||||
if len(id) > 1:
|
||||
j = {'code': 'eq', 'mod': 'actor', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id, 'page': str(pg), 'id': id[1], 'actor': id[2]}
|
||||
else:
|
||||
j = {'code': 'search', 'mod': 'av', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id, 'page': str(pg), 'tag': id[0]}
|
||||
|
||||
body = self.aes(j)
|
||||
data = self.post(f'{self.host}/api.php?t={str(int(time.time() * 1000))}', data=body, headers=self.headers).json()['data']
|
||||
data1 = self.aes(data, False)['data']
|
||||
videos = []
|
||||
if tid == 'avsearch' and len(id) == 1:
|
||||
for item in data1:
|
||||
videos.append({"vod_id": id[0] + "@@" + str(item.get('tags')), 'vod_name': item.get('name'),
|
||||
'vod_pic': self.imgs(item.get('ico')), 'vod_tag': 'folder',
|
||||
'style': {"type": "rect", "ratio": 1.33}})
|
||||
elif tid == 'actor' and len(id) == 1:
|
||||
for item in data1:
|
||||
videos.append({"vod_id": id[0] + "@@" + str(item.get('id')) + "@@" + item.get('name'),
|
||||
'vod_name': item.get('name'), 'vod_pic': self.imgs(item.get('cover')),
|
||||
'vod_tag': 'folder', 'style': {"type": "oval"}})
|
||||
else:
|
||||
for item in data1:
|
||||
if item.get('_id'):
|
||||
videos.append({"vod_id": str(item.get('id')), 'vod_name': item.get('title'),
|
||||
'vod_pic': self.imgs(item.get('cover_thumb') or item.get('cover_full')),
|
||||
'vod_remarks': item.get('good'), 'style': {"type": "rect", "ratio": 1.33}})
|
||||
result["list"] = videos
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
id = ids[0]
|
||||
j = {'code': 'detail', 'mod': 'av', 'channel': 'self', 'via': 'agent', 'bundleId': 'com.tvlutv',
|
||||
'app_type': 'rn', 'os_version': '12.0.5', 'version': '3.2.3', 'oauth_type': 'android_rn',
|
||||
'oauth_id': self.id, 'id': id}
|
||||
body = self.aes(j)
|
||||
data = self.post(f'{self.host}/api.php?t={str(int(time.time() * 1000))}', data=body, headers=self.headers).json()['data']
|
||||
data1 = self.aes(data, False)['line']
|
||||
vod = {}
|
||||
play = []
|
||||
for itt in data1:
|
||||
a = itt['line'].get('s720')
|
||||
if a:
|
||||
b = a.split('.')
|
||||
b[0] = 'https://m3u8'
|
||||
a = '.'.join(b)
|
||||
play.append(itt['info']['tips'] + "$" + a)
|
||||
break
|
||||
vod["vod_play_from"] = 'LAV'
|
||||
vod["vod_play_url"] = "#".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pass
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = self.getProxyUrl() + "&url=" + b64encode(id.encode('utf-8')).decode('utf-8') + "&type=m3u8"
|
||||
self.hh = {'User-Agent': 'dd', 'Connection': 'Keep-Alive', 'Referer': self.r}
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["url"] = url
|
||||
result["header"] = self.hh
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = param["url"]
|
||||
if param.get('type') == "m3u8":
|
||||
return self.vod(b64decode(url).decode('utf-8'))
|
||||
else:
|
||||
return self.img(url)
|
||||
|
||||
def vod(self, url):
|
||||
data = self.fetch(url, headers=self.hh).text
|
||||
key = bytes.fromhex("13d47399bda541b85e55830528d4e66f1791585b2d2216f23215c4c63ebace31")
|
||||
iv = bytes.fromhex(data[:32])
|
||||
data = data[32:]
|
||||
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
|
||||
data_bytes = bytes.fromhex(data)
|
||||
decrypted = cipher.decrypt(data_bytes)
|
||||
encoded = decrypted.decode("utf-8").replace("\x08", "")
|
||||
return [200, "application/vnd.apple.mpegur", encoded]
|
||||
|
||||
def imgs(self, url):
|
||||
return self.getProxyUrl() + '&url=' + url
|
||||
|
||||
def img(self, url):
|
||||
type = url.split('.')[-1]
|
||||
data = self.fetch(url).text
|
||||
key = bytes.fromhex("ba78f184208d775e1553550f2037f4af22cdcf1d263a65b4d5c74536f084a4b2")
|
||||
iv = bytes.fromhex(data[:32])
|
||||
data = data[32:]
|
||||
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
|
||||
data_bytes = bytes.fromhex(data)
|
||||
decrypted = cipher.decrypt(data_bytes)
|
||||
return [200, f"image/{type}", decrypted]
|
||||
|
||||
def ms(self, data, m=False):
|
||||
h = MD5.new()
|
||||
if m:
|
||||
h = SHA256.new()
|
||||
h.update(data.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def aes(self, data, operation=True):
|
||||
key = bytes.fromhex("620f15cfdb5c79c34b3940537b21eda072e22f5d7151456dec3932d7a2b22c53")
|
||||
t = str(int(time.time()))
|
||||
ivt = self.ms(t)
|
||||
if operation:
|
||||
data = json.dumps(data, separators=(',', ':'))
|
||||
iv = bytes.fromhex(ivt)
|
||||
else:
|
||||
iv = bytes.fromhex(data[:32])
|
||||
data = data[32:]
|
||||
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
|
||||
if operation:
|
||||
data_bytes = data.encode('utf-8')
|
||||
encrypted = cipher.encrypt(data_bytes)
|
||||
ep = f'{ivt}{encrypted.hex()}'
|
||||
edata = f"data={ep}×tamp={t}0d27dfacef1338483561a46b246bf36d"
|
||||
sign = self.ms(self.ms(edata, True))
|
||||
edata = f"timestamp={t}&data={ep}&sign={sign}"
|
||||
return edata
|
||||
else:
|
||||
data_bytes = bytes.fromhex(data)
|
||||
decrypted = cipher.decrypt(data_bytes)
|
||||
return json.loads(decrypted.decode('utf-8'))
|
||||
|
||||
1196
PY1/leaflow_checkin.py
Normal file
1196
PY1/leaflow_checkin.py
Normal file
File diff suppressed because it is too large
Load Diff
362
PY1/onlyfans gv.py
Normal file
362
PY1/onlyfans gv.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#author:嗷呜群fans&claude4⚡
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
from base64 import b64encode
|
||||
from urllib.parse import urljoin, urlencode
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
try:
|
||||
self.proxies = json.loads(extend) if extend else {}
|
||||
except:
|
||||
self.proxies = {}
|
||||
|
||||
if isinstance(self.proxies, dict) and 'proxy' in self.proxies and isinstance(self.proxies['proxy'], dict):
|
||||
self.proxies = self.proxies['proxy']
|
||||
|
||||
fixed = {}
|
||||
for k, v in (self.proxies or {}).items():
|
||||
if isinstance(v, str) and not v.startswith('http'):
|
||||
fixed[k] = f'http://{v}'
|
||||
else:
|
||||
fixed[k] = v
|
||||
self.proxies = fixed
|
||||
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.3,en;q=0.2',
|
||||
'Referer': 'https://gayvidsclub.com/',
|
||||
'Origin': 'https://gayvidsclub.com',
|
||||
}
|
||||
|
||||
self.host = "https://gayvidsclub.com"
|
||||
self.session = Session()
|
||||
self.session.proxies.update(self.proxies)
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def getName(self):
|
||||
return "GayVidsClub"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return '.m3u8' in url or '.mp4' in url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"最新": "/all-gay-porn/",
|
||||
"COAT": "/all-gay-porn/coat/",
|
||||
"MEN'S RUSH.TV": "/all-gay-porn/mens-rush-tv/",
|
||||
"HUNK CHANNEL": "/all-gay-porn/hunk-channel/",
|
||||
"KO": "/all-gay-porn/ko/",
|
||||
"EXFEED": "/all-gay-porn/exfeed/",
|
||||
"BRAVO!": "/all-gay-porn/bravo/",
|
||||
"STR8BOYS": "/all-gay-porn/str8boys/",
|
||||
"G-BOT": "/all-gay-porn/g-bot/"
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.fetchPage("/")
|
||||
vlist = self.getlist(data("article"))
|
||||
if not vlist:
|
||||
data = self.fetchPage('/all-gay-porn/')
|
||||
vlist = self.getlist(data("article"))
|
||||
return {'list': vlist}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
|
||||
if pg == 1:
|
||||
url = tid
|
||||
else:
|
||||
url = f"{tid}page/{pg}/"
|
||||
|
||||
data = self.fetchPage(url)
|
||||
vdata = self.getlist(data("article"))
|
||||
|
||||
result['list'] = vdata
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.fetchPage(ids[0])
|
||||
|
||||
title = data('h1').text().strip()
|
||||
|
||||
iframe_src = None
|
||||
iframe_elem = data('iframe')
|
||||
if iframe_elem:
|
||||
iframe_src = iframe_elem.attr('src')
|
||||
|
||||
if not iframe_src:
|
||||
scripts = data('script')
|
||||
for script in scripts.items():
|
||||
script_text = script.text()
|
||||
if 'iframe' in script_text and 'src' in script_text:
|
||||
matches = re.findall(r'iframe.*?src=[\'"](https?://[^\'"]+)[\'"]', script_text)
|
||||
if matches:
|
||||
iframe_src = matches[0]
|
||||
break
|
||||
|
||||
# 获取海报图片 - 确保使用横版图片
|
||||
vod_pic = ""
|
||||
img_elem = data('img')
|
||||
if img_elem:
|
||||
vod_pic = img_elem.attr('src')
|
||||
# 确保使用横版海报图
|
||||
if vod_pic and ('poster' in vod_pic or 'cover' in vod_pic):
|
||||
# 已经是横版图片,不做处理
|
||||
pass
|
||||
elif vod_pic:
|
||||
# 尝试转换为横版图片
|
||||
vod_pic = self.ensure_horizontal_poster(vod_pic)
|
||||
|
||||
vod = {
|
||||
'vod_name': title,
|
||||
'vod_content': 'GayVidsClub视频',
|
||||
'vod_tag': 'GayVidsClub',
|
||||
'vod_pic': vod_pic, # 添加海报图片
|
||||
'vod_play_from': 'GayVidsClub',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
play_lines = []
|
||||
|
||||
if iframe_src:
|
||||
if not iframe_src.startswith('http'):
|
||||
iframe_src = urljoin(self.host, iframe_src)
|
||||
play_lines.append(f"直连${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"嗅探${self.e64(ids[0])}")
|
||||
|
||||
if iframe_src:
|
||||
play_lines.append(f"阿里云盘解析${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"夸克网盘解析${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"115网盘解析${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"迅雷解析${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"PikPak解析${self.e64(iframe_src)}")
|
||||
|
||||
play_lines.append(f"手机推送${iframe_src}")
|
||||
else:
|
||||
fallback_url = ids[0]
|
||||
play_lines.append(f"阿里云盘解析${self.e64(fallback_url)}")
|
||||
play_lines.append(f"夸克网盘解析${self.e64(fallback_url)}")
|
||||
play_lines.append(f"115网盘解析${self.e64(fallback_url)}")
|
||||
play_lines.append(f"迅雷解析${self.e64(fallback_url)}")
|
||||
play_lines.append(f"PikPak解析${self.e64(fallback_url)}")
|
||||
play_lines.append(f"手机推送${fallback_url}")
|
||||
|
||||
vod['vod_play_url'] = '#'.join(play_lines)
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
if pg == 1:
|
||||
url = f"/?s={key}"
|
||||
else:
|
||||
url = f"/page/{pg}/?s={key}"
|
||||
|
||||
data = self.fetchPage(url)
|
||||
return {'list': self.getlist(data("article")), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = self.d64(id)
|
||||
|
||||
if "直连" in flag:
|
||||
return {'parse': 0, 'url': url, 'header': self.headers}
|
||||
elif "嗅探" in flag:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
elif "阿里云盘解析" in flag:
|
||||
return self.parse_with_aliyun(url)
|
||||
elif "夸克网盘解析" in flag:
|
||||
return self.parse_with_quark(url)
|
||||
elif "115网盘解析" in flag:
|
||||
return self.parse_with_115(url)
|
||||
elif "迅雷解析" in flag:
|
||||
return self.parse_with_thunder(url)
|
||||
elif "PikPak解析" in flag:
|
||||
return self.parse_with_pikpak(url)
|
||||
elif "手机推送" in flag:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
else:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
|
||||
def fetchPage(self, url):
|
||||
if not url.startswith('http'):
|
||||
url = urljoin(self.host, url)
|
||||
response = self.session.get(url)
|
||||
return pq(response.text)
|
||||
|
||||
def getlist(self, items):
|
||||
vlist = []
|
||||
for item in items.items():
|
||||
vid = item.find('a').attr('href')
|
||||
img = item.find('img').attr('src')
|
||||
name = item.find('h2').text()
|
||||
if not name:
|
||||
name = item.find('h3').text()
|
||||
|
||||
# 确保使用横版海报图
|
||||
if img:
|
||||
if '?' in img:
|
||||
img = img.split('?')[0]
|
||||
# 确保使用横版图片
|
||||
img = self.ensure_horizontal_poster(img)
|
||||
|
||||
vlist.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': name,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': '',
|
||||
'style': {'type': 'rect', 'ratio': 1.33} # 添加横版样式
|
||||
})
|
||||
return vlist
|
||||
|
||||
def ensure_horizontal_poster(self, img_url):
|
||||
"""
|
||||
确保使用横版海报图片
|
||||
"""
|
||||
if not img_url:
|
||||
return img_url
|
||||
|
||||
# 如果已经是横版图片,直接返回
|
||||
if 'poster' in img_url or 'cover' in img_url:
|
||||
return img_url
|
||||
|
||||
# 尝试转换为横版图片
|
||||
# 常见的竖版图片标识
|
||||
vertical_indicators = ['thumb', 'vertical', 'portrait', 'square']
|
||||
|
||||
# 常见的横版图片标识
|
||||
horizontal_indicators = ['poster', 'cover', 'horizontal', 'landscape']
|
||||
|
||||
# 检查是否是竖版图片
|
||||
is_vertical = any(indicator in img_url for indicator in vertical_indicators)
|
||||
|
||||
if is_vertical:
|
||||
# 尝试转换为横版图片
|
||||
for v_indicator in vertical_indicators:
|
||||
for h_indicator in horizontal_indicators:
|
||||
if v_indicator in img_url:
|
||||
# 替换竖版标识为横版标识
|
||||
new_url = img_url.replace(v_indicator, h_indicator)
|
||||
# 检查新URL是否有效
|
||||
try:
|
||||
response = self.session.head(new_url, timeout=3)
|
||||
if response.status_code == 200:
|
||||
return new_url
|
||||
except:
|
||||
continue
|
||||
|
||||
# 如果无法转换,尝试添加横版参数
|
||||
if '?' in img_url:
|
||||
new_url = img_url + '&type=horizontal'
|
||||
else:
|
||||
new_url = img_url + '?type=horizontal'
|
||||
|
||||
return new_url
|
||||
|
||||
return img_url
|
||||
|
||||
def e64(self, data):
|
||||
return b64encode(data.encode()).decode()
|
||||
|
||||
def d64(self, data):
|
||||
from base64 import b64decode
|
||||
return b64decode(data).decode()
|
||||
|
||||
def parse_with_aliyun(self, url):
|
||||
try:
|
||||
parse_result = {
|
||||
'parse': 1,
|
||||
'url': url,
|
||||
'header': self.headers,
|
||||
'parse_type': 'aliyun',
|
||||
'message': '使用阿里云盘解析服务'
|
||||
}
|
||||
return parse_result
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
|
||||
def parse_with_quark(self, url):
|
||||
try:
|
||||
parse_result = {
|
||||
'parse': 1,
|
||||
'url': url,
|
||||
'header': self.headers,
|
||||
'parse_type': 'quark',
|
||||
'message': '使用夸克网盘解析服务'
|
||||
}
|
||||
return parse_result
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
|
||||
def parse_with_115(self, url):
|
||||
try:
|
||||
parse_result = {
|
||||
'parse': 1,
|
||||
'url': url,
|
||||
'header': self.headers,
|
||||
'parse_type': '115',
|
||||
'message': '使用115网盘解析服务'
|
||||
}
|
||||
return parse_result
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
|
||||
def parse_with_thunder(self, url):
|
||||
try:
|
||||
parse_result = {
|
||||
'parse': 1,
|
||||
'url': url,
|
||||
'header': self.headers,
|
||||
'parse_type': 'thunder',
|
||||
'message': '使用迅雷解析服务'
|
||||
}
|
||||
return parse_result
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
|
||||
def parse_with_pikpak(self, url):
|
||||
try:
|
||||
parse_result = {
|
||||
'parse': 1,
|
||||
'url': url,
|
||||
'header': self.headers,
|
||||
'parse_type': 'pikpak',
|
||||
'message': '使用PikPak解析服务'
|
||||
}
|
||||
return parse_result
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': url, 'header': self.headers}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user