Sync all projects
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import hashlib
|
||||
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 as BaseSpider
|
||||
|
||||
img_cache = {}
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
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/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',
|
||||
}
|
||||
self.host = self.get_working_host()
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
print(f"使用站点: {self.host}")
|
||||
|
||||
def getName(self):
|
||||
return "🌈 91吃瓜中心|终极完美版"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
global img_cache
|
||||
img_cache.clear()
|
||||
|
||||
def get_working_host(self):
|
||||
dynamic_urls = [
|
||||
'https://but.vncchqw.cc/'
|
||||
]
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return url
|
||||
except Exception:
|
||||
continue
|
||||
return dynamic_urls[0]
|
||||
|
||||
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)
|
||||
|
||||
classes = []
|
||||
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()
|
||||
if not href or href == '#' or not name: continue
|
||||
classes.append({'type_name': name, 'type_id': href})
|
||||
if classes: break
|
||||
|
||||
if not classes:
|
||||
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
|
||||
|
||||
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
|
||||
except Exception as 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, article'))}
|
||||
except Exception as e:
|
||||
return {'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
|
||||
|
||||
if tid.startswith('http'):
|
||||
base_url = tid.rstrip('/')
|
||||
else:
|
||||
path = tid if tid.startswith('/') else f"/{tid}"
|
||||
base_url = f"{self.host}{path}".rstrip('/')
|
||||
|
||||
if pg == 1:
|
||||
url = f"{base_url}/"
|
||||
else:
|
||||
url = f"{base_url}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article, #index article, article'), tid)
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
except Exception as e:
|
||||
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=15)
|
||||
data = self.getpq(response.text)
|
||||
|
||||
plist = []
|
||||
used_names = set()
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
try:
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
|
||||
if video_url:
|
||||
ep_name = ''
|
||||
parent = k.parents().eq(0)
|
||||
for _ in range(4):
|
||||
if not parent: break
|
||||
heading = parent.find('h2, h3, h4').eq(0).text().strip()
|
||||
if heading:
|
||||
ep_name = heading
|
||||
break
|
||||
parent = parent.parents().eq(0)
|
||||
|
||||
base_name = ep_name if ep_name else f"视频{c}"
|
||||
name = base_name
|
||||
count = 2
|
||||
while name in used_names:
|
||||
name = f"{base_name} {count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
|
||||
plist.append(f"{name}${video_url}")
|
||||
except: continue
|
||||
|
||||
if not plist:
|
||||
content_area = data('.post-content, article')
|
||||
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 ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
|
||||
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
|
||||
if not ep_name: ep_name = f"视频{i}"
|
||||
|
||||
if not link_href.startswith('http'):
|
||||
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
|
||||
|
||||
plist.append(f"{ep_name}${link_href}")
|
||||
|
||||
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
|
||||
|
||||
vod_content = ''
|
||||
try:
|
||||
tags = []
|
||||
seen_names = set()
|
||||
seen_ids = set()
|
||||
|
||||
tag_links = data('.tags a, .keywords a, .post-tags a')
|
||||
|
||||
candidates = []
|
||||
for k in tag_links.items():
|
||||
title = k.text().strip()
|
||||
href = k.attr('href')
|
||||
if title and href:
|
||||
candidates.append({'name': title, 'id': href})
|
||||
|
||||
candidates.sort(key=lambda x: len(x['name']), reverse=True)
|
||||
|
||||
for item in candidates:
|
||||
name = item['name']
|
||||
id_ = item['id']
|
||||
|
||||
if id_ in seen_ids: continue
|
||||
|
||||
is_duplicate = False
|
||||
for seen in seen_names:
|
||||
if name in seen:
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if not is_duplicate:
|
||||
target = json.dumps({'id': id_, 'name': name})
|
||||
tags.append(f'[a=cr:{target}/]{name}[/a]')
|
||||
seen_names.add(name)
|
||||
seen_ids.add(id_)
|
||||
|
||||
if tags:
|
||||
vod_content = ' '.join(tags)
|
||||
else:
|
||||
vod_content = data('.post-title').text()
|
||||
except Exception:
|
||||
vod_content = '获取标签失败'
|
||||
|
||||
if not vod_content:
|
||||
vod_content = data('h1').text() or '91吃瓜中心'
|
||||
|
||||
return {'list': [{'vod_play_from': '91吃瓜中心', 'vod_play_url': play_url, 'vod_content': vod_content}]}
|
||||
except:
|
||||
return {'list': [{'vod_play_from': '91吃瓜中心', 'vod_play_url': '获取失败'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
pg = int(pg) if pg else 1
|
||||
|
||||
if pg == 1:
|
||||
url = f"{self.host}/search/{key}/"
|
||||
else:
|
||||
url = f"{self.host}/search/{key}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
|
||||
except:
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parse = 0 if self.isVideoFormat(id) else 1
|
||||
url = self.proxy(id) if '.m3u8' in id else id
|
||||
return {'parse': parse, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
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:
|
||||
return self.tsProxy(url)
|
||||
except:
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
url = self.d64(url)
|
||||
res = requests.get(url, headers=self.headers, proxies=self.proxies)
|
||||
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'):
|
||||
line = f"{base}/{line}"
|
||||
lines.append(self.proxy(line, 'ts'))
|
||||
else:
|
||||
lines.append(line)
|
||||
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
|
||||
|
||||
def tsProxy(self, url):
|
||||
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
|
||||
|
||||
def e64(self, text):
|
||||
return b64encode(str(text).encode()).decode()
|
||||
|
||||
def d64(self, text):
|
||||
return b64decode(str(text).encode()).decode()
|
||||
|
||||
def aesimg(self, data):
|
||||
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'): 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, tid=''):
|
||||
videos = []
|
||||
is_folder = '/mrdg' in (tid or '')
|
||||
for k in data.items():
|
||||
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
|
||||
a = k if k.is_('a') else k('a').eq(0)
|
||||
href = a.attr('href')
|
||||
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
|
||||
if not title and k.is_('a'): title = k.text()
|
||||
|
||||
if href and title:
|
||||
img = self.getimg(k('script').text(), k, card_html)
|
||||
videos.append({
|
||||
'vod_id': f"{href}{'@folder' if is_folder else ''}",
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': k('time').text() or '',
|
||||
'vod_tag': 'folder' if is_folder 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)
|
||||
videos = []
|
||||
for i, h2 in enumerate(data('.post-content h2').items()):
|
||||
p_txt = data('.post-content p').eq(i * 2)
|
||||
p_img = data('.post-content p').eq(i * 2 + 1)
|
||||
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
|
||||
videos.append({
|
||||
'vod_id': p_txt('a').attr('href'),
|
||||
'vod_name': p_txt.text().strip(),
|
||||
'vod_pic': self.getimg('', p_img, p_html),
|
||||
'vod_remarks': h2.text().strip()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getimg(self, text, elem=None, html_content=None):
|
||||
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('&', '&')
|
||||
|
||||
if 'data:image' in html_content:
|
||||
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
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))
|
||||
|
||||
return ''
|
||||
|
||||
def _proc_url(self, url):
|
||||
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 = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
|
||||
|
||||
def getpq(self, data):
|
||||
try: return pq(data)
|
||||
except: return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,259 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 123AV短视频 - Fongmi影视App适配爬虫
|
||||
# 优化为短视频模式,支持滑动切换
|
||||
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "123AV"
|
||||
|
||||
def init(self, extend=''):
|
||||
self.home_url = 'https://123av.fun'
|
||||
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{'type_id': 'publish-time/sort-desc', 'type_name': '最新发布'},
|
||||
{'type_id': 'view-count/sort-desc', 'type_name': '最多播放'},
|
||||
{'type_id': 'comment-count/sort-desc', 'type_name': '最多评论'},
|
||||
{'type_id': 'favorite-count/sort-desc', 'type_name': '最多收藏'},
|
||||
{'type_id': 'explore', 'type_name': '探索发现'},
|
||||
{'type_id': 'list', 'type_name': '排行榜'},
|
||||
],
|
||||
'filters': {}
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.categoryContent('publish-time/sort-desc', 1, {}, {})
|
||||
|
||||
def _fetch_html(self, url):
|
||||
try:
|
||||
rsp = self.fetch(url, headers={
|
||||
"User-Agent": self.ua,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
}, timeout=15)
|
||||
if rsp and hasattr(rsp, 'text') and rsp.text:
|
||||
return rsp.text
|
||||
except Exception as e:
|
||||
print(f'fetch error: {e}')
|
||||
return ''
|
||||
|
||||
def _extract_video_list(self, html):
|
||||
videos = []
|
||||
if not html:
|
||||
return videos
|
||||
|
||||
# 匹配视频卡片
|
||||
card_pattern = re.compile(
|
||||
r'<a\s+([^>]*data-src="https://static\.123av\.fun/[^"]+\.m3u8"[^>]*)>(.*?)</a>',
|
||||
re.S
|
||||
)
|
||||
|
||||
cards = card_pattern.findall(html)
|
||||
|
||||
for attrs, content in cards:
|
||||
try:
|
||||
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', attrs)
|
||||
poster_match = re.search(r'data-poster="([^"]*)"', attrs)
|
||||
id_match = re.search(r'data-id="(\d+)"', attrs)
|
||||
dur_match = re.search(r'data-duration="(\d+)"', attrs)
|
||||
|
||||
title_match = re.search(r'<xwya-video[^>]*alt="([^"]*)"', content)
|
||||
|
||||
if src_match and id_match:
|
||||
m3u8_url = src_match.group(1)
|
||||
vid = id_match.group(1)
|
||||
poster = poster_match.group(1) if poster_match else ''
|
||||
duration = dur_match.group(1) if dur_match else '0'
|
||||
title = title_match.group(1).strip() if title_match else f'视频{vid}'
|
||||
|
||||
dur = int(duration)
|
||||
if dur >= 3600:
|
||||
duration_str = f'{dur // 3600}:{(dur % 3600) // 60:02d}:{dur % 60:02d}'
|
||||
else:
|
||||
duration_str = f'{dur // 60:02d}:{dur % 60:02d}'
|
||||
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': poster,
|
||||
'vod_remarks': duration_str,
|
||||
})
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
return videos
|
||||
|
||||
def categoryContent(self, tid, page, filter, ext):
|
||||
video_list = []
|
||||
|
||||
if tid in ('explore', 'list', 'subscribe'):
|
||||
url = f'{self.home_url}/{tid}/page-{page}'
|
||||
else:
|
||||
url = f'{self.home_url}/{tid}/page-{page}'
|
||||
|
||||
html = self._fetch_html(url)
|
||||
video_list = self._extract_video_list(html)
|
||||
|
||||
return {
|
||||
'list': video_list,
|
||||
'page': int(page),
|
||||
'pagecount': 999,
|
||||
'limit': 20,
|
||||
'total': 999 * 20
|
||||
}
|
||||
|
||||
def detailContent(self, did):
|
||||
"""视频详情 - 关键修改:返回播放URL让playerContent处理"""
|
||||
video_list = []
|
||||
try:
|
||||
vid = did[0]
|
||||
detail_url = f'{self.home_url}/detail/{vid}'
|
||||
html = self._fetch_html(detail_url)
|
||||
|
||||
if html:
|
||||
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', html)
|
||||
poster_match = re.search(r'data-poster="([^"]*)"', html)
|
||||
title_match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
|
||||
if not title_match:
|
||||
title_match = re.search(r'property="og:title"\s+content="([^"]*)"', html)
|
||||
if not title_match:
|
||||
title_match = re.search(r'<xwya-video[^>]*alt="([^"]*)"', html)
|
||||
|
||||
desc_match = re.search(r'property="og:description"\s+content="([^"]*)"', html)
|
||||
dur_match = re.search(r'data-duration="(\d+)"', html)
|
||||
|
||||
m3u8_url = src_match.group(1) if src_match else ''
|
||||
vod_pic = poster_match.group(1) if poster_match else ''
|
||||
vod_name = title_match.group(1).strip() if title_match else ''
|
||||
vod_content = desc_match.group(1) if desc_match else ''
|
||||
|
||||
duration_str = ''
|
||||
if dur_match:
|
||||
dur = int(dur_match.group(1))
|
||||
if dur >= 3600:
|
||||
duration_str = f'{dur // 3600}:{(dur % 3600) // 60:02d}:{dur % 60:02d}'
|
||||
else:
|
||||
duration_str = f'{dur // 60:02d}:{dur % 60:02d}'
|
||||
else:
|
||||
m3u8_url = ''
|
||||
vod_pic = ''
|
||||
vod_name = ''
|
||||
vod_content = ''
|
||||
duration_str = ''
|
||||
|
||||
# 关键修改:如果直接有m3u8,放入播放URL
|
||||
# 使用特殊格式让Fongmi识别为短视频
|
||||
if m3u8_url:
|
||||
# 格式: 集数名称$url#集数名称$url
|
||||
vod_play_url = f'正片${m3u8_url}'
|
||||
else:
|
||||
vod_play_url = ''
|
||||
|
||||
video_list.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': duration_str,
|
||||
'vod_content': vod_content,
|
||||
'vod_play_from': '短视频', # 改为短视频,可能触发滑动模式
|
||||
'vod_play_url': vod_play_url,
|
||||
'type_name': '短视频',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_director': '',
|
||||
'vod_actor': '',
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
|
||||
return {
|
||||
'list': video_list,
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
video_list = []
|
||||
try:
|
||||
encoded_key = urllib.parse.quote(key)
|
||||
url = f'{self.home_url}/search/{encoded_key}/page-{page}'
|
||||
html = self._fetch_html(url)
|
||||
video_list = self._extract_video_list(html)
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
|
||||
return {
|
||||
'list': video_list,
|
||||
'page': int(page),
|
||||
'pagecount': 99,
|
||||
'limit': 20,
|
||||
'total': 99 * 20
|
||||
}
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
"""播放器内容 - 关键修改"""
|
||||
# 如果pid已经是m3u8地址,直接返回
|
||||
if pid.startswith('http') and '.m3u8' in pid:
|
||||
return {
|
||||
'parse': 0, # 直接播放
|
||||
'url': pid,
|
||||
'header': {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.home_url + '/'
|
||||
}
|
||||
}
|
||||
|
||||
# 如果是详情页URL,获取m3u8
|
||||
if '/detail/' in pid:
|
||||
html = self._fetch_html(pid)
|
||||
if html:
|
||||
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', html)
|
||||
if src_match:
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': src_match.group(1),
|
||||
'header': {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.home_url + '/'
|
||||
}
|
||||
}
|
||||
|
||||
# 默认返回,让外部解析
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': pid,
|
||||
'header': {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.home_url + '/'
|
||||
}
|
||||
}
|
||||
|
||||
def localProxy(self, params):
|
||||
return {}
|
||||
|
||||
def destroy(self):
|
||||
return '正在Destroy'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
@@ -0,0 +1,479 @@
|
||||
# -*- 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, urljoin
|
||||
|
||||
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': '/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:
|
||||
# 处理分页逻辑
|
||||
page_num = int(pg) if pg and pg.isdigit() else 1
|
||||
|
||||
# 构建基础URL
|
||||
if tid.startswith('/'):
|
||||
base_url = f"{self.host}{tid}"
|
||||
else:
|
||||
base_url = f"{self.host}/{tid}"
|
||||
|
||||
# 移除可能存在的末尾斜杠,以便统一添加分页
|
||||
base_url = base_url.rstrip('/')
|
||||
|
||||
# 根据页码构建URL - 使用你提供的分页格式: /category/name/页码/
|
||||
if page_num > 1:
|
||||
url = f"{base_url}/{page_num}/"
|
||||
else:
|
||||
# 第一页使用基础URL
|
||||
url = f"{base_url}/"
|
||||
|
||||
self.log(f"分类请求URL: {url}")
|
||||
print(f"分类请求URL: {url}")
|
||||
|
||||
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)
|
||||
|
||||
# 尝试获取总页数
|
||||
pagecount = 1
|
||||
try:
|
||||
# 查找分页元素 - 尝试多种可能的选择器
|
||||
pagination_selectors = ['.pagination', '.page-nav', '.pager', '.nav-links', '.pages']
|
||||
for selector in pagination_selectors:
|
||||
pagination = data(selector)
|
||||
if pagination:
|
||||
page_links = pagination.find('a')
|
||||
page_numbers = []
|
||||
for link in page_links.items():
|
||||
text = link.text().strip()
|
||||
if text.isdigit():
|
||||
page_numbers.append(int(text))
|
||||
if page_numbers:
|
||||
pagecount = max(page_numbers)
|
||||
break
|
||||
|
||||
# 如果没有找到分页元素,尝试查找下一页按钮判断页数
|
||||
if pagecount == 1:
|
||||
next_buttons = data('a:contains("下一页"), a:contains("Next")')
|
||||
if next_buttons:
|
||||
pagecount = 999 # 有下一页按钮但无法确定具体页数,设一个较大值
|
||||
except Exception as e:
|
||||
self.log(f"获取总页数失败: {e}")
|
||||
pagecount = 999 # 如果无法获取,设置一个较大的值
|
||||
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = pagecount
|
||||
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:
|
||||
# 处理搜索分页
|
||||
page_num = int(pg) if pg and pg.isdigit() else 1
|
||||
|
||||
# 使用统一的分页格式
|
||||
if page_num > 1:
|
||||
url = f"{self.host}/search/{key}/{page_num}/"
|
||||
else:
|
||||
url = f"{self.host}/search/{key}/"
|
||||
|
||||
self.log(f"搜索请求URL: {url}")
|
||||
print(f"搜索请求URL: {url}")
|
||||
|
||||
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'))
|
||||
|
||||
# 尝试获取搜索结果的页数
|
||||
pagecount = 1
|
||||
try:
|
||||
pagination_selectors = ['.pagination', '.page-nav', '.pager', '.nav-links', '.pages']
|
||||
for selector in pagination_selectors:
|
||||
pagination = data(selector)
|
||||
if pagination:
|
||||
page_links = pagination.find('a')
|
||||
page_numbers = []
|
||||
for link in page_links.items():
|
||||
text = link.text().strip()
|
||||
if text.isdigit():
|
||||
page_numbers.append(int(text))
|
||||
if page_numbers:
|
||||
pagecount = max(page_numbers)
|
||||
break
|
||||
|
||||
# 如果没有找到分页元素,尝试查找下一页按钮判断页数
|
||||
if pagecount == 1:
|
||||
next_buttons = data('a:contains("下一页"), a:contains("Next")')
|
||||
if next_buttons:
|
||||
pagecount = 999
|
||||
except:
|
||||
pagecount = 999
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': pagecount}
|
||||
|
||||
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://artist.vgwtswi.xyz',
|
||||
'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'))
|
||||
@@ -0,0 +1,390 @@
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import hashlib
|
||||
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 as BaseSpider
|
||||
|
||||
img_cache = {}
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
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/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',
|
||||
}
|
||||
self.host = self.get_working_host()
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
print(f"使用站点: {self.host}")
|
||||
|
||||
def getName(self):
|
||||
return "🌈 51大赛|终极完美版"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
global img_cache
|
||||
img_cache.clear()
|
||||
|
||||
def get_working_host(self):
|
||||
dynamic_urls = [
|
||||
'https://jbgcz2.dzebypd.xyz/',
|
||||
'https://jbgcz3.dzebypd.xyz/'
|
||||
]
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return url
|
||||
except Exception:
|
||||
continue
|
||||
return dynamic_urls[0]
|
||||
|
||||
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)
|
||||
|
||||
classes = []
|
||||
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()
|
||||
if not href or href == '#' or not name: continue
|
||||
classes.append({'type_name': name, 'type_id': href})
|
||||
if classes: break
|
||||
|
||||
if not classes:
|
||||
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
|
||||
|
||||
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
|
||||
except Exception as 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, article'))}
|
||||
except Exception as e:
|
||||
return {'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
|
||||
|
||||
if tid.startswith('http'):
|
||||
base_url = tid.rstrip('/')
|
||||
else:
|
||||
path = tid if tid.startswith('/') else f"/{tid}"
|
||||
base_url = f"{self.host}{path}".rstrip('/')
|
||||
|
||||
if pg == 1:
|
||||
url = f"{base_url}/"
|
||||
else:
|
||||
url = f"{base_url}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article, #index article, article'), tid)
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
except Exception as e:
|
||||
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=15)
|
||||
data = self.getpq(response.text)
|
||||
|
||||
plist = []
|
||||
used_names = set()
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
try:
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
|
||||
if video_url:
|
||||
ep_name = ''
|
||||
parent = k.parents().eq(0)
|
||||
for _ in range(4):
|
||||
if not parent: break
|
||||
heading = parent.find('h2, h3, h4').eq(0).text().strip()
|
||||
if heading:
|
||||
ep_name = heading
|
||||
break
|
||||
parent = parent.parents().eq(0)
|
||||
|
||||
base_name = ep_name if ep_name else f"视频{c}"
|
||||
name = base_name
|
||||
count = 2
|
||||
while name in used_names:
|
||||
name = f"{base_name} {count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
|
||||
plist.append(f"{name}${video_url}")
|
||||
except: continue
|
||||
|
||||
if not plist:
|
||||
content_area = data('.post-content, article')
|
||||
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 ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
|
||||
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
|
||||
if not ep_name: ep_name = f"视频{i}"
|
||||
|
||||
if not link_href.startswith('http'):
|
||||
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
|
||||
|
||||
plist.append(f"{ep_name}${link_href}")
|
||||
|
||||
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
|
||||
|
||||
vod_content = ''
|
||||
try:
|
||||
tags = []
|
||||
seen_names = set()
|
||||
seen_ids = set()
|
||||
|
||||
tag_links = data('.tags a, .keywords a, .post-tags a')
|
||||
|
||||
candidates = []
|
||||
for k in tag_links.items():
|
||||
title = k.text().strip()
|
||||
href = k.attr('href')
|
||||
if title and href:
|
||||
candidates.append({'name': title, 'id': href})
|
||||
|
||||
candidates.sort(key=lambda x: len(x['name']), reverse=True)
|
||||
|
||||
for item in candidates:
|
||||
name = item['name']
|
||||
id_ = item['id']
|
||||
|
||||
if id_ in seen_ids: continue
|
||||
|
||||
is_duplicate = False
|
||||
for seen in seen_names:
|
||||
if name in seen:
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if not is_duplicate:
|
||||
target = json.dumps({'id': id_, 'name': name})
|
||||
tags.append(f'[a=cr:{target}/]{name}[/a]')
|
||||
seen_names.add(name)
|
||||
seen_ids.add(id_)
|
||||
|
||||
if tags:
|
||||
vod_content = ' '.join(tags)
|
||||
else:
|
||||
vod_content = data('.post-title').text()
|
||||
except Exception:
|
||||
vod_content = '获取标签失败'
|
||||
|
||||
if not vod_content:
|
||||
vod_content = data('h1').text() or '51大赛'
|
||||
|
||||
return {'list': [{'vod_play_from': '51大赛', 'vod_play_url': play_url, 'vod_content': vod_content}]}
|
||||
except:
|
||||
return {'list': [{'vod_play_from': '51大赛', 'vod_play_url': '获取失败'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
pg = int(pg) if pg else 1
|
||||
|
||||
if pg == 1:
|
||||
url = f"{self.host}/search/{key}/"
|
||||
else:
|
||||
url = f"{self.host}/search/{key}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
|
||||
except:
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parse = 0 if self.isVideoFormat(id) else 1
|
||||
url = self.proxy(id) if '.m3u8' in id else id
|
||||
return {'parse': parse, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
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:
|
||||
return self.tsProxy(url)
|
||||
except:
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
url = self.d64(url)
|
||||
res = requests.get(url, headers=self.headers, proxies=self.proxies)
|
||||
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'):
|
||||
line = f"{base}/{line}"
|
||||
lines.append(self.proxy(line, 'ts'))
|
||||
else:
|
||||
lines.append(line)
|
||||
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
|
||||
|
||||
def tsProxy(self, url):
|
||||
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
|
||||
|
||||
def e64(self, text):
|
||||
return b64encode(str(text).encode()).decode()
|
||||
|
||||
def d64(self, text):
|
||||
return b64decode(str(text).encode()).decode()
|
||||
|
||||
def aesimg(self, data):
|
||||
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'): 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, tid=''):
|
||||
videos = []
|
||||
is_folder = '/mrdg' in (tid or '')
|
||||
for k in data.items():
|
||||
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
|
||||
a = k if k.is_('a') else k('a').eq(0)
|
||||
href = a.attr('href')
|
||||
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
|
||||
if not title and k.is_('a'): title = k.text()
|
||||
|
||||
if href and title:
|
||||
img = self.getimg(k('script').text(), k, card_html)
|
||||
videos.append({
|
||||
'vod_id': f"{href}{'@folder' if is_folder else ''}",
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': k('time').text() or '',
|
||||
'vod_tag': 'folder' if is_folder 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)
|
||||
videos = []
|
||||
for i, h2 in enumerate(data('.post-content h2').items()):
|
||||
p_txt = data('.post-content p').eq(i * 2)
|
||||
p_img = data('.post-content p').eq(i * 2 + 1)
|
||||
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
|
||||
videos.append({
|
||||
'vod_id': p_txt('a').attr('href'),
|
||||
'vod_name': p_txt.text().strip(),
|
||||
'vod_pic': self.getimg('', p_img, p_html),
|
||||
'vod_remarks': h2.text().strip()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getimg(self, text, elem=None, html_content=None):
|
||||
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('&', '&')
|
||||
|
||||
if 'data:image' in html_content:
|
||||
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
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))
|
||||
|
||||
return ''
|
||||
|
||||
def _proc_url(self, url):
|
||||
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 = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
|
||||
|
||||
def getpq(self, data):
|
||||
try: return pq(data)
|
||||
except: return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,390 @@
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import hashlib
|
||||
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 as BaseSpider
|
||||
|
||||
img_cache = {}
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
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/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',
|
||||
}
|
||||
self.host = self.get_working_host()
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
print(f"使用站点: {self.host}")
|
||||
|
||||
def getName(self):
|
||||
return "🌈 51爆料|终极完美版"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
global img_cache
|
||||
img_cache.clear()
|
||||
|
||||
def get_working_host(self):
|
||||
dynamic_urls = [
|
||||
'https://carry.wlmrpodg.cc/',
|
||||
'https://analyze.wlmrpodg.cc/'
|
||||
]
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code == 200:
|
||||
return url
|
||||
except Exception:
|
||||
continue
|
||||
return dynamic_urls[0]
|
||||
|
||||
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)
|
||||
|
||||
classes = []
|
||||
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()
|
||||
if not href or href == '#' or not name: continue
|
||||
classes.append({'type_name': name, 'type_id': href})
|
||||
if classes: break
|
||||
|
||||
if not classes:
|
||||
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
|
||||
|
||||
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
|
||||
except Exception as 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, article'))}
|
||||
except Exception as e:
|
||||
return {'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
|
||||
|
||||
if tid.startswith('http'):
|
||||
base_url = tid.rstrip('/')
|
||||
else:
|
||||
path = tid if tid.startswith('/') else f"/{tid}"
|
||||
base_url = f"{self.host}{path}".rstrip('/')
|
||||
|
||||
if pg == 1:
|
||||
url = f"{base_url}/"
|
||||
else:
|
||||
url = f"{base_url}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article, #index article, article'), tid)
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
except Exception as e:
|
||||
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=15)
|
||||
data = self.getpq(response.text)
|
||||
|
||||
plist = []
|
||||
used_names = set()
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
try:
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
|
||||
if video_url:
|
||||
ep_name = ''
|
||||
parent = k.parents().eq(0)
|
||||
for _ in range(4):
|
||||
if not parent: break
|
||||
heading = parent.find('h2, h3, h4').eq(0).text().strip()
|
||||
if heading:
|
||||
ep_name = heading
|
||||
break
|
||||
parent = parent.parents().eq(0)
|
||||
|
||||
base_name = ep_name if ep_name else f"视频{c}"
|
||||
name = base_name
|
||||
count = 2
|
||||
while name in used_names:
|
||||
name = f"{base_name} {count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
|
||||
plist.append(f"{name}${video_url}")
|
||||
except: continue
|
||||
|
||||
if not plist:
|
||||
content_area = data('.post-content, article')
|
||||
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 ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
|
||||
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
|
||||
if not ep_name: ep_name = f"视频{i}"
|
||||
|
||||
if not link_href.startswith('http'):
|
||||
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
|
||||
|
||||
plist.append(f"{ep_name}${link_href}")
|
||||
|
||||
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
|
||||
|
||||
vod_content = ''
|
||||
try:
|
||||
tags = []
|
||||
seen_names = set()
|
||||
seen_ids = set()
|
||||
|
||||
tag_links = data('.tags a, .keywords a, .post-tags a')
|
||||
|
||||
candidates = []
|
||||
for k in tag_links.items():
|
||||
title = k.text().strip()
|
||||
href = k.attr('href')
|
||||
if title and href:
|
||||
candidates.append({'name': title, 'id': href})
|
||||
|
||||
candidates.sort(key=lambda x: len(x['name']), reverse=True)
|
||||
|
||||
for item in candidates:
|
||||
name = item['name']
|
||||
id_ = item['id']
|
||||
|
||||
if id_ in seen_ids: continue
|
||||
|
||||
is_duplicate = False
|
||||
for seen in seen_names:
|
||||
if name in seen:
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if not is_duplicate:
|
||||
target = json.dumps({'id': id_, 'name': name})
|
||||
tags.append(f'[a=cr:{target}/]{name}[/a]')
|
||||
seen_names.add(name)
|
||||
seen_ids.add(id_)
|
||||
|
||||
if tags:
|
||||
vod_content = ' '.join(tags)
|
||||
else:
|
||||
vod_content = data('.post-title').text()
|
||||
except Exception:
|
||||
vod_content = '获取标签失败'
|
||||
|
||||
if not vod_content:
|
||||
vod_content = data('h1').text() or '51爆料'
|
||||
|
||||
return {'list': [{'vod_play_from': '51爆料', 'vod_play_url': play_url, 'vod_content': vod_content}]}
|
||||
except:
|
||||
return {'list': [{'vod_play_from': '51爆料', 'vod_play_url': '获取失败'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
pg = int(pg) if pg else 1
|
||||
|
||||
if pg == 1:
|
||||
url = f"{self.host}/search/{key}/"
|
||||
else:
|
||||
url = f"{self.host}/search/{key}/{pg}/"
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
|
||||
except:
|
||||
return {'list': [], 'page': pg, 'pagecount': 9999}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parse = 0 if self.isVideoFormat(id) else 1
|
||||
url = self.proxy(id) if '.m3u8' in id else id
|
||||
return {'parse': parse, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
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:
|
||||
return self.tsProxy(url)
|
||||
except:
|
||||
return [404, 'text/plain', b'']
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
url = self.d64(url)
|
||||
res = requests.get(url, headers=self.headers, proxies=self.proxies)
|
||||
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'):
|
||||
line = f"{base}/{line}"
|
||||
lines.append(self.proxy(line, 'ts'))
|
||||
else:
|
||||
lines.append(line)
|
||||
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
|
||||
|
||||
def tsProxy(self, url):
|
||||
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
|
||||
|
||||
def e64(self, text):
|
||||
return b64encode(str(text).encode()).decode()
|
||||
|
||||
def d64(self, text):
|
||||
return b64decode(str(text).encode()).decode()
|
||||
|
||||
def aesimg(self, data):
|
||||
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'): 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, tid=''):
|
||||
videos = []
|
||||
is_folder = '/mrdg' in (tid or '')
|
||||
for k in data.items():
|
||||
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
|
||||
a = k if k.is_('a') else k('a').eq(0)
|
||||
href = a.attr('href')
|
||||
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
|
||||
if not title and k.is_('a'): title = k.text()
|
||||
|
||||
if href and title:
|
||||
img = self.getimg(k('script').text(), k, card_html)
|
||||
videos.append({
|
||||
'vod_id': f"{href}{'@folder' if is_folder else ''}",
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': k('time').text() or '',
|
||||
'vod_tag': 'folder' if is_folder 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)
|
||||
videos = []
|
||||
for i, h2 in enumerate(data('.post-content h2').items()):
|
||||
p_txt = data('.post-content p').eq(i * 2)
|
||||
p_img = data('.post-content p').eq(i * 2 + 1)
|
||||
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
|
||||
videos.append({
|
||||
'vod_id': p_txt('a').attr('href'),
|
||||
'vod_name': p_txt.text().strip(),
|
||||
'vod_pic': self.getimg('', p_img, p_html),
|
||||
'vod_remarks': h2.text().strip()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getimg(self, text, elem=None, html_content=None):
|
||||
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('&', '&')
|
||||
|
||||
if 'data:image' in html_content:
|
||||
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
|
||||
if m: return self._proc_url(m.group(1))
|
||||
|
||||
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))
|
||||
|
||||
return ''
|
||||
|
||||
def _proc_url(self, url):
|
||||
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 = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
|
||||
|
||||
def getpq(self, data):
|
||||
try: return pq(data)
|
||||
except: return pq(data.encode('utf-8'))
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
import re
|
||||
import requests
|
||||
|
||||
class Spider():
|
||||
def getName(self):
|
||||
return 'JAV36'
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{'type_name': '最新更新', 'type_id': 'latest-updates/'},
|
||||
{'type_name': '4K高清', 'type_id': 'tags/4k/'}
|
||||
],
|
||||
'list': []
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.categoryContent('latest-updates/', 1, False, {})
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
url = f'https://jav36.com/{tid}'
|
||||
if int(pg) > 1:
|
||||
url = f'https://jav36.com/{tid}{pg}/'
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
|
||||
try:
|
||||
res = requests.get(url, headers=headers, timeout=15)
|
||||
return {'list': self.parse_list(res.text)}
|
||||
except: return {'list': []}
|
||||
|
||||
def searchContent(self, keyword, quick, pg=1):
|
||||
url = f'https://jav36.com/search/{keyword}/'
|
||||
if int(pg) > 1:
|
||||
url = f'https://jav36.com/search/{keyword}/{pg}/'
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
|
||||
try:
|
||||
res = requests.get(url, headers=headers, timeout=15)
|
||||
return {'list': self.parse_list(res.text)}
|
||||
except: return {'list': []}
|
||||
|
||||
def parse_list(self, html):
|
||||
vod_list = []
|
||||
pattern = r'href=\"https://jav36\.com/videos/(?P<id_path>\d+/(?P<id>[^/]+)/)\" title=\"(?P<name>[^\"]+)\".*?data-original=\"(?P<pic>[^\"]+)\"'
|
||||
for m in re.finditer(pattern, html, re.S):
|
||||
vod_list.append({
|
||||
'vod_id': m.group('id_path'),
|
||||
'vod_name': m.group('name').strip(),
|
||||
'vod_pic': m.group('pic'),
|
||||
'vod_remarks': 'Full HD'
|
||||
})
|
||||
return vod_list
|
||||
|
||||
def detailContent(self, ids):
|
||||
return {'list': [{'vod_id': ids[0], 'vod_name': 'Video', 'vod_play_from': 'Direct', 'vod_play_url': 'Play$'+ids[0]}]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = f'https://jav36.com/videos/{id}'
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
|
||||
try:
|
||||
res = requests.get(url, headers=headers, timeout=10)
|
||||
match = re.search(r'\"contentUrl\"\s*:\s*\"([^\"]+)\"', res.text)
|
||||
if match:
|
||||
return {'parse': 0, 'url': match.group(1), 'header': {'User-Agent': headers['User-Agent'], 'Referer': 'https://jav36.com/'}}
|
||||
except: pass
|
||||
return {'parse': 0, 'url': url}
|
||||
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "PigAV_Stable"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.base_url = "https://pigav.ws"
|
||||
self.api_url = "https://pigav.ws/api/v1"
|
||||
# 基础浏览器特征
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Origin": "https://pigav.ws",
|
||||
"Referer": "https://pigav.ws/"
|
||||
}
|
||||
self.page_size = 24
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {"class": []}
|
||||
result["class"] = [
|
||||
{"type_id": "publishedAt", "type_name": "最近更新"},
|
||||
{"type_id": "hot", "type_name": "热门视频"},
|
||||
{"type_id": "views", "type_name": "最多观看"}
|
||||
]
|
||||
result["list"] = self.get_videos(f"{self.api_url}/videos?sort=-publishedAt&count={self.page_size}&start=0")
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
sort_map = {"publishedAt": "-publishedAt", "hot": "-hot", "views": "-views"}
|
||||
sort = sort_map.get(tid, "-publishedAt")
|
||||
p = int(pg)
|
||||
start = (max(1, p) - 1) * self.page_size
|
||||
url = f"{self.api_url}/videos?sort={sort}&count={self.page_size}&start={start}"
|
||||
return {"list": self.get_videos(url)}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0] if isinstance(ids, list) else ids
|
||||
url = f"{self.api_url}/videos/{vid}"
|
||||
try:
|
||||
res = self.fetch(url, headers=self.headers, timeout=10)
|
||||
data = json.loads(res.text if hasattr(res, 'text') else res)
|
||||
vod = {
|
||||
"vod_id": vid,
|
||||
"vod_name": data.get("name", ""),
|
||||
"vod_pic": self.fix_url(data.get("thumbnailPath", "")),
|
||||
"vod_remarks": self.format_time(data.get("duration", 0)),
|
||||
"vod_actor": data.get("channel", {}).get("displayName", ""),
|
||||
"vod_content": data.get("description", ""),
|
||||
"vod_play_from": "PigAV",
|
||||
"vod_play_url": f"播放正片${vid}"
|
||||
}
|
||||
return {"list": [vod]}
|
||||
except:
|
||||
return {"list": []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
p = int(pg)
|
||||
start = (max(1, p) - 1) * self.page_size
|
||||
url = f"{self.api_url}/search/videos?search={key}&count={self.page_size}&start={start}"
|
||||
return {"list": self.get_videos(url)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = f"{self.api_url}/videos/{id}"
|
||||
|
||||
# 终极伪装 Header:模拟 Chrome 播放内核请求行为,防止 CDN 断流
|
||||
play_headers = {
|
||||
"User-Agent": self.headers["User-Agent"],
|
||||
"Referer": f"https://pigav.ws/videos/{id}",
|
||||
"Origin": "https://pigav.ws",
|
||||
"Accept": "*/*",
|
||||
"Range": "bytes=0-", # 核心:开启断点续传支持
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Dest": "video",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "cross-site"
|
||||
}
|
||||
|
||||
try:
|
||||
res = self.fetch(url, headers=self.headers, timeout=12)
|
||||
data = json.loads(res.text if hasattr(res, 'text') else res)
|
||||
|
||||
play_url = ""
|
||||
|
||||
# 优先方案:HLS (m3u8) 分片加载,最不容易超时
|
||||
streaming = data.get("streamingPlaylists", [])
|
||||
if streaming:
|
||||
# 策略:如果网络差,选列表中间的画质(通常 0 是低,最后是高)
|
||||
# 这里我们尝试选倒数第二个,通常是 720P,兼顾清晰度与速度
|
||||
idx = max(0, len(streaming) - 2)
|
||||
play_url = streaming[idx].get("playlistUrl")
|
||||
|
||||
# 备选方案:MP4 直链
|
||||
if not play_url:
|
||||
files = data.get("files", [])
|
||||
if files:
|
||||
# 优先寻找高度 <= 720 的文件
|
||||
suitable_files = [f for f in files if f.get("resolution", {}).get("height", 0) <= 720]
|
||||
best_file = suitable_files[-1] if suitable_files else files[0]
|
||||
play_url = best_file.get("fileUrl") or best_file.get("fileDownloadUrl")
|
||||
|
||||
if play_url:
|
||||
return {
|
||||
"parse": 0,
|
||||
"url": self.fix_url(play_url),
|
||||
"header": play_headers,
|
||||
"timeout": 60 # 延长内核超时建议值
|
||||
}
|
||||
except:
|
||||
pass
|
||||
return {"parse": 0, "url": ""}
|
||||
|
||||
def get_videos(self, url):
|
||||
videos = []
|
||||
try:
|
||||
res = self.fetch(url, headers=self.headers, timeout=10)
|
||||
content = res.text if hasattr(res, 'text') else res
|
||||
data = json.loads(content)
|
||||
items = data.get("data", []) if isinstance(data, dict) else data
|
||||
for item in items:
|
||||
videos.append({
|
||||
"vod_id": item.get("shortUUID") or item.get("uuid"),
|
||||
"vod_name": item.get("name", ""),
|
||||
"vod_pic": self.fix_url(item.get("thumbnailPath", "")),
|
||||
"vod_remarks": self.format_time(item.get("duration", 0))
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return videos
|
||||
|
||||
def fix_url(self, path):
|
||||
if not path: return ""
|
||||
if path.startswith("http"): return path
|
||||
return self.base_url + path
|
||||
|
||||
def format_time(self, seconds):
|
||||
try:
|
||||
sec = int(seconds)
|
||||
m, s = divmod(sec, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return f"{h:02d}:{m:02d}:{s:02d}" if h > 0 else f"{m:02d}:{s:02d}"
|
||||
except: return ""
|
||||
@@ -0,0 +1,124 @@
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "TOPTV"
|
||||
|
||||
def init(self, extend=""):
|
||||
super().init(extend)
|
||||
self.site_url = "https://toptv15.cyou"
|
||||
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": self.site_url
|
||||
}
|
||||
self.sess = requests.Session()
|
||||
self.sess.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1)))
|
||||
|
||||
def fetch(self, url, timeout=10):
|
||||
try:
|
||||
res = self.sess.get(url, headers=self.headers, timeout=timeout, verify=False)
|
||||
res.encoding = "utf-8"
|
||||
return res
|
||||
except:
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
cate_list = [
|
||||
{"type_name": "国产自拍", "type_id": "1"},
|
||||
{"type_name": "国产传媒", "type_id": "2"},
|
||||
{"type_name": "探花系列", "type_id": "3"},
|
||||
{"type_name": "人妻熟女", "type_id": "4"},
|
||||
{"type_name": "日本无码", "type_id": "5"},
|
||||
{"type_name": "美乳巨乳", "type_id": "6"},
|
||||
{"type_name": "强制侵犯", "type_id": "7"},
|
||||
{"type_name": "制服诱惑", "type_id": "8"},
|
||||
{"type_name": "绝色佳人", "type_id": "9"},
|
||||
{"type_name": "家庭乱伦", "type_id": "10"},
|
||||
{"type_name": "绝顶潮吹", "type_id": "11"},
|
||||
{"type_name": "网红主播", "type_id": "12"}
|
||||
]
|
||||
return {"class": cate_list}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if not hasattr(self, 'site_url'): self.init()
|
||||
pg = int(pg) if str(pg).isdigit() else 1
|
||||
list_url = f"{self.site_url}/index.php/vod/type/id/{tid}/page/{pg}.html"
|
||||
res = self.fetch(list_url)
|
||||
video_list = []
|
||||
if res:
|
||||
pattern = r'href="(/index.php/vod/detail/id/(\d+).html)".*?data-original="(.*?)".*?vod-name.*?>(.*?)<'
|
||||
matches = re.findall(pattern, res.text, re.S)
|
||||
for href, v_id, pic, name in matches:
|
||||
video_list.append({
|
||||
"vod_id": v_id,
|
||||
"vod_name": name.strip(),
|
||||
"vod_pic": pic if pic.startswith("http") else self.site_url + pic,
|
||||
"vod_remarks": ""
|
||||
})
|
||||
return {'list': video_list, 'page': pg, 'pagecount': 999, 'limit': 20, 'total': 9999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not hasattr(self, 'site_url'): self.init()
|
||||
vod_id = ids[0]
|
||||
res = self.fetch(f"{self.site_url}/index.php/vod/detail/id/{vod_id}.html")
|
||||
if not res: return {}
|
||||
html = res.text
|
||||
name_match = re.search(r'vod-name.*?>(.*?)<', html) or re.search(r'title-box.*?>(.*?)<', html)
|
||||
pic_match = re.search(r'detail-pic.*?src="(.*?)"', html) or re.search(r'data-original="(.*?)"', html)
|
||||
|
||||
play_matches = re.findall(r'href="(/index.php/vod/play/id/(\d+)/sid/(\d+)/nid/(\d+).html)">(.*?)<', html)
|
||||
play_urls = []
|
||||
for m in play_matches:
|
||||
play_urls.append(f"{m[4]}${m[1]}-{m[2]}-{m[3]}")
|
||||
|
||||
if not play_urls:
|
||||
play_urls.append(f"立即播放${vod_id}-1-1")
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": name_match.group(1).strip() if name_match else "视频详情",
|
||||
"vod_pic": pic_match.group(1) if pic_match else "",
|
||||
"vod_play_from": "TOP-TV",
|
||||
"vod_play_url": "#".join(play_urls)
|
||||
}
|
||||
return {"list": [vod]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if not hasattr(self, 'site_url'): self.init()
|
||||
parts = id.split('-')
|
||||
if len(parts) == 3:
|
||||
v_id, s_id, n_id = parts
|
||||
play_url = f"{self.site_url}/index.php/vod/play/id/{v_id}/sid/{s_id}/nid/{n_id}.html"
|
||||
else:
|
||||
play_url = f"{self.site_url}/index.php/vod/play/id/{id}.html"
|
||||
res = self.fetch(play_url)
|
||||
if res:
|
||||
data_json = re.search(r'var player_aaaa=(.*?)</script>', res.text)
|
||||
if data_json:
|
||||
try:
|
||||
url = json.loads(data_json.group(1)).get("url", "")
|
||||
return {"parse": 0, "url": url, "header": self.headers}
|
||||
except:
|
||||
pass
|
||||
return {"parse": 1, "url": play_url}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
if not hasattr(self, 'site_url'): self.init()
|
||||
res = self.fetch(f"{self.site_url}/index.php/vod/search/page/{pg}/wd/{key}.html")
|
||||
video_list = []
|
||||
if res:
|
||||
pattern = r'href="(/index.php/vod/detail/id/(\d+).html)".*?data-original="(.*?)".*?vod-name.*?>(.*?)<'
|
||||
matches = re.findall(pattern, res.text, re.S)
|
||||
for href, v_id, pic, name in matches:
|
||||
video_list.append({
|
||||
"vod_id": v_id,
|
||||
"vod_name": name.strip(),
|
||||
"vod_pic": pic if pic.startswith("http") else self.site_url + pic
|
||||
})
|
||||
return {"list": video_list}
|
||||
@@ -0,0 +1,303 @@
|
||||
# coding: utf-8
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import ssl
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
VERSION = '2.0.0'
|
||||
|
||||
SITE_URL = 'https://newxvideos.pages.dev'
|
||||
API_URL = 'https://newxvideos.pages.dev/api'
|
||||
|
||||
CATEGORIES = [
|
||||
{"type_id": "Arab-159", "type_name": "阿拉伯"},
|
||||
{"type_id": "Mature-38", "type_name": "成熟"},
|
||||
{"type_id": "Cuckold-237", "type_name": "出轨背叛"},
|
||||
{"type_id": "Femdom-235", "type_name": "调教"},
|
||||
{"type_id": "Anal-12", "type_name": "肛交"},
|
||||
{"type_id": "Brunette-25", "type_name": "褐发"},
|
||||
{"type_id": "Black_Woman-30", "type_name": "黑人"},
|
||||
{"type_id": "Redhead-31", "type_name": "红发"},
|
||||
{"type_id": "Fucked_Up_Family-81", "type_name": "家庭乱搞"},
|
||||
{"type_id": "Blonde-20", "type_name": "金发"},
|
||||
{"type_id": "Big_Cock-34", "type_name": "巨屌"},
|
||||
{"type_id": "Big_Tits-23", "type_name": "巨乳"},
|
||||
{"type_id": "Big_Ass-24", "type_name": "巨臀"},
|
||||
{"type_id": "Blowjob-15", "type_name": "口交"},
|
||||
{"type_id": "Latina-16", "type_name": "拉丁裔"},
|
||||
{"type_id": "Milf-19", "type_name": "辣妈"},
|
||||
{"type_id": "Gapes-167", "type_name": "裂开"},
|
||||
{"type_id": "Ass-14", "type_name": "美臀"},
|
||||
{"type_id": "Lesbian-26", "type_name": "女同"},
|
||||
{"type_id": "bbw-51", "type_name": "胖女"},
|
||||
{"type_id": "Squirting-56", "type_name": "喷出"},
|
||||
{"type_id": "Fisting-165", "type_name": "拳交"},
|
||||
{"type_id": "Gangbang-69", "type_name": "群交"},
|
||||
{"type_id": "Teen-13", "type_name": "少女"},
|
||||
{"type_id": "Cumshot-18", "type_name": "射颜"},
|
||||
{"type_id": "Cam_Porn-58", "type_name": "摄像头"},
|
||||
{"type_id": "Bi_Sexual-62", "type_name": "双性恋"},
|
||||
{"type_id": "Stockings-28", "type_name": "丝袜"},
|
||||
{"type_id": "Oiled-22", "type_name": "涂油"},
|
||||
{"type_id": "Lingerie-83", "type_name": "性感内衣"},
|
||||
{"type_id": "Asian_Woman-32", "type_name": "亚洲"},
|
||||
{"type_id": "Amateur-65", "type_name": "业余"},
|
||||
{"type_id": "Interracial-27", "type_name": "异族"},
|
||||
{"type_id": "Indian-89", "type_name": "印度"},
|
||||
{"type_id": "Creampie-40", "type_name": "中出"},
|
||||
{"type_id": "Solo_and_Masturbation-33", "type_name": "自慰"},
|
||||
{"type_id": "AI-239", "type_name": "AI"},
|
||||
{"type_id": "ASMR-229", "type_name": "ASMR"},
|
||||
]
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "V-HUB[成人]"
|
||||
|
||||
def init(self, extend):
|
||||
if extend:
|
||||
self.host = extend.get('host', SITE_URL)
|
||||
else:
|
||||
self.host = SITE_URL
|
||||
self.api_url = self.host.rstrip('/') + '/api'
|
||||
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': self.host + '/',
|
||||
'Origin': self.host
|
||||
}
|
||||
self._ssl_context = ssl.create_default_context()
|
||||
self._ssl_context.check_hostname = False
|
||||
self._ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def _xhttp(self, params):
|
||||
"""使用标准库urllib发起HTTP GET请求"""
|
||||
try:
|
||||
qs = urllib.parse.urlencode(params)
|
||||
full_url = self.api_url + '?' + qs
|
||||
req = urllib.request.Request(full_url, headers=self.headers, method='GET')
|
||||
resp = urllib.request.urlopen(req, context=self._ssl_context, timeout=15)
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
elif isinstance(data, dict) and 'data' in data:
|
||||
return data['data']
|
||||
return []
|
||||
except Exception as e:
|
||||
print('_xhttp error: %s' % str(e), file=sys.stderr)
|
||||
return []
|
||||
|
||||
def _format_time_cn(self, time_str):
|
||||
"""将英文时间格式转为中文,如 '11 min' -> '11分钟'"""
|
||||
if not time_str:
|
||||
return ''
|
||||
m = re.match(r'^(\d+)\s*min\s*$', time_str.strip(), re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1) + '分钟'
|
||||
m = re.match(r'^(\d+)\s*h(?:our)?s?\s*(\d+)?\s*min\s*$', time_str.strip(), re.IGNORECASE)
|
||||
if m:
|
||||
h = m.group(1)
|
||||
mi = m.group(2)
|
||||
if mi:
|
||||
return h + '小时' + mi + '分钟'
|
||||
return h + '小时'
|
||||
return time_str
|
||||
|
||||
def _extract_xvid(self, url):
|
||||
"""从视频URL的查询参数中提取xvid值"""
|
||||
if not url:
|
||||
return ''
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
qs = urllib.parse.parse_qs(parsed.query)
|
||||
if 'xvid' in qs:
|
||||
return qs['xvid'][0]
|
||||
return ''
|
||||
|
||||
def _build_vod_list(self, raw_data):
|
||||
"""将API返回的原始数据构造为vod列表"""
|
||||
videos = []
|
||||
for item in raw_data:
|
||||
title = item.get('title', '')
|
||||
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
|
||||
if not clean_title:
|
||||
clean_title = title
|
||||
|
||||
url = item.get('url', '')
|
||||
vod_id = self._extract_xvid(url)
|
||||
if not vod_id:
|
||||
vod_id = str(item.get('videoid', ''))
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': clean_title,
|
||||
'vod_pic': item.get('img', ''),
|
||||
'vod_remarks': self._format_time_cn(item.get('time', '')),
|
||||
'vod_url': url
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""首页:返回分类列表 + 首页视频"""
|
||||
classes = []
|
||||
for cat in CATEGORIES:
|
||||
classes.append({'type_id': cat['type_id'], 'type_name': cat['type_name']})
|
||||
|
||||
raw_data = self._xhttp({'play': 'list', 'page': 1})
|
||||
videos = self._build_vod_list(raw_data)
|
||||
|
||||
return {'class': classes, 'list': videos}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容"""
|
||||
raw_data = self._xhttp({'play': 'class', 'c': tid, 'page': pg})
|
||||
videos = self._build_vod_list(raw_data)
|
||||
|
||||
type_name = tid
|
||||
for cat in CATEGORIES:
|
||||
if cat['type_id'] == tid:
|
||||
type_name = cat['type_name']
|
||||
break
|
||||
|
||||
return {
|
||||
'page': int(pg),
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 9999,
|
||||
'type_name': type_name,
|
||||
'list': videos
|
||||
}
|
||||
|
||||
def detailContent(self, array):
|
||||
"""详情:通过xvid获取视频播放地址"""
|
||||
result = {}
|
||||
if not array or not array[0]:
|
||||
return result
|
||||
|
||||
xvid = array[0]
|
||||
vod = {
|
||||
'vod_id': xvid,
|
||||
'vod_name': '视频详情',
|
||||
'vod_pic': '',
|
||||
'vod_remarks': '',
|
||||
'vod_play_from': 'newxvideos',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
try:
|
||||
qs = urllib.parse.urlencode({'xvid': xvid})
|
||||
full_url = self.api_url + '?' + qs
|
||||
req = urllib.request.Request(full_url, headers=self.headers, method='GET')
|
||||
resp = urllib.request.urlopen(req, context=self._ssl_context, timeout=15)
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
except Exception as e:
|
||||
print('detailContent error: %s' % str(e), file=sys.stderr)
|
||||
result['list'] = [vod]
|
||||
return result
|
||||
|
||||
play_urls = []
|
||||
|
||||
if isinstance(data, dict):
|
||||
item = data
|
||||
if 'data' in data and isinstance(data['data'], dict):
|
||||
item = data['data']
|
||||
|
||||
hls_url = item.get('hls') or item.get('m3u8') or ''
|
||||
hight_url = item.get('hight') or item.get('high') or item.get('hd') or ''
|
||||
low_url = item.get('low') or item.get('sd') or ''
|
||||
|
||||
if hls_url:
|
||||
play_urls.append('高清HLS$' + hls_url)
|
||||
if hight_url:
|
||||
play_urls.append('高清MP4$' + hight_url)
|
||||
if low_url:
|
||||
play_urls.append('低清MP4$' + low_url)
|
||||
|
||||
title = item.get('title', '')
|
||||
if title:
|
||||
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
|
||||
if clean_title:
|
||||
vod['vod_name'] = clean_title
|
||||
|
||||
img = item.get('img', '')
|
||||
if img:
|
||||
vod['vod_pic'] = img
|
||||
|
||||
time_str = item.get('time', '')
|
||||
if time_str:
|
||||
vod['vod_remarks'] = self._format_time_cn(time_str)
|
||||
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
hls_url = item.get('hls') or item.get('m3u8') or ''
|
||||
hight_url = item.get('hight') or item.get('high') or item.get('hd') or ''
|
||||
low_url = item.get('low') or item.get('sd') or ''
|
||||
|
||||
if hls_url:
|
||||
play_urls.append('高清HLS$' + hls_url)
|
||||
if hight_url:
|
||||
play_urls.append('高清MP4$' + hight_url)
|
||||
if low_url:
|
||||
play_urls.append('低清MP4$' + low_url)
|
||||
|
||||
if vod['vod_name'] == '视频详情':
|
||||
title = item.get('title', '')
|
||||
if title:
|
||||
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
|
||||
if clean_title:
|
||||
vod['vod_name'] = clean_title
|
||||
img = item.get('img', '')
|
||||
if img:
|
||||
vod['vod_pic'] = img
|
||||
time_str = item.get('time', '')
|
||||
if time_str:
|
||||
vod['vod_remarks'] = self._format_time_cn(time_str)
|
||||
|
||||
if play_urls:
|
||||
vod['vod_play_url'] = '#'.join(play_urls)
|
||||
|
||||
result['list'] = [vod]
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
"""搜索"""
|
||||
raw_data = self._xhttp({'play': 'k', 'k': key, 'page': pg})
|
||||
videos = self._build_vod_list(raw_data)
|
||||
|
||||
return {
|
||||
'page': int(pg),
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 9999,
|
||||
'list': videos
|
||||
}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放地址解析 - 直接返回用户选择的清晰度地址"""
|
||||
if id and (id.startswith('http://') or id.startswith('https://')):
|
||||
return {
|
||||
'parse': 0,
|
||||
'playUrl': '',
|
||||
'url': id,
|
||||
'header': {
|
||||
'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.host + '/'
|
||||
}
|
||||
}
|
||||
return {'parse': 0, 'playUrl': '', 'url': '', 'header': {}}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def localProxy(self, param):
|
||||
return {}
|
||||
@@ -0,0 +1,360 @@
|
||||
# coding=utf-8
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
from base.spider import Spider
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "\u4e45\u4e45\u7f51"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://ww.jiujiu.one"
|
||||
print(f"Initialized with host: {self.host}")
|
||||
|
||||
def header(self):
|
||||
return {
|
||||
'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.host,
|
||||
'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',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""\u8fd4\u56de\u5206\u7c7b\u5217\u8868"""
|
||||
result = {}
|
||||
classes = [
|
||||
{"type_name": "亞洲無碼", "type_id": "68"},
|
||||
{"type_name": "日本女優", "type_id": "67"},
|
||||
{"type_name": "日本無碼", "type_id": "23"},
|
||||
{"type_name": "中文字幕", "type_id": "9"},
|
||||
{"type_name": "日本有碼", "type_id": "24"},
|
||||
{"type_name": "日韓無碼", "type_id": "82"},
|
||||
{"type_name": "無碼專區", "type_id": "113"},
|
||||
{"type_name": "AV明星", "type_id": "78"},
|
||||
{"type_name": "倫理影片", "type_id": "269"},
|
||||
{"type_name": "日本片商", "type_id": "90"},
|
||||
{"type_name": "國產自拍", "type_id": "80"},
|
||||
{"type_name": "傳媒原創", "type_id": "231"},
|
||||
{"type_name": "國產精品", "type_id": "63"},
|
||||
{"type_name": "國產情色", "type_id": "77"},
|
||||
{"type_name": "美女主播", "type_id": "105"},
|
||||
{"type_name": "強姦亂倫", "type_id": "33"},
|
||||
{"type_name": "國產主播", "type_id": "36"},
|
||||
{"type_name": "亞洲有碼", "type_id": "66"},
|
||||
{"type_name": "偷拍自拍", "type_id": "3"},
|
||||
{"type_name": "抖陰視頻", "type_id": "91"},
|
||||
{"type_name": "制服誘惑", "type_id": "31"},
|
||||
{"type_name": "黑料不打烊", "type_id": "10"},
|
||||
{"type_name": "歐美精品", "type_id": "25"},
|
||||
]
|
||||
result["class"] = classes
|
||||
result["list"] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""\u9996\u9875\u63a8\u8350\u89c6\u9891"""
|
||||
try:
|
||||
print("Fetching home page...")
|
||||
rsp = self.fetch(self.host, headers=self.header())
|
||||
print(f"Response status: {rsp.status}")
|
||||
|
||||
# \u4f7f\u7528\u6b63\u5219\u8865\u6551\u65b9\u6848\uff0c\u76f4\u63a5\u4eceHTML\u4e2d\u63d0\u53d6\u89c6\u9891\u4fe1\u606f
|
||||
html = rsp.text
|
||||
|
||||
# \u5339\u914d\u89c6\u9891\u9879\u6a21\u5f0f
|
||||
# \u67e5\u627e\u6240\u6709 div.item
|
||||
videos = []
|
||||
|
||||
# \u4f7f\u7528BeautifulSoup
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
# \u76f4\u63a5\u67e5\u627e\u6240\u6709\u5e26\u6709\u89c6\u9891\u7684\u5361\u7247
|
||||
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
|
||||
print(f"Found {len(items)} items with class containing 'item'")
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# \u627e\u5230\u94fe\u63a5
|
||||
links = item.find_all('a', href=True)
|
||||
if len(links) < 2:
|
||||
continue
|
||||
|
||||
# \u6807\u9898\u94fe\u63a5\u901a\u5e38\u662f\u7b2c\u4e8c\u4e2a
|
||||
title_link = links[1] if len(links) > 1 else links[0]
|
||||
href = title_link.get('href', '')
|
||||
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# \u6784\u5efa\u5b8c\u6574\u7684URL
|
||||
if href.startswith('/'):
|
||||
vod_id = self.host + href
|
||||
else:
|
||||
vod_id = href
|
||||
|
||||
# \u6807\u9898
|
||||
vod_name = title_link.get_text(strip=True)
|
||||
|
||||
# \u5c01\u9762\u56fe
|
||||
vod_pic = ''
|
||||
img = item.find('img')
|
||||
if img:
|
||||
vod_pic = img.get('src') or img.get('data-src') or ''
|
||||
if vod_pic and not vod_pic.startswith('http'):
|
||||
if vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
else:
|
||||
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
|
||||
|
||||
# \u5907\u6ce8
|
||||
vod_remarks = ''
|
||||
badge = item.find('span', class_='badge')
|
||||
if badge:
|
||||
vod_remarks = badge.get_text(strip=True)
|
||||
|
||||
if vod_name and vod_id:
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': vod_remarks
|
||||
})
|
||||
print(f"Added video: {vod_name}")
|
||||
except Exception as e:
|
||||
print(f"Error processing item: {e}")
|
||||
continue
|
||||
|
||||
print(f"Total videos extracted: {len(videos)}")
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
print(f"Error in homeVideoContent: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""\u5206\u7c7b\u9875\u5185\u5bb9"""
|
||||
try:
|
||||
# \u6784\u5efa\u5206\u7c7b\u9875URL\uff0c\u652f\u6301\u5206\u9875
|
||||
if pg == "1":
|
||||
url = f"{self.host}/c/{tid}"
|
||||
else:
|
||||
url = f"{self.host}/c/{tid}?page={pg}"
|
||||
|
||||
print(f"Fetching category URL: {url}")
|
||||
rsp = self.fetch(url, headers=self.header())
|
||||
soup = BeautifulSoup(rsp.text, 'html.parser')
|
||||
videos = []
|
||||
|
||||
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
links = item.find_all('a', href=True)
|
||||
if len(links) < 2:
|
||||
continue
|
||||
|
||||
title_link = links[1] if len(links) > 1 else links[0]
|
||||
href = title_link.get('href', '')
|
||||
|
||||
if not href:
|
||||
continue
|
||||
|
||||
if href.startswith('/'):
|
||||
vod_id = self.host + href
|
||||
else:
|
||||
vod_id = href
|
||||
|
||||
vod_name = title_link.get_text(strip=True)
|
||||
|
||||
vod_pic = ''
|
||||
img = item.find('img')
|
||||
if img:
|
||||
vod_pic = img.get('src') or img.get('data-src') or ''
|
||||
if vod_pic and not vod_pic.startswith('http'):
|
||||
if vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
else:
|
||||
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
|
||||
|
||||
vod_remarks = ''
|
||||
badge = item.find('span', class_='badge')
|
||||
if badge:
|
||||
vod_remarks = badge.get_text(strip=True)
|
||||
|
||||
if vod_name and vod_id:
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': vod_remarks
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error processing item: {e}")
|
||||
continue
|
||||
|
||||
# \u83b7\u53d6\u603b\u9875\u6570
|
||||
total_pages = 1
|
||||
pagination = soup.find('ul', class_='pagination')
|
||||
if pagination:
|
||||
page_links = pagination.find_all('a')
|
||||
for link in page_links:
|
||||
text = link.get_text(strip=True)
|
||||
if text.isdigit():
|
||||
page_num = int(text)
|
||||
if page_num > total_pages:
|
||||
total_pages = page_num
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': total_pages,
|
||||
'limit': len(videos),
|
||||
'total': total_pages * len(videos) if total_pages > 0 else len(videos)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error in categoryContent: {e}")
|
||||
return {'list': [], 'page': int(pg), 'pagecount': 1, 'limit': 0, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""\u8be6\u60c5\u9875\u5185\u5bb9"""
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
if not vod_id.startswith('http'):
|
||||
vod_id = self.host + vod_id
|
||||
|
||||
print(f"Fetching detail page: {vod_id}")
|
||||
rsp = self.fetch(vod_id, headers=self.header())
|
||||
soup = BeautifulSoup(rsp.text, 'html.parser')
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': '',
|
||||
'vod_pic': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': '',
|
||||
'vod_play_from': '\u4e45\u4e45\u7f51',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
# \u6807\u9898
|
||||
title = soup.find('h1')
|
||||
if title:
|
||||
vod['vod_name'] = title.get_text(strip=True)
|
||||
|
||||
# \u5c01\u9762
|
||||
img = soup.find('meta', property='og:image')
|
||||
if img and img.get('content'):
|
||||
vod['vod_pic'] = img.get('content')
|
||||
else:
|
||||
img = soup.find('img', class_='card-img-top')
|
||||
if img:
|
||||
vod['vod_pic'] = img.get('src') or ''
|
||||
|
||||
# \u64ad\u653e\u5730\u5740 - \u67e5\u627evideo\u6807\u7b64\u6216iframe
|
||||
play_url = ''
|
||||
video = soup.find('video')
|
||||
if video:
|
||||
source = video.find('source')
|
||||
if source and source.get('src'):
|
||||
play_url = source.get('src')
|
||||
|
||||
if not play_url:
|
||||
iframe = soup.find('iframe')
|
||||
if iframe and iframe.get('src'):
|
||||
play_url = iframe.get('src')
|
||||
|
||||
if play_url:
|
||||
vod['vod_play_url'] = f'\u6b63\u7247${play_url}'
|
||||
|
||||
return {'list': [vod]}
|
||||
except Exception as e:
|
||||
print(f"Error in detailContent: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
"""\u641c\u7d22\u529f\u80fd"""
|
||||
try:
|
||||
search_url = f"{self.host}/node/search?q={urllib.parse.quote(keyword)}"
|
||||
print(f"Search URL: {search_url}")
|
||||
rsp = self.fetch(search_url, headers=self.header())
|
||||
soup = BeautifulSoup(rsp.text, 'html.parser')
|
||||
videos = []
|
||||
|
||||
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
links = item.find_all('a', href=True)
|
||||
if len(links) < 2:
|
||||
continue
|
||||
|
||||
title_link = links[1] if len(links) > 1 else links[0]
|
||||
href = title_link.get('href', '')
|
||||
|
||||
if not href:
|
||||
continue
|
||||
|
||||
if href.startswith('/'):
|
||||
vod_id = self.host + href
|
||||
else:
|
||||
vod_id = href
|
||||
|
||||
vod_name = title_link.get_text(strip=True)
|
||||
|
||||
vod_pic = ''
|
||||
img = item.find('img')
|
||||
if img:
|
||||
vod_pic = img.get('src') or img.get('data-src') or ''
|
||||
if vod_pic and not vod_pic.startswith('http'):
|
||||
if vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
else:
|
||||
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
|
||||
|
||||
vod_remarks = ''
|
||||
badge = item.find('span', class_='badge')
|
||||
if badge:
|
||||
vod_remarks = badge.get_text(strip=True)
|
||||
|
||||
if vod_name and vod_id:
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_remarks': vod_remarks
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error processing search item: {e}")
|
||||
continue
|
||||
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
print(f"Error in searchContent: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""\u8fd4\u56de\u64ad\u653e\u5730\u5740"""
|
||||
return {
|
||||
'parse': 0,
|
||||
'playUrl': '',
|
||||
'url': id
|
||||
}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
"""\u5224\u65ad\u662f\u5426\u4e3a\u89c6\u9891\u683c\u5f0f"""
|
||||
video_extensions = ['.mp4', '.m3u8', '.flv', '.avi', '.mkv', '.wmv', '.mov']
|
||||
lower_url = url.lower()
|
||||
for ext in video_extensions:
|
||||
if ext in lower_url:
|
||||
return True
|
||||
return False
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,375 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/env python3
|
||||
# @name 咖啡直播
|
||||
# @author 转自 OmniBox JS
|
||||
# @description 体育赛事录像回放 + 直播(足球/篮球/NBA)
|
||||
# @version 2.0.0
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
class Spider:
|
||||
|
||||
def getName(self):
|
||||
return "咖啡直播"
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://kafeizhibo.com"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://kafeizhibo.com/live/all"
|
||||
}
|
||||
|
||||
def log(self, msg):
|
||||
print("[咖啡直播] " + str(msg))
|
||||
|
||||
# =========================================================
|
||||
# 工具
|
||||
# =========================================================
|
||||
|
||||
def _normalize_url(self, path):
|
||||
if not path:
|
||||
return ""
|
||||
if path.startswith("http"):
|
||||
return path
|
||||
if path.startswith("//"):
|
||||
return "https:" + path
|
||||
return self.host + ("" if path.startswith("/") else "/") + path
|
||||
|
||||
def _get(self, path, params=None, referer=None):
|
||||
h = dict(self.headers)
|
||||
if referer:
|
||||
h["Referer"] = referer
|
||||
resp = requests.get(self.host + path, headers=h, params=params, timeout=10)
|
||||
return resp.json()
|
||||
|
||||
# =========================================================
|
||||
# 直播部分
|
||||
# =========================================================
|
||||
|
||||
def _fetch_live_all(self):
|
||||
"""GET /api/v1/archor — 返回所有正在直播的频道"""
|
||||
return self._get("/api/v1/archor", referer=self.host + "/live/all")
|
||||
|
||||
def _parse_live_list(self, items, category_filter=None):
|
||||
"""
|
||||
category: 1=足球, 2=篮球, None=全部
|
||||
每个 archor 代表一个独立直播频道(同一场球可能有多个频道)
|
||||
合并同 match_id 的频道到一个 vod,多线路在 detail 里处理
|
||||
"""
|
||||
# 按 room_id 去重(同一 room_id 只取第一个,避免重复)
|
||||
seen_rooms = set()
|
||||
result = []
|
||||
for item in items:
|
||||
if category_filter and item.get("category") != category_filter:
|
||||
continue
|
||||
room_id = str(item.get("room_id", ""))
|
||||
if room_id in seen_rooms:
|
||||
continue
|
||||
seen_rooms.add(room_id)
|
||||
|
||||
home = item.get("home_team", "")
|
||||
away = item.get("away_team", "")
|
||||
league = item.get("league_name", "")
|
||||
h_score = item.get("home_score", 0)
|
||||
a_score = item.get("away_score", 0)
|
||||
title = "{} vs {} ({})".format(home, away, league)
|
||||
|
||||
pic = self._normalize_url(item.get("screenshot", ""))
|
||||
if not pic or "default" in pic:
|
||||
mi = item.get("match_info") or {}
|
||||
pic = mi.get("home_team_logo", "")
|
||||
|
||||
result.append({
|
||||
"vod_id": "live_{}".format(room_id),
|
||||
"vod_name": "🔴 " + title,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": "{} - {} | {}".format(h_score, a_score, item.get("name", "")),
|
||||
})
|
||||
return result
|
||||
|
||||
def _detail_live(self, room_id):
|
||||
"""GET /api/v1/room/{room_id} — 获取直播间多线路"""
|
||||
try:
|
||||
data = self._get(
|
||||
"/api/v1/room/{}".format(room_id),
|
||||
referer=self.host + "/room/{}".format(room_id)
|
||||
)
|
||||
if data.get("code") != 200 or not data.get("data"):
|
||||
return {"list": []}
|
||||
|
||||
d = data["data"]
|
||||
room_info = d.get("room_info", {})
|
||||
signals = d.get("signals", [])
|
||||
|
||||
home = room_info.get("home_team", "")
|
||||
away = room_info.get("away_team", "")
|
||||
league = room_info.get("league", "")
|
||||
h_score = room_info.get("home_score", 0)
|
||||
a_score = room_info.get("away_score", 0)
|
||||
title = "{} vs {} ({})".format(home, away, league)
|
||||
|
||||
teams = d.get("teams", {})
|
||||
pic = (teams.get("home") or {}).get("logo", "")
|
||||
|
||||
# 每条 signal 是一个线路(官方直播/原声直播)
|
||||
episodes = []
|
||||
for sig in signals:
|
||||
url = sig.get("stream_url", "")
|
||||
if url:
|
||||
name = sig.get("name", "线路")
|
||||
episodes.append("{}${}".format(name, url))
|
||||
|
||||
# 如果 signals 为空,fallback 到 archor
|
||||
if not episodes:
|
||||
archor = d.get("archor", {})
|
||||
url = archor.get("stream_url", "")
|
||||
if url:
|
||||
episodes.append("{}${}".format(archor.get("name", "直播"), url))
|
||||
|
||||
vod = {
|
||||
"vod_id": "live_{}".format(room_id),
|
||||
"vod_name": "🔴 " + title,
|
||||
"vod_pic": pic,
|
||||
"vod_content": "{} {} vs {},比分 {} - {}".format(
|
||||
league, home, away, h_score, a_score
|
||||
),
|
||||
"vod_play_from": "直播线路",
|
||||
"vod_play_url": "#".join(episodes),
|
||||
}
|
||||
return {"list": [vod]}
|
||||
except Exception as e:
|
||||
self.log("直播详情失败: " + str(e))
|
||||
return {"list": []}
|
||||
|
||||
# =========================================================
|
||||
# 录像部分
|
||||
# =========================================================
|
||||
|
||||
def _fetch_recordings(self, page=1, size=30, league=None, type_id=None):
|
||||
params = {"page": page, "size": size}
|
||||
if league:
|
||||
params["league"] = league
|
||||
elif type_id and type_id not in ("all", "nba", "live_all", "live_1", "live_2"):
|
||||
params["type"] = type_id
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/pc/replay"
|
||||
resp = requests.get(self.host + "/api/v1/recordings", headers=h, params=params, timeout=10)
|
||||
return resp.json()
|
||||
|
||||
def _parse_video_list(self, items):
|
||||
result = []
|
||||
for item in items:
|
||||
title = "{} vs {} ({})".format(
|
||||
item["home_team"], item["away_team"], item["league_name"]
|
||||
)
|
||||
score = "{} - {}".format(item["home_score"], item["away_score"])
|
||||
pic = item.get("cover_image", "")
|
||||
if pic and not pic.startswith("http"):
|
||||
pic = self._normalize_url(pic)
|
||||
if not pic or "default_cover" in pic:
|
||||
pic = item.get("home_team_logo", "")
|
||||
remarks = "{} | {} | {}个录像".format(
|
||||
score, item["start_time"], item.get("recording_count", 0)
|
||||
)
|
||||
result.append({
|
||||
"vod_id": str(item["match_id"]),
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks,
|
||||
})
|
||||
return result
|
||||
|
||||
def _detail_recording(self, vid):
|
||||
try:
|
||||
h = dict(self.headers)
|
||||
h["Referer"] = self.host + "/pc/replay"
|
||||
resp = requests.get(
|
||||
"{}/api/v1/match/{}/recordings".format(self.host, vid),
|
||||
headers=h,
|
||||
timeout=10
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("code") != 200 or not data.get("data"):
|
||||
return {"list": []}
|
||||
|
||||
match = data["data"]["match"]
|
||||
replays = data["data"].get("replays", [])
|
||||
highlights = data["data"].get("highlights", [])
|
||||
|
||||
title = "{} vs {} ({})".format(
|
||||
match["home_team"], match["away_team"], match["league_name"]
|
||||
)
|
||||
pic = match.get("home_team_logo") or match.get("away_team_logo") or ""
|
||||
|
||||
episodes = []
|
||||
for idx, rec in enumerate(replays):
|
||||
if rec.get("video_url"):
|
||||
name = rec.get("title") or "录像{}".format(idx + 1)
|
||||
episodes.append("{}${}".format(name, rec["video_url"]))
|
||||
for idx, rec in enumerate(highlights):
|
||||
if rec.get("video_url"):
|
||||
name = rec.get("title") or "集锦{}".format(idx + 1)
|
||||
episodes.append("{}${}".format(name, rec["video_url"]))
|
||||
|
||||
vod = {
|
||||
"vod_id": str(vid),
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"vod_content": "{} {} {} vs {},比分 {} - {},比赛时间:{}".format(
|
||||
match["league_name"], match.get("match_round", ""),
|
||||
match["home_team"], match["away_team"],
|
||||
match["home_score"], match["away_score"],
|
||||
match["start_time"]
|
||||
),
|
||||
"vod_play_from": "录像源",
|
||||
"vod_play_url": "#".join(episodes),
|
||||
}
|
||||
return {"list": [vod]}
|
||||
except Exception as e:
|
||||
self.log("录像详情失败: " + str(e))
|
||||
return {"list": []}
|
||||
|
||||
# =========================================================
|
||||
# FongMi 接口
|
||||
# =========================================================
|
||||
|
||||
def homeContent(self, filter):
|
||||
categories = [
|
||||
# 直播分类
|
||||
{"type_id": "live_all", "type_name": "🔴 直播全部"},
|
||||
{"type_id": "live_1", "type_name": "🔴 直播足球"},
|
||||
{"type_id": "live_2", "type_name": "🔴 直播篮球"},
|
||||
# 录像分类
|
||||
{"type_id": "all", "type_name": "录像全部"},
|
||||
{"type_id": "1", "type_name": "录像足球"},
|
||||
{"type_id": "2", "type_name": "录像篮球"},
|
||||
{"type_id": "nba", "type_name": "录像NBA"},
|
||||
]
|
||||
# 首页展示直播列表
|
||||
try:
|
||||
data = self._fetch_live_all()
|
||||
vod_list = self._parse_live_list(data.get("data", [])) if data.get("code") == 200 else []
|
||||
except Exception as e:
|
||||
self.log("首页失败: " + str(e))
|
||||
vod_list = []
|
||||
return {"class": categories, "list": vod_list}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {"list": []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
pg = int(pg) if pg else 1
|
||||
|
||||
# ---- 直播分类 ----
|
||||
if tid in ("live_all", "live_1", "live_2"):
|
||||
try:
|
||||
data = self._fetch_live_all()
|
||||
if data.get("code") == 200:
|
||||
cat = None if tid == "live_all" else int(tid.split("_")[1])
|
||||
vod_list = self._parse_live_list(data.get("data", []), category_filter=cat)
|
||||
else:
|
||||
vod_list = []
|
||||
except Exception as e:
|
||||
self.log("直播分类失败: " + str(e))
|
||||
vod_list = []
|
||||
return {"list": vod_list, "page": 1, "pagecount": 1, "limit": 100, "total": len(vod_list)}
|
||||
|
||||
# ---- 录像分类 ----
|
||||
try:
|
||||
if tid == "nba":
|
||||
data = self._fetch_recordings(pg, 20, league="NBA")
|
||||
size = 20
|
||||
elif tid == "all":
|
||||
data = self._fetch_recordings(pg, 30)
|
||||
size = 30
|
||||
else:
|
||||
data = self._fetch_recordings(pg, 30, type_id=tid)
|
||||
size = 30
|
||||
|
||||
vod_list = []
|
||||
pagecount = 1
|
||||
if data.get("code") == 200 and data.get("data"):
|
||||
vod_list = self._parse_video_list(data["data"])
|
||||
pagecount = pg + 1 if len(data["data"]) == size else pg
|
||||
except Exception as e:
|
||||
self.log("录像分类失败: " + str(e))
|
||||
vod_list = []
|
||||
pagecount = 1
|
||||
|
||||
return {"list": vod_list, "page": pg, "pagecount": pagecount, "limit": 30, "total": len(vod_list)}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0] if isinstance(ids, list) and ids else str(ids)
|
||||
if vid.startswith("live_"):
|
||||
room_id = vid[5:] # 去掉 "live_" 前缀
|
||||
return self._detail_live(room_id)
|
||||
else:
|
||||
return self._detail_recording(vid)
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
if not key:
|
||||
return {"list": []}
|
||||
keyword = key.lower()
|
||||
result = []
|
||||
|
||||
# 搜索直播
|
||||
try:
|
||||
data = self._fetch_live_all()
|
||||
if data.get("code") == 200:
|
||||
for item in data.get("data", []):
|
||||
if (keyword in item.get("home_team", "").lower()
|
||||
or keyword in item.get("away_team", "").lower()
|
||||
or keyword in item.get("league_name", "").lower()
|
||||
or keyword in item.get("title", "").lower()):
|
||||
room_id = str(item.get("room_id", ""))
|
||||
home = item.get("home_team", "")
|
||||
away = item.get("away_team", "")
|
||||
league = item.get("league_name", "")
|
||||
result.append({
|
||||
"vod_id": "live_{}".format(room_id),
|
||||
"vod_name": "🔴 {} vs {} ({})".format(home, away, league),
|
||||
"vod_pic": "",
|
||||
"vod_remarks": "直播中",
|
||||
})
|
||||
except Exception as e:
|
||||
self.log("搜索直播失败: " + str(e))
|
||||
|
||||
# 搜索录像
|
||||
try:
|
||||
data = self._fetch_recordings(1, 100)
|
||||
if data.get("code") == 200:
|
||||
for item in data["data"]:
|
||||
if (keyword in item["home_team"].lower()
|
||||
or keyword in item["away_team"].lower()
|
||||
or keyword in item["league_name"].lower()):
|
||||
title = "{} vs {} ({})".format(
|
||||
item["home_team"], item["away_team"], item["league_name"]
|
||||
)
|
||||
result.append({
|
||||
"vod_id": str(item["match_id"]),
|
||||
"vod_name": title,
|
||||
"vod_pic": "",
|
||||
"vod_remarks": "{} - {}".format(item["home_score"], item["away_score"]),
|
||||
})
|
||||
except Exception as e:
|
||||
self.log("搜索录像失败: " + str(e))
|
||||
|
||||
return {"list": result, "page": 1, "pagecount": 1}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": id,
|
||||
"header": json.dumps({
|
||||
"User-Agent": self.headers["User-Agent"],
|
||||
"Referer": self.host,
|
||||
"Origin": self.host,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
# coding = utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://app.whjzjx.cn"
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Linux; Android 12; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36'
|
||||
}
|
||||
|
||||
headerf = {
|
||||
"platform": "1",
|
||||
"user_agent": "Mozilla/5.0 (Linux; Android 9; V1938T Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36",
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
times = int(time.time() * 1000)
|
||||
|
||||
data = {
|
||||
"device": "2a50580e69d38388c94c93605241fb306",
|
||||
"package_name": "com.jz.xydj",
|
||||
"android_id": "ec1280db12795506",
|
||||
"install_first_open": True,
|
||||
"first_install_time": 1752505243345,
|
||||
"last_update_time": 1752505243345,
|
||||
"report_link_url": "",
|
||||
"authorization": "",
|
||||
"timestamp": times
|
||||
}
|
||||
|
||||
plain_text = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
key = "B@ecf920Od8A4df7"
|
||||
key_bytes = key.encode('utf-8')
|
||||
plain_bytes = plain_text.encode('utf-8')
|
||||
cipher = AES.new(key_bytes, AES.MODE_ECB)
|
||||
padded_data = pad(plain_bytes, AES.block_size)
|
||||
ciphertext = cipher.encrypt(padded_data)
|
||||
encrypted = base64.b64encode(ciphertext).decode('utf-8')
|
||||
|
||||
response = requests.post("https://u.shytkjgs.com/user/v3/account/login", headers=headerf, data=encrypted)
|
||||
response_data = response.json()
|
||||
Authorization = response_data['data']['token']
|
||||
|
||||
headerx = {
|
||||
'authorization': Authorization,
|
||||
'platform': '1',
|
||||
'version_name': '3.8.3.1'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
global headers
|
||||
|
||||
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": "1", "type_name": "剧场"},
|
||||
{"type_id": "3", "type_name": "新剧"},
|
||||
{"type_id": "2", "type_name": "热播"},
|
||||
{"type_id": "7", "type_name": "星选"},
|
||||
{"type_id": "5", "type_name": "阳光"}],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
|
||||
url= f'{xurl}/v1/theater/home_page?theater_class_id=1&class2_id=4&page_num=1&page_size=24'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['list']:
|
||||
|
||||
name = vod['theater']['title']
|
||||
|
||||
id = vod['theater']['id']
|
||||
|
||||
pic = vod['theater']['cover_url']
|
||||
|
||||
remark = vod['theater']['play_amount_str']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
url = f'{xurl}/v1/theater/home_page?theater_class_id={cid}&page_num={pg}&page_size=24'
|
||||
detail = requests.get(url=url,headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['list']:
|
||||
|
||||
name = vod['theater']['title']
|
||||
|
||||
id = vod['theater']['id']
|
||||
|
||||
pic = vod['theater']['cover_url']
|
||||
|
||||
remark = vod['theater']['theme']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
xianlu = ''
|
||||
bofang = ''
|
||||
|
||||
url = f'{xurl}/v2/theater_parent/detail?theater_parent_id={did}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
if detail.status_code == 200:
|
||||
data = detail.json()
|
||||
|
||||
url = 'https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/1732707176882/jiduo.txt'
|
||||
response = requests.get(url)
|
||||
response.encoding = 'utf-8'
|
||||
code = response.text
|
||||
name = self.extract_middle_text(code, "s1='", "'", 0)
|
||||
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
|
||||
|
||||
content = '剧情:' + data['data']['introduction']
|
||||
|
||||
area = data['data']['desc_tags'][0]
|
||||
|
||||
remarks = data['data']['filing']
|
||||
|
||||
# 修复剧集只有一集的问题 - 检查theaters数据是否存在且不为空
|
||||
if 'theaters' in data['data'] and data['data']['theaters']:
|
||||
for sou in data['data']['theaters']:
|
||||
id = sou['son_video_url']
|
||||
name = sou['num']
|
||||
bofang = bofang + str(name) + '$' + id + '#'
|
||||
|
||||
bofang = bofang[:-1] if bofang.endswith('#') else bofang
|
||||
xianlu = '星芽'
|
||||
else:
|
||||
# 如果没有theaters数据,检查是否有单个视频URL
|
||||
if 'video_url' in data['data'] and data['data']['video_url']:
|
||||
bofang = '1$' + data['data']['video_url']
|
||||
xianlu = '星芽'
|
||||
else:
|
||||
bofang = Jumps
|
||||
xianlu = '1'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_content": content,
|
||||
"vod_remarks": remarks,
|
||||
"vod_area": area,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = id
|
||||
result["header"] = headers
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
payload = {
|
||||
"text": key
|
||||
}
|
||||
|
||||
url = f"{xurl}/v3/search"
|
||||
detail = requests.post(url=url, headers=headerx, json=payload)
|
||||
if detail.status_code == 200:
|
||||
detail.encoding = "utf-8"
|
||||
data = detail.json()
|
||||
|
||||
for vod in data['data']['theater']['search_data']:
|
||||
|
||||
name = vod['title']
|
||||
|
||||
id = vod['id']
|
||||
|
||||
pic = vod['cover_url']
|
||||
|
||||
remark = vod['score_str']
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = page
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
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
|
||||
@@ -0,0 +1,392 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re, urllib.parse
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.ht10010.com"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return '枫叶影院'
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {"class": [
|
||||
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
|
||||
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
|
||||
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
|
||||
{"type_id": "2", "type_name": "电视剧"},
|
||||
{"type_id": "1", "type_name": "电影"},
|
||||
{"type_id": "4", "type_name": "动漫"},
|
||||
{"type_id": "3", "type_name": "综艺"},
|
||||
{"type_id": "5", "type_name": "热门短剧"},
|
||||
], "filters": self._build_filters()}
|
||||
|
||||
def _build_filters(self):
|
||||
area = [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "泰国", "v": "泰国"}, {"n": "新加坡", "v": "新加坡"},
|
||||
{"n": "马来西亚", "v": "马来西亚"}, {"n": "印度", "v": "印度"}, {"n": "英国", "v": "英国"},
|
||||
{"n": "法国", "v": "法国"}, {"n": "加拿大", "v": "加拿大"}, {"n": "西班牙", "v": "西班牙"},
|
||||
{"n": "俄罗斯", "v": "俄罗斯"}, {"n": "其它", "v": "其它"}]
|
||||
year = [{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"}, {"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"}, {"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"}, {"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}, {"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"},
|
||||
{"n": "2015", "v": "2015"}, {"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"},
|
||||
{"n": "2012", "v": "2012"}, {"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"},
|
||||
{"n": "2009", "v": "2009"}, {"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"},
|
||||
{"n": "2006", "v": "2006"}, {"n": "2005", "v": "2005"}, {"n": "2004", "v": "2004"}]
|
||||
lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
|
||||
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
|
||||
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
|
||||
{"n": "其它", "v": "其它"}]
|
||||
sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}]
|
||||
letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"},
|
||||
{"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"},
|
||||
{"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"},
|
||||
{"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"},
|
||||
{"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"},
|
||||
{"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"},
|
||||
{"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}]
|
||||
return {
|
||||
"2": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"},
|
||||
{"n": "海外剧", "v": "16"}]},
|
||||
{"key": "area", "name": "地区", "value": area},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("古装", "古装"), ("战争", "战争"),
|
||||
("青春偶像", "青春偶像"), ("喜剧", "喜剧"),
|
||||
("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"),
|
||||
("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"),
|
||||
("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"),
|
||||
("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"1": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
|
||||
{"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"},
|
||||
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]},
|
||||
{"key": "area", "name": "地区", "value": area},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"),
|
||||
("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"),
|
||||
("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"),
|
||||
("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"),
|
||||
("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"),
|
||||
("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"),
|
||||
("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"),
|
||||
("古装", "古装"), ("历史", "历史"), ("运动", "运动"),
|
||||
("农村", "农村"), ("儿童", "儿童"),
|
||||
("网络电影", "网络电影")]]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"4": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("情感", "情感"), ("科幻", "科幻"),
|
||||
("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"),
|
||||
("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"),
|
||||
("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"),
|
||||
("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"),
|
||||
("励志", "励志"), ("其他", "其他")]]},
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
|
||||
{"n": "其它", "v": "其它"}]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
"3": [
|
||||
{"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]},
|
||||
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
|
||||
[("全部", ""), ("选秀", "选秀"), ("情感", "情感"),
|
||||
("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"),
|
||||
("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"),
|
||||
("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]},
|
||||
{"key": "area", "name": "地区",
|
||||
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
|
||||
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
|
||||
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]},
|
||||
{"key": "year", "name": "年份", "value": year},
|
||||
{"key": "lang", "name": "语言", "value": lang},
|
||||
{"key": "letter", "name": "字母", "value": letter},
|
||||
{"key": "sort", "name": "排序", "value": sort},
|
||||
],
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
html = self._fetch('/')
|
||||
return {"list": self._parse_video_list(html)}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
# 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter
|
||||
if tid.startswith('/label'):
|
||||
url = f'{tid}/page/{pg}.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
page = int(pg)
|
||||
page_count = page if len(items) < 24 else page + 2
|
||||
return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24}
|
||||
|
||||
args = {}
|
||||
if extend and isinstance(extend, dict):
|
||||
for k, v in extend.items():
|
||||
if v:
|
||||
args[k] = str(v)
|
||||
if isinstance(filter, dict):
|
||||
for k, v in filter.items():
|
||||
if v and k not in args:
|
||||
args[k] = str(v)
|
||||
route_tid = args.get('class', args.get('tid', str(tid)))
|
||||
area = args.get('area', '')
|
||||
genre = args.get('genre', '')
|
||||
year = args.get('year', '')
|
||||
lang = args.get('lang', '')
|
||||
letter = args.get('letter', '')
|
||||
sort = args.get('sort', '')
|
||||
# 无筛选走正常分页
|
||||
if not area and not genre and not year and not lang and not letter and not sort:
|
||||
url = f'/cupfox-list/{route_tid}--------{pg}---.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
page = int(pg)
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
pagecount = page
|
||||
for a in soup.select('a.page-link'):
|
||||
if a.text == '尾页':
|
||||
m = re.search(r'---(\d+)---', a.get('href', ''))
|
||||
if m:
|
||||
pagecount = int(m.group(1))
|
||||
break
|
||||
if not items:
|
||||
pagecount = 0
|
||||
return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999}
|
||||
# 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html
|
||||
segs = [route_tid, area, sort, genre, lang, letter, '', '', year]
|
||||
url = '/cupfox-list/' + '-'.join(segs) + '.html'
|
||||
html = self._fetch(url)
|
||||
items = self._parse_video_list(html)
|
||||
return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {"list": []}
|
||||
vid = ids[0].split(',')[0].strip()
|
||||
try:
|
||||
html = self._fetch(f'/detail/{vid}.html')
|
||||
if not html: return result
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
vod_name = soup.select_one('h3.slide-info-title')
|
||||
vod_name = vod_name.text.strip() if vod_name else ''
|
||||
vod_pic = soup.select_one('img.lazy')
|
||||
vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else ''
|
||||
vod_director = ''
|
||||
vod_actor = ''
|
||||
for el in soup.select('.slide-info'):
|
||||
text = el.get_text(' ').strip()
|
||||
if text.startswith('导演:'):
|
||||
vod_director = text.replace('导演:', '').strip()
|
||||
elif text.startswith('演员:'):
|
||||
vod_actor = text.replace('演员:', '').strip()
|
||||
vod_content = soup.select_one('#height_limit')
|
||||
vod_content = vod_content.get_text(' ', strip=True) if vod_content else ''
|
||||
play_from, play_url = [], []
|
||||
for tab in soup.select('.anthology-tab a.swiper-slide'):
|
||||
src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip()
|
||||
if src_name:
|
||||
play_from.append(src_name)
|
||||
tab_blocks = soup.select('.anthology-list-box')
|
||||
for i, block in enumerate(tab_blocks):
|
||||
ep_list = []
|
||||
for a in block.select('li a'):
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/play/(.*?)\.html', href)
|
||||
if m:
|
||||
ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}')
|
||||
ep_list.reverse()
|
||||
if ep_list and i < len(play_from):
|
||||
play_url.append('#'.join(ep_list))
|
||||
valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)]
|
||||
result["list"].append({
|
||||
"vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic,
|
||||
"vod_director": vod_director, "vod_actor": vod_actor,
|
||||
"vod_content": vod_content,
|
||||
"vod_play_from": "$$$".join(valid_from),
|
||||
"vod_play_url": "$$$".join(play_url),
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
decoded = urllib.parse.unquote(key)
|
||||
except:
|
||||
decoded = key
|
||||
html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html')
|
||||
items = self._parse_search_list(html)
|
||||
return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = ''
|
||||
try:
|
||||
url = id if id.startswith('http') else f'{self.host}/play/{id}.html'
|
||||
html = self._fetch(url)
|
||||
if html:
|
||||
m = re.search(r'player_aaaa=(.*?)</script>', html, re.S)
|
||||
if m:
|
||||
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pd = {}
|
||||
# print('pd:', pd)
|
||||
play_url = pd.get('url')
|
||||
play_id = pd.get('from')
|
||||
|
||||
api_map = {
|
||||
'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php',
|
||||
'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php',
|
||||
}
|
||||
if not play_url:
|
||||
return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8',
|
||||
"header": {'User-Agent': 'Mozilla/5.0'}}
|
||||
if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')):
|
||||
return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}}
|
||||
|
||||
else:
|
||||
headers = {
|
||||
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.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://www.ht10010.com/",
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers)
|
||||
token = re.search(r'data-te="(.*?)"', response.text)
|
||||
if token:
|
||||
token = token.group(1)
|
||||
payload = {
|
||||
'url': play_url,
|
||||
'token': token
|
||||
}
|
||||
# print('payload', payload)
|
||||
try:
|
||||
response = self.post(api_map[play_id], data=payload, headers=headers)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
# print('result:', result)
|
||||
if result['code'] == 200 and 'url' in result:
|
||||
play_url = result['url']
|
||||
return {"parse": 0, "url": play_url, "header": {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'}}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {"parse": 1, "url": url}
|
||||
|
||||
def localProxy(self, param=''):
|
||||
return {}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def _fetch(self, url):
|
||||
try:
|
||||
if not url.startswith('http'):
|
||||
url = self.host + url
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
return rsp.text if rsp else ''
|
||||
except:
|
||||
return ''
|
||||
|
||||
def _fix_pic(self, u):
|
||||
if not u: return ''
|
||||
if u.startswith('//'): return 'https:' + u
|
||||
return u.replace('&', '&')
|
||||
|
||||
def _parse_video_list(self, html):
|
||||
videos, seen = [], set()
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
cards = soup.select('a.public-list-exp')
|
||||
for a in cards:
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not m: continue
|
||||
vod_id = m.group(1)
|
||||
if vod_id in seen: continue
|
||||
seen.add(vod_id)
|
||||
span = ','.join([span.text for span in a.select('span.public-prt')])
|
||||
# print('span', span)
|
||||
vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or ''
|
||||
pic_el = a.select_one('img')
|
||||
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
|
||||
remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb')
|
||||
vod_remarks = remark_el.text.strip() if remark_el else ''
|
||||
videos.append(
|
||||
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span})
|
||||
return videos
|
||||
|
||||
def _parse_search_list(self, html):
|
||||
videos, seen = [], set()
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
cards = soup.select('a.public-list-exp')
|
||||
for a in cards:
|
||||
href = a.get('href', '')
|
||||
m = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not m: continue
|
||||
vod_id = m.group(1)
|
||||
if vod_id in seen: continue
|
||||
seen.add(vod_id)
|
||||
pic_el = a.select_one('img')
|
||||
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
|
||||
title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]')
|
||||
if title_el:
|
||||
vod_name = title_el.text.strip()
|
||||
else:
|
||||
vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or ''
|
||||
remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2')
|
||||
vod_remarks = remark_el.text.strip() if remark_el else ''
|
||||
videos.append(
|
||||
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
|
||||
return videos
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sp = Spider()
|
||||
sp.init()
|
||||
# 20067-5-189
|
||||
print(sp.categoryContent('/label/qq','1',True, {}))
|
||||
# print(sp.playerContent('', '20067-6-189', []))
|
||||
# print(sp.playerContent('', '20067-5-189', []))
|
||||
pass
|
||||
@@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
sys.path.append('../../')
|
||||
try:
|
||||
from base.spider import Spider
|
||||
except ImportError:
|
||||
# 定义一个基础接口类,用于本地测试
|
||||
class Spider:
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.siteUrl = "https://www.kuaikaw.cn"
|
||||
self.cateManual = {
|
||||
"甜宠": "462",
|
||||
"古装仙侠": "1102",
|
||||
"现代言情": "1145",
|
||||
"青春": "1170",
|
||||
"豪门恩怨": "585",
|
||||
"逆袭": "417-464",
|
||||
"重生": "439-465",
|
||||
"系统": "1159",
|
||||
"总裁": "1147",
|
||||
"职场商战": "943"
|
||||
}
|
||||
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 Edg/120.0.0.0",
|
||||
"Referer": self.siteUrl,
|
||||
"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,en;q=0.8"
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return "河马短剧"
|
||||
|
||||
def init(self, extend=""):
|
||||
return
|
||||
|
||||
def fetch(self, url, headers=None, retry=2):
|
||||
"""统一的网络请求接口"""
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
for i in range(retry + 1):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
if i == retry:
|
||||
print(f"请求异常: {url}, 错误: {str(e)}")
|
||||
return None
|
||||
continue
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.mp4', '.mkv', '.avi', '.wmv', '.m3u8', '.flv', '.rmvb']
|
||||
return any(format in url.lower() for format in video_formats)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in self.cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
try:
|
||||
result['list'] = self.homeVideoContent()['list']
|
||||
except:
|
||||
result['list'] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
response = self.fetch(self.siteUrl)
|
||||
if not response:
|
||||
return {'list': []}
|
||||
|
||||
html_content = response.text
|
||||
next_data_pattern = r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>'
|
||||
next_data_match = re.search(next_data_pattern, html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return {'list': []}
|
||||
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
# 处理轮播图数据
|
||||
if "bannerList" in page_props:
|
||||
for banner in page_props["bannerList"]:
|
||||
if banner.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{banner['bookId']}",
|
||||
"vod_name": banner.get("bookName", ""),
|
||||
"vod_pic": banner.get("coverWap", ""),
|
||||
"vod_remarks": f"{banner.get('statusDesc', '')} {banner.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 处理SEO分类推荐
|
||||
if "seoColumnVos" in page_props:
|
||||
for column in page_props["seoColumnVos"]:
|
||||
for book in column.get("bookInfos", []):
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 去重处理
|
||||
seen = set()
|
||||
unique_videos = []
|
||||
for video in videos:
|
||||
key = (video["vod_id"], video["vod_name"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_videos.append(video)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取首页推荐内容出错: {e}")
|
||||
unique_videos = []
|
||||
|
||||
return {'list': unique_videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
url = f"{self.siteUrl}/browse/{tid}/{pg}"
|
||||
|
||||
response = self.fetch(url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
current_page = page_props.get("page", 1)
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'page': int(current_page),
|
||||
'pagecount': total_pages,
|
||||
'limit': len(videos),
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"分类内容获取出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
return self.searchContentPage(key, quick, pg)
|
||||
|
||||
def searchContentPage(self, key, quick, pg=1):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
search_url = f"{self.siteUrl}/search?searchValue={quote(key)}&page={pg}"
|
||||
|
||||
response = self.fetch(search_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'pagecount': total_pages,
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索内容出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
if not ids:
|
||||
return result
|
||||
|
||||
vod_id = ids[0]
|
||||
if not vod_id.startswith('/drama/'):
|
||||
vod_id = f'/drama/{vod_id}'
|
||||
|
||||
drama_url = f"{self.siteUrl}{vod_id}"
|
||||
response = self.fetch(drama_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
book_info = page_props.get("bookInfoVo", {})
|
||||
chapter_list = page_props.get("chapterList", [])
|
||||
|
||||
if not book_info.get("bookId"):
|
||||
return result
|
||||
|
||||
# 基本信息
|
||||
categories = [c.get("name", "") for c in book_info.get("categoryList", [])]
|
||||
performers = [p.get("name", "") for p in book_info.get("performerList", [])]
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": book_info.get("title", ""),
|
||||
"vod_pic": book_info.get("coverWap", ""),
|
||||
"type_name": ",".join(categories),
|
||||
"vod_year": "",
|
||||
"vod_area": book_info.get("countryName", ""),
|
||||
"vod_remarks": f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}集".strip(),
|
||||
"vod_actor": ", ".join(performers),
|
||||
"vod_director": "",
|
||||
"vod_content": book_info.get("introduction", "")
|
||||
}
|
||||
|
||||
# 处理剧集
|
||||
play_urls = self.processEpisodes(vod_id, chapter_list)
|
||||
if play_urls:
|
||||
vod['vod_play_from'] = '河马剧场'
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
|
||||
result['list'] = [vod]
|
||||
|
||||
except Exception as e:
|
||||
print(f"详情页解析出错: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
return result
|
||||
|
||||
def processEpisodes(self, vod_id, chapter_list):
|
||||
play_urls = []
|
||||
episodes = []
|
||||
|
||||
for chapter in chapter_list:
|
||||
chapter_id = chapter.get("chapterId", "")
|
||||
chapter_name = chapter.get("chapterName", "")
|
||||
|
||||
if not chapter_id or not chapter_name:
|
||||
continue
|
||||
|
||||
# 尝试获取直接视频链接
|
||||
video_url = self.getDirectVideoUrl(chapter)
|
||||
if video_url:
|
||||
episodes.append(f"{chapter_name}${video_url}")
|
||||
continue
|
||||
|
||||
# 回退方案
|
||||
episodes.append(f"{chapter_name}${vod_id}${chapter_id}${chapter_name}")
|
||||
|
||||
if episodes:
|
||||
play_urls.append("#".join(episodes))
|
||||
|
||||
return play_urls
|
||||
|
||||
def getDirectVideoUrl(self, chapter):
|
||||
if "chapterVideoVo" not in chapter or not chapter["chapterVideoVo"]:
|
||||
return None
|
||||
|
||||
video_info = chapter["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
return None
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {
|
||||
"parse": 0,
|
||||
"url": id,
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
|
||||
# 如果已经是视频链接直接返回
|
||||
if 'http' in id and ('.mp4' in id or '.m3u8' in id):
|
||||
return result
|
||||
|
||||
# 解析参数
|
||||
parts = id.split('$')
|
||||
if len(parts) < 2:
|
||||
return result
|
||||
|
||||
drama_id = parts[0].replace('/drama/', '')
|
||||
chapter_id = parts[1]
|
||||
|
||||
# 尝试获取视频链接
|
||||
video_url = self.getEpisodeVideoUrl(drama_id, chapter_id)
|
||||
if video_url:
|
||||
result["url"] = video_url
|
||||
|
||||
return result
|
||||
|
||||
def getEpisodeVideoUrl(self, drama_id, chapter_id):
|
||||
episode_url = f"{self.siteUrl}/episode/{drama_id}/{chapter_id}"
|
||||
response = self.fetch(episode_url)
|
||||
if not response:
|
||||
return None
|
||||
|
||||
html = response.text
|
||||
|
||||
# 方法1: 从NEXT_DATA提取
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__".*?>(.*?)</script>', html, re.DOTALL)
|
||||
if next_data_match:
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
chapter_info = page_props.get("chapterInfo", {})
|
||||
|
||||
if chapter_info and "chapterVideoVo" in chapter_info:
|
||||
video_info = chapter_info["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2: 直接从HTML提取
|
||||
mp4_matches = re.findall(r'(https?://[^"\']+\.mp4)', html)
|
||||
if mp4_matches:
|
||||
for url in mp4_matches:
|
||||
if chapter_id in url or drama_id in url:
|
||||
return url
|
||||
return mp4_matches[0]
|
||||
|
||||
return None
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, "video/MP2T", {}, param]
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,199 @@
|
||||
#Kyele
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base = 'https://www.pandalive.co.kr'; self.api = 'https://api.pandalive.co.kr'; self.session = requests.Session()
|
||||
self.ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
|
||||
self.common_headers = {'User-Agent': self.ua, 'Accept': 'application/json, text/plain, */*', 'Origin': self.base, 'Referer': self.base + '/'}
|
||||
self.x_device_info = {"t": "webPc", "v": "1.0", "ui": "0", "ck": {"sessKeyAsp": ""}}; self.extra_cookie = ''
|
||||
def init(self, extend=""):
|
||||
try:
|
||||
if extend:
|
||||
cfg = extend if isinstance(extend, dict) else json.loads(extend)
|
||||
self.x_device_info = cfg.get('x_device_info', self.x_device_info); self.extra_cookie = cfg.get('cookie', '')
|
||||
except Exception: pass
|
||||
try:
|
||||
self.session.headers.update(self.common_headers)
|
||||
if self.extra_cookie: self.session.headers['Cookie'] = self.extra_cookie
|
||||
self.session.get(self.base, timeout=8); self._app_token()
|
||||
except Exception: pass
|
||||
return self
|
||||
def getName(self): return 'PandaLive'
|
||||
def isVideoFormat(self, url): return url.endswith('.m3u8') or url.endswith('.mp4')
|
||||
def manualVideoCheck(self): return False
|
||||
def destroy(self):
|
||||
try: self.session.close()
|
||||
except Exception: pass
|
||||
def _app_token(self):
|
||||
headers = self._with_x_device_info(dict(self.session.headers))
|
||||
return self.session.get(f'{self.api}/v1/member/app_token', headers=headers, timeout=8)
|
||||
def _list_live(self, page=None, page_size=None, order_by='user', only_new='N'):
|
||||
if page_size is None: page_size = 60
|
||||
limit = page_size; offset = 0 if page is None else max(0, (page - 1) * limit)
|
||||
headers = self._with_x_device_info(dict(self.session.headers))
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
data = {'orderBy': order_by, 'onlyNewBj': only_new, 'limit': str(limit), 'offset': str(offset)}
|
||||
try:
|
||||
r = self.session.post(f'{self.api}/v1/live', data=data, headers=headers, timeout=8)
|
||||
j = {}
|
||||
try: j = r.json()
|
||||
except Exception: pass
|
||||
if isinstance(j, dict) and isinstance(j.get('list'), list) and len(j['list']) > 0: return j
|
||||
except Exception: pass
|
||||
try: return self.session.get(f'{self.api}/v1/live', timeout=8).json()
|
||||
except Exception: return {}
|
||||
def _list_live_page(self, page, page_size, order_by='user', only_new='N'):
|
||||
j = self._list_live(page=page, page_size=page_size, order_by=order_by, only_new=only_new)
|
||||
return j.get('list', []) if isinstance(j, dict) else []
|
||||
def _list_live_aggregate(self, max_pages=5, page_size=60, min_expect=60, order_by='user', only_new='N'):
|
||||
try:
|
||||
cache_key = f'pandalive_agg_v2_{order_by}_{only_new}'; cached = self.getCache(cache_key)
|
||||
if isinstance(cached, dict) and isinstance(cached.get('list'), list): return cached['list']
|
||||
except Exception: pass
|
||||
seen = set(); result = []
|
||||
first = self._list_live(page=None, page_size=page_size, order_by=order_by, only_new=only_new)
|
||||
items = first.get('list', []) if isinstance(first, dict) else []
|
||||
for it in items:
|
||||
code = it.get('code') or it.get('userId')
|
||||
if code and code not in seen: seen.add(code); result.append(it)
|
||||
p = 1
|
||||
while len(result) < min_expect and p <= max_pages:
|
||||
page_items = self._list_live_page(page=p, page_size=page_size, order_by=order_by, only_new=only_new)
|
||||
added = 0
|
||||
for it in page_items:
|
||||
code = it.get('code') or it.get('userId')
|
||||
if code and code not in seen: seen.add(code); result.append(it); added += 1
|
||||
if added == 0 and p > 1: break
|
||||
p += 1
|
||||
try:
|
||||
payload = {"expiresAt": int(time.time()) + 20, "list": result}
|
||||
self.setCache(f'pandalive_agg_v2_{order_by}_{only_new}', payload)
|
||||
except Exception: pass
|
||||
return result
|
||||
def _live_play(self, play_id):
|
||||
body = {'play_id': play_id, 'device': 'webPc', 'player': 'ivs'}
|
||||
headers = self._with_x_device_info(dict(self.session.headers))
|
||||
headers['Content-Type'] = 'application/json'; headers['Referer'] = f'{self.base}/play/{play_id.split("_")[0]}'
|
||||
r = self.session.post(f'{self.api}/v1/live/play', headers=headers, data=json.dumps(body), timeout=8)
|
||||
if r.status_code != 200:
|
||||
try: self._app_token()
|
||||
except Exception: pass
|
||||
r = self.session.post(f'{self.api}/v1/live/play', headers=headers, data=json.dumps(body), timeout=8)
|
||||
try: return r.json()
|
||||
except Exception: return {'result': False, 'status': r.status_code, 'text': r.text}
|
||||
def _with_x_device_info(self, headers):
|
||||
try: headers['x-device-info'] = json.dumps(self.x_device_info, separators=(',', ':'))
|
||||
except Exception: headers['x-device-info'] = '{"t":"webPc","v":"1.0","ui":"0","ck":{"sessKeyAsp":""}}'
|
||||
return headers
|
||||
def homeContent(self, filter):
|
||||
classes = [{"type_name": "LIVE", "type_id": "live"}]
|
||||
if filter:
|
||||
filters = {"live": [{"key": "sort", "value": [
|
||||
{"n": "观看次数", "v": "user-N"}, {"n": "热门", "v": "hot-N"},
|
||||
{"n": "最新", "v": "new-N"}, {"n": "新人", "v": "user-Y"}
|
||||
]}]}
|
||||
else: filters = {}
|
||||
return {"class": classes, "filters": filters}
|
||||
def homeVideoContent(self):
|
||||
items = self._list_live_aggregate(max_pages=3, page_size=60, min_expect=48, order_by='user', only_new='N')
|
||||
if not items:
|
||||
items = (self._list_live().get('list', []))
|
||||
return {"list": [self._to_vod(it) for it in items[:48]]}
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page_size = 24; p = 1
|
||||
try: p = int(pg)
|
||||
except Exception: pass
|
||||
order_by, only_new = 'user', 'N'
|
||||
try:
|
||||
if isinstance(extend, str):
|
||||
try: extend = json.loads(extend) if extend.strip().startswith('{') else {}
|
||||
except Exception: extend = {}
|
||||
if isinstance(extend, dict):
|
||||
s = extend.get('sort')
|
||||
if isinstance(s, str) and '-' in s:
|
||||
ab = s.split('-', 1)
|
||||
if len(ab) == 2: order_by, only_new = (ab[0] or 'user'), (ab[1] or 'N')
|
||||
if (order_by, only_new) == ('user', 'N') and (tid or '').lower() != 'live':
|
||||
order_by, only_new = self._tid_to_sort(tid)
|
||||
except Exception: pass
|
||||
server_items = self._list_live_page(page=p, page_size=page_size, order_by=order_by, only_new=only_new)
|
||||
if len(server_items) >= page_size:
|
||||
return {"page": p, "pagecount": 99999, "limit": page_size, "total": 999999, "list": [self._to_vod(it) for it in server_items]}
|
||||
all_items = self._list_live_aggregate(max_pages=12, page_size=100, min_expect=120, order_by=order_by, only_new=only_new)
|
||||
total = len(all_items)
|
||||
if total <= 0:
|
||||
base_list = self._list_live().get('list', [])
|
||||
total = len(base_list)
|
||||
if total <= 0: return {"page": p, "pagecount": 1, "limit": page_size, "total": 0, "list": []}
|
||||
start = (p - 1) * page_size; end = start + page_size
|
||||
part = base_list[start:end]
|
||||
videos = [self._to_vod(it) for it in part]
|
||||
return {"page": p, "pagecount": (total + page_size - 1)//page_size, "limit": page_size, "total": total, "list": videos}
|
||||
start = ((p - 1) * page_size) % total; part = []
|
||||
for i in range(page_size): part.append(all_items[(start + i) % total])
|
||||
return {"page": p, "pagecount": 99999, "limit": page_size, "total": 999999, "list": [self._to_vod(it) for it in part]}
|
||||
def _tid_to_sort(self, tid):
|
||||
t = (tid or '').lower()
|
||||
if t == 'live_newbj': return 'user', 'Y'
|
||||
if t == 'live_hot': return 'hot', 'N'
|
||||
if t == 'live_new': return 'new', 'N'
|
||||
return 'user', 'N'
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]; parts = vid.split('|'); play_id = parts[0]
|
||||
user_id = parts[1] if len(parts) > 1 else play_id.split('_')[0]
|
||||
title = parts[2] if len(parts) > 2 else user_id
|
||||
vod = {"vod_id": vid, "vod_name": title, "vod_pic": "", "type_name": "LIVE", "vod_year": "", "vod_area": "", "vod_remarks": "PandaLive", "vod_actor": "", "vod_director": "", "vod_content": title, "vod_play_from": "IVS", "vod_play_url": f"直播${vid}"}
|
||||
return {"list": [vod]}
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
items = self._list_live().get('list', [])
|
||||
key_l = key.lower(); result = []
|
||||
for it in items:
|
||||
title = str(it.get('title', '')); user_id = str(it.get('userId', '')); user_nick = str(it.get('userNick', ''))
|
||||
if key_l in title.lower() or key_l in user_id.lower() or key_l in user_nick.lower():
|
||||
result.append(self._to_vod(it))
|
||||
return {"list": result, "page": 1}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
vid = id; parts = vid.split('|'); play_id = parts[0]
|
||||
j = self._live_play(play_id); m3u8 = self._find_first_m3u8(j) if isinstance(j, dict) else ''
|
||||
if not m3u8: return {"parse": 1, "playUrl": "", "url": f"{self.base}/play/{play_id.split('_')[0]}", "header": self._play_headers()}
|
||||
return {"parse": 0, "playUrl": "", "url": m3u8, "header": self._play_headers()}
|
||||
except Exception: return {"parse": 1, "playUrl": "", "url": f"{self.base}", "header": self._play_headers()}
|
||||
def liveContent(self, url):
|
||||
try:
|
||||
play_id = url; j = self._live_play(play_id)
|
||||
m3u8 = self._find_first_m3u8(j)
|
||||
if m3u8: return {"parse": 0, "url": m3u8, "header": self._play_headers()}
|
||||
except Exception: pass
|
||||
return {"parse": 1, "url": f"{self.base}/play/{url}", "header": self._play_headers()}
|
||||
def localProxy(self, param):
|
||||
action = param.get('action') if isinstance(param, dict) else None
|
||||
if action == 'play':
|
||||
play_id = param.get('play_id', ''); j = self._live_play(play_id)
|
||||
m3u8 = self._find_first_m3u8(j)
|
||||
if m3u8: return self._redirect(m3u8)
|
||||
return None
|
||||
def _redirect(self, url):
|
||||
return {"code": 302, "headers": {"Location": url}}
|
||||
def _find_first_m3u8(self, obj):
|
||||
try:
|
||||
text = json.dumps(obj, ensure_ascii=False)
|
||||
m = re.search(r'https?://[^\s"\\]+\.m3u8[^\s"\\]*', text)
|
||||
if m: return m.group(0)
|
||||
except Exception: pass
|
||||
return ''
|
||||
def _play_headers(self):
|
||||
return {'User-Agent': self.ua, 'Referer': self.base + '/', 'Origin': self.base}
|
||||
def _to_vod(self, it):
|
||||
title = it.get('title') or it.get('userNick') or it.get('userId') or 'LIVE'
|
||||
pic = it.get('thumbUrl') or it.get('ivsThumbnail') or ''
|
||||
user_id = it.get('userId', ''); play_id = it.get('code', user_id); vod_id = f"{play_id}|{user_id}|{title}"
|
||||
remarks = f"观众 {it.get('user', 0)} | 点赞 {it.get('likeCnt', 0)}"
|
||||
return {'vod_id': vod_id, 'vod_name': title, 'vod_pic': pic, 'vod_remarks': remarks}
|
||||
@@ -0,0 +1,72 @@
|
||||
#coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self,extend=""):
|
||||
self.base_url='http://api.hclyz.com:81/mf'
|
||||
|
||||
def homeContent(self,filter):
|
||||
classes = [{"type_name": "色播聚合","type_id":"/json.txt"}]
|
||||
result = {"class": classes}
|
||||
return result
|
||||
|
||||
def categoryContent(self,tid,pg,filter,extend):
|
||||
home = self.fetch(f'{self.base_url}/json.txt').json()
|
||||
data = home.get("pingtai")[1:]
|
||||
videos = [
|
||||
{
|
||||
"vod_id": "/" + item['address'],
|
||||
"vod_name": item['title'],
|
||||
"vod_pic": item['xinimg'].replace("http://cdn.gcufbd.top/img/",
|
||||
"https://slink.ltd/https://raw.githubusercontent.com/fish2018/lib/refs/heads/main/imgs/"),
|
||||
"vod_remarks": item['Number'],
|
||||
"style": {"type": "rect", "ratio": 1.33}
|
||||
} for item in sorted(data, key=lambda x: int(x['Number']), reverse=True)
|
||||
]
|
||||
result = {
|
||||
"page": pg,
|
||||
"pagecount": 1,
|
||||
"limit": len(videos),
|
||||
"total": len(videos),
|
||||
"list": videos
|
||||
}
|
||||
return result
|
||||
|
||||
def detailContent(self,array):
|
||||
id = array[0]
|
||||
data = self.fetch(f'{self.base_url}/{id}').json()
|
||||
zhubo = data['zhubo']
|
||||
playUrls = '#'.join([f"{vod['title']}${vod['address']}" for vod in zhubo])
|
||||
vod = [{
|
||||
"vod_play_from": 'sebo',
|
||||
"vod_play_url": playUrls,
|
||||
"vod_content": 'https://github.com/fish2018',
|
||||
}]
|
||||
result = {"list": vod}
|
||||
return result
|
||||
|
||||
def playerContent(self,flag,id,vipFlags):
|
||||
result = {
|
||||
'parse': 0,
|
||||
'url': id
|
||||
}
|
||||
return result
|
||||
|
||||
def getName(self):
|
||||
return '色播聚合'
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
def isVideoFormat(self,url):
|
||||
pass
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
def searchContent(self,key,quick):
|
||||
pass
|
||||
def destroy(self):
|
||||
pass
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,350 @@
|
||||
# coding=utf-8
|
||||
# 文件名: test.py
|
||||
# 描述: 萝莉AV爬虫 - 修正版
|
||||
|
||||
from base.spider import Spider
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "萝莉AV"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://212602.luoliav.cc"
|
||||
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': self.host,
|
||||
'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',
|
||||
}
|
||||
# 分类列表 - 根据页面导航栏
|
||||
self.classes = [
|
||||
{"type_name": "国产精选", "type_id": "1"},
|
||||
{"type_name": "日韩AV", "type_id": "2"},
|
||||
{"type_name": "蓝光超清", "type_id": "3"},
|
||||
{"type_name": "欧美精品", "type_id": "4"},
|
||||
{"type_name": "异族风情", "type_id": "5"},
|
||||
{"type_name": "动漫专区", "type_id": "6"},
|
||||
]
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""返回分类列表"""
|
||||
result = {}
|
||||
result["class"] = self.classes
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""首页推荐视频"""
|
||||
try:
|
||||
url = f"{self.host}/index.html"
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
html = rsp.text
|
||||
|
||||
videos = self._parse_video_list(html)
|
||||
print(f"首页解析到 {len(videos)} 个视频")
|
||||
|
||||
return {"list": videos}
|
||||
except Exception as e:
|
||||
print(f"首页视频解析错误: {e}")
|
||||
return {"list": []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类页面内容"""
|
||||
try:
|
||||
# 构建分类URL
|
||||
url = f"{self.host}/list.php?cid={tid}&page={pg}"
|
||||
print(f"分类页面URL: {url}")
|
||||
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
html = rsp.text
|
||||
|
||||
# 解析视频列表
|
||||
videos = self._parse_video_list(html)
|
||||
|
||||
# 解析分页信息
|
||||
pagecount = int(pg)
|
||||
# 查找分页链接
|
||||
pagination_pattern = r'<a href="list\.php\?cid=' + tid + r'&page=(\d+)">(\d+)</a>'
|
||||
page_matches = re.findall(pagination_pattern, html)
|
||||
if page_matches:
|
||||
pages = [int(p[1]) for p in page_matches]
|
||||
if pages:
|
||||
pagecount = max(pages)
|
||||
else:
|
||||
# 如果找不到分页,假设有3页
|
||||
pagecount = int(pg) + 2
|
||||
|
||||
print(f"分类解析完成: 第 {pg} 页,共 {pagecount} 页,找到 {len(videos)} 个视频")
|
||||
|
||||
return {
|
||||
"list": videos,
|
||||
"page": int(pg),
|
||||
"pagecount": pagecount,
|
||||
"limit": 30,
|
||||
"total": len(videos) * pagecount
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"分类页面解析错误: {e}")
|
||||
return {
|
||||
"list": [],
|
||||
"page": int(pg),
|
||||
"pagecount": 1,
|
||||
"limit": 30,
|
||||
"total": 0
|
||||
}
|
||||
|
||||
def _parse_video_list(self, html):
|
||||
"""解析视频列表的通用方法"""
|
||||
videos = []
|
||||
|
||||
# 找到所有视频项
|
||||
# 模式:<div class="group ... item"> ... </div>
|
||||
pattern = r'<div[^>]*class="[^"]*group[^"]*item[^"]*"[^>]*>(.*?)</div>\s*</div>\s*</div>'
|
||||
items = re.findall(pattern, html, re.DOTALL)
|
||||
|
||||
if not items:
|
||||
# 备选模式:更简单的匹配
|
||||
pattern = r'<div class="group[^"]*item[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<img[^>]*data-original="([^"]+)"[^>]*>.*?<a[^>]*class="[^"]*font-bold[^"]*"[^>]*>([^<]+)</a>'
|
||||
matches = re.findall(pattern, html, re.DOTALL)
|
||||
for match in matches:
|
||||
vod_id = match[0]
|
||||
if not vod_id.startswith('http'):
|
||||
if vod_id.startswith('/'):
|
||||
vod_id = self.host + vod_id
|
||||
else:
|
||||
vod_id = self.host + '/' + vod_id
|
||||
|
||||
# 去掉URL中的时间戳参数
|
||||
vod_id = re.sub(r'\?.*$', '', vod_id)
|
||||
|
||||
vod_pic = match[1]
|
||||
if vod_pic and not vod_pic.startswith('http'):
|
||||
if vod_pic.startswith('//'):
|
||||
vod_pic = 'https:' + vod_pic
|
||||
elif vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
|
||||
vod_name = match[2].strip()
|
||||
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": vod_name,
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": ""
|
||||
})
|
||||
return videos
|
||||
|
||||
# 逐个解析每个视频项
|
||||
for item in items:
|
||||
try:
|
||||
# 提取视频链接
|
||||
href_match = re.search(r'<a[^>]*href="([^"]+)"[^>]*>', item)
|
||||
if not href_match:
|
||||
continue
|
||||
vod_id = href_match.group(1)
|
||||
if not vod_id.startswith('http'):
|
||||
if vod_id.startswith('/'):
|
||||
vod_id = self.host + vod_id
|
||||
else:
|
||||
vod_id = self.host + '/' + vod_id
|
||||
|
||||
# 去掉URL中的时间戳参数
|
||||
vod_id = re.sub(r'\?.*$', '', vod_id)
|
||||
|
||||
# 提取封面图
|
||||
img_match = re.search(r'<img[^>]*data-original="([^"]+)"[^>]*>', item)
|
||||
if not img_match:
|
||||
img_match = re.search(r'<img[^>]*src="([^"]+)"[^>]*>', item)
|
||||
if not img_match:
|
||||
continue
|
||||
vod_pic = img_match.group(1)
|
||||
if vod_pic and not vod_pic.startswith('http'):
|
||||
if vod_pic.startswith('//'):
|
||||
vod_pic = 'https:' + vod_pic
|
||||
elif vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
|
||||
# 提取标题
|
||||
title_match = re.search(r'<a[^>]*class="[^"]*font-bold[^"]*"[^>]*>([^<]+)</a>', item)
|
||||
if not title_match:
|
||||
title_match = re.search(r'<a[^>]*>([^<]+)</a>\s*</div>\s*$', item)
|
||||
if not title_match:
|
||||
continue
|
||||
vod_name = title_match.group(1).strip()
|
||||
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": vod_name,
|
||||
"vod_pic": vod_pic,
|
||||
"vod_remarks": ""
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"解析单个视频时出错: {e}")
|
||||
continue
|
||||
|
||||
return videos
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""视频详情页"""
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
# 确保URL完整
|
||||
if not vod_id.startswith('http'):
|
||||
if vod_id.startswith('/'):
|
||||
url = self.host + vod_id
|
||||
else:
|
||||
url = self.host + '/' + vod_id
|
||||
else:
|
||||
url = vod_id
|
||||
|
||||
print(f"详情页URL: {url}")
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
html = rsp.text
|
||||
|
||||
# 提取标题
|
||||
vod_name = "未知标题"
|
||||
title_match = re.search(r'<title>([^<]+)</title>', html)
|
||||
if title_match:
|
||||
vod_name = title_match.group(1)
|
||||
# 清理标题
|
||||
vod_name = re.sub(r'\s*-\s*少女AV.*', '', vod_name)
|
||||
|
||||
# 提取封面图
|
||||
vod_pic = ""
|
||||
pic_match = re.search(r'<img[^>]+src="([^"]+)"[^>]*class="[^"]*thumb[^"]*"', html)
|
||||
if not pic_match:
|
||||
pic_match = re.search(r'<meta property="og:image" content="([^"]+)"', html)
|
||||
if not pic_match:
|
||||
pic_match = re.search(r'<img[^>]+data-original="([^"]+)"', html)
|
||||
|
||||
if pic_match:
|
||||
vod_pic = pic_match.group(1)
|
||||
if vod_pic.startswith('//'):
|
||||
vod_pic = 'https:' + vod_pic
|
||||
elif vod_pic.startswith('/'):
|
||||
vod_pic = self.host + vod_pic
|
||||
|
||||
# 提取播放地址 - 这个需要您提供详情页HTML才能准确解析
|
||||
play_url = ""
|
||||
|
||||
# 尝试找m3u8
|
||||
m3u8_match = re.search(r'(https?://[^"\']+\.m3u8[^"\']*)', html)
|
||||
if m3u8_match:
|
||||
play_url = m3u8_match.group(1)
|
||||
else:
|
||||
# 找mp4
|
||||
mp4_match = re.search(r'(https?://[^"\']+\.mp4[^"\']*)', html)
|
||||
if mp4_match:
|
||||
play_url = mp4_match.group(1)
|
||||
|
||||
# 如果没找到,可能需要从JavaScript变量中提取
|
||||
if not play_url:
|
||||
# 查找类似 var video_url = '...' 的代码
|
||||
js_match = re.search(r'var[^;]*video[^=]*=[^"\']*["\']([^"\']+\.m3u8[^"\']*)["\']', html, re.IGNORECASE)
|
||||
if js_match:
|
||||
play_url = js_match.group(1)
|
||||
|
||||
if not play_url:
|
||||
play_url = url # 如果没找到,让TVBox去解析
|
||||
|
||||
video = {
|
||||
"vod_id": ids[0],
|
||||
"vod_name": vod_name,
|
||||
"vod_pic": vod_pic,
|
||||
"vod_content": "",
|
||||
"vod_year": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_area": "",
|
||||
"vod_play_from": "萝莉AV",
|
||||
"vod_play_url": f"播放${play_url}"
|
||||
}
|
||||
|
||||
return {"list": [video]}
|
||||
|
||||
except Exception as e:
|
||||
print(f"详情页解析错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
video = {
|
||||
"vod_id": ids[0],
|
||||
"vod_name": "加载失败",
|
||||
"vod_pic": "",
|
||||
"vod_content": "",
|
||||
"vod_play_from": "默认",
|
||||
"vod_play_url": f"播放${ids[0]}"
|
||||
}
|
||||
return {"list": [video]}
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
"""搜索功能"""
|
||||
try:
|
||||
# 构建搜索URL
|
||||
encoded_key = urllib.parse.quote(key)
|
||||
url = f"{self.host}/search.php?keyword={encoded_key}&page={pg}"
|
||||
print(f"搜索URL: {url}")
|
||||
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
html = rsp.text
|
||||
|
||||
# 搜索结果页可能和分类页结构相同
|
||||
videos = self._parse_video_list(html)
|
||||
|
||||
print(f"搜索到 {len(videos)} 个结果")
|
||||
|
||||
return {
|
||||
"list": videos,
|
||||
"page": int(pg),
|
||||
"pagecount": 1,
|
||||
"limit": 30,
|
||||
"total": len(videos)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索解析错误: {e}")
|
||||
return {"list": []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""解析播放地址"""
|
||||
result = {}
|
||||
|
||||
try:
|
||||
# 如果已经是视频链接,直接返回
|
||||
if id.endswith(('.m3u8', '.mp4', '.flv', '.ts')):
|
||||
result["parse"] = 0
|
||||
result["url"] = id
|
||||
result["header"] = self.headers
|
||||
else:
|
||||
# 否则需要二次解析
|
||||
if not id.startswith('http'):
|
||||
if id.startswith('/'):
|
||||
play_url = self.host + id
|
||||
else:
|
||||
play_url = self.host + '/' + id
|
||||
else:
|
||||
play_url = id
|
||||
|
||||
# 去掉时间戳参数
|
||||
play_url = re.sub(r'\?.*$', '', play_url)
|
||||
|
||||
result["parse"] = 1
|
||||
result["url"] = play_url
|
||||
result["header"] = self.headers
|
||||
|
||||
except Exception as e:
|
||||
print(f"播放地址解析错误: {e}")
|
||||
result["parse"] = 1
|
||||
result["url"] = id
|
||||
result["header"] = self.headers
|
||||
|
||||
return result
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
"""判断是否为视频格式"""
|
||||
return url.endswith(('.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts'))
|
||||
|
||||
def localProxy(self, params):
|
||||
"""本地代理"""
|
||||
return [200, "video/MP2T", ""]
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
from urllib.parse import urljoin, quote
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "香肠派对"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://xiang512.xiang.party/xcpd"
|
||||
pass
|
||||
|
||||
def header(self):
|
||||
return {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Referer': self.host
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {"class": [], "filters": {}, "list": []}
|
||||
|
||||
# 分类列表
|
||||
classes = [
|
||||
{"type_id": "1", "type_name": "在线看片"},
|
||||
{"type_id": "2", "type_name": "无需等待"},
|
||||
{"type_id": "3", "type_name": "不用下载"},
|
||||
{"type_id": "4", "type_name": "全部免费"}
|
||||
]
|
||||
result["class"] = classes
|
||||
|
||||
# 获取首页视频
|
||||
url = f"{self.host}/"
|
||||
rsp = self.fetch(url, headers=self.header())
|
||||
if rsp.status_code != 200:
|
||||
return result
|
||||
|
||||
root = BeautifulSoup(rsp.text, 'html.parser')
|
||||
videos = []
|
||||
|
||||
# 查找视频列表
|
||||
items = root.select('ul.thumbnail-group.clearfix li')
|
||||
for item in items:
|
||||
try:
|
||||
a = item.select_one('a.thumbnail')
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
|
||||
if not vod_id:
|
||||
continue
|
||||
vod_id = vod_id.group(1)
|
||||
|
||||
img = a.select_one('img')
|
||||
pic = img.get('src', '') if img else ''
|
||||
|
||||
info = item.select_one('.video-info')
|
||||
if info:
|
||||
h5 = info.select_one('h5 a')
|
||||
name = h5.get('title', '') if h5 else ''
|
||||
if not name:
|
||||
name = h5.text.strip() if h5 else ''
|
||||
p = info.select_one('p')
|
||||
remarks = p.text.strip() if p else ''
|
||||
else:
|
||||
name = a.get('title', '')
|
||||
remarks = ''
|
||||
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
if len(videos) >= 20:
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
result["list"] = videos
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
return self.homeContent(False)
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
p = int(pg)
|
||||
# 修复分页URL格式
|
||||
url = f"{self.host}/vodtype/{tid}-{p}.html"
|
||||
rsp = self.fetch(url, headers=self.header())
|
||||
if rsp.status_code != 200:
|
||||
return {"list": [], "page": p, "pagecount": 1}
|
||||
|
||||
root = BeautifulSoup(rsp.text, 'html.parser')
|
||||
videos = []
|
||||
|
||||
# 提取总页数(从“共XX条数据,当前X/913页”)
|
||||
pagecount = p
|
||||
page_info = root.find(string=re.compile(r'共\d+条数据,当前\d+/(\d+)页'))
|
||||
if page_info:
|
||||
try:
|
||||
pagecount = int(re.search(r'/(\d+)页', page_info).group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 提取视频列表
|
||||
items = root.select('ul.thumbnail-group.clearfix li')
|
||||
for item in items:
|
||||
try:
|
||||
a = item.select_one('a.thumbnail')
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
|
||||
if not vod_id:
|
||||
continue
|
||||
vod_id = vod_id.group(1)
|
||||
|
||||
img = a.select_one('img')
|
||||
pic = img.get('src', '') if img else ''
|
||||
|
||||
info = item.select_one('.video-info')
|
||||
if info:
|
||||
h5 = info.select_one('h5 a')
|
||||
name = h5.get('title', '') if h5 else ''
|
||||
if not name:
|
||||
name = h5.text.strip() if h5 else ''
|
||||
p_elem = info.select_one('p')
|
||||
remarks = p_elem.text.strip() if p_elem else ''
|
||||
else:
|
||||
name = a.get('title', '')
|
||||
remarks = ''
|
||||
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
except:
|
||||
continue
|
||||
|
||||
return {
|
||||
"list": videos,
|
||||
"page": p,
|
||||
"pagecount": pagecount
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0] if isinstance(ids, list) else ids.split(",")[0]
|
||||
url = f"{self.host}/voddetail/{vid}.html"
|
||||
rsp = self.fetch(url, headers=self.header())
|
||||
if rsp.status_code != 200:
|
||||
return {"list": []}
|
||||
|
||||
root = BeautifulSoup(rsp.text, 'html.parser')
|
||||
|
||||
# 标题
|
||||
title = ""
|
||||
h1 = root.select_one('h1.appel-title')
|
||||
if h1:
|
||||
title = h1.text.strip()
|
||||
if not title:
|
||||
title_elem = root.select_one('title')
|
||||
if title_elem:
|
||||
title = title_elem.text.replace('视频介绍--香肠派对', '').strip()
|
||||
|
||||
# 图片
|
||||
pic = ""
|
||||
img = root.select_one('img.appel-img')
|
||||
if img:
|
||||
pic = img.get('src', '')
|
||||
if not pic:
|
||||
img = root.select_one('.detail-poster img')
|
||||
if img:
|
||||
pic = img.get('src', '')
|
||||
|
||||
# 描述
|
||||
desc = ""
|
||||
desc_elem = root.select_one('.detail-content')
|
||||
if desc_elem:
|
||||
desc = desc_elem.text.strip()
|
||||
if not desc:
|
||||
desc_elem = root.select_one('.appel-content')
|
||||
if desc_elem:
|
||||
desc = desc_elem.text.strip()
|
||||
|
||||
# 播放列表
|
||||
play_from_list = []
|
||||
play_url_list = []
|
||||
|
||||
# 查找线路
|
||||
tabs = root.select('.detail-tab li a')
|
||||
play_blocks = root.select('ul.detail-play-list')
|
||||
|
||||
for i, block in enumerate(play_blocks):
|
||||
line_name = tabs[i].text.strip() if i < len(tabs) else f"线路{i+1}"
|
||||
urls = []
|
||||
for a in block.select('a'):
|
||||
href = a.get('href', '')
|
||||
if href:
|
||||
full_url = urljoin(self.host, href)
|
||||
name = a.text.strip() or f"第{len(urls)+1}集"
|
||||
urls.append(f"{name}${full_url}")
|
||||
if urls:
|
||||
play_from_list.append(line_name)
|
||||
play_url_list.append("#".join(urls))
|
||||
|
||||
# 如果没有找到,尝试其他选择器
|
||||
if not play_from_list:
|
||||
lines = root.select('.ff-playurl-tab li a')
|
||||
for i, block in enumerate(root.select('.ff-playurl-tab-pane')):
|
||||
line_name = lines[i].text.strip() if i < len(lines) else f"线路{i+1}"
|
||||
urls = []
|
||||
for a in block.select('a'):
|
||||
href = a.get('href', '')
|
||||
if href:
|
||||
full_url = urljoin(self.host, href)
|
||||
name = a.text.strip() or f"第{len(urls)+1}集"
|
||||
urls.append(f"{name}${full_url}")
|
||||
if urls:
|
||||
play_from_list.append(line_name)
|
||||
play_url_list.append("#".join(urls))
|
||||
|
||||
vod_play_from = "$$$".join(play_from_list) if play_from_list else ""
|
||||
vod_play_url = "$$$".join(play_url_list) if play_url_list else ""
|
||||
|
||||
return {"list": [{
|
||||
"vod_id": vid,
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"vod_content": desc,
|
||||
"vod_play_from": vod_play_from,
|
||||
"vod_play_url": vod_play_url
|
||||
}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
p = int(pg)
|
||||
url = f"{self.host}/vodsearch/-------------.html?wd={quote(key)}&page={p}"
|
||||
rsp = self.fetch(url, headers=self.header())
|
||||
if rsp.status_code != 200:
|
||||
return {"list": []}
|
||||
|
||||
root = BeautifulSoup(rsp.text, 'html.parser')
|
||||
videos = []
|
||||
|
||||
items = root.select('ul.thumbnail-group.clearfix li')
|
||||
for item in items:
|
||||
try:
|
||||
a = item.select_one('a.thumbnail')
|
||||
if not a:
|
||||
continue
|
||||
href = a.get('href', '')
|
||||
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
|
||||
if not vod_id:
|
||||
continue
|
||||
vod_id = vod_id.group(1)
|
||||
|
||||
img = a.select_one('img')
|
||||
pic = img.get('src', '') if img else ''
|
||||
|
||||
info = item.select_one('.video-info')
|
||||
if info:
|
||||
h5 = info.select_one('h5 a')
|
||||
name = h5.get('title', '') if h5 else ''
|
||||
if not name:
|
||||
name = h5.text.strip() if h5 else ''
|
||||
p_elem = info.select_one('p')
|
||||
remarks = p_elem.text.strip() if p_elem else ''
|
||||
else:
|
||||
name = a.get('title', '')
|
||||
remarks = ''
|
||||
|
||||
videos.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
except:
|
||||
continue
|
||||
|
||||
return {"list": videos, "page": p}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
# 构建播放页URL
|
||||
if id.startswith('http'):
|
||||
play_url = id
|
||||
else:
|
||||
play_url = urljoin(self.host, id)
|
||||
|
||||
rsp = self.fetch(play_url, headers=self.header())
|
||||
if rsp.status_code != 200:
|
||||
return {"parse": 0, "playUrl": play_url}
|
||||
|
||||
html = rsp.text
|
||||
|
||||
# 方法1:从 player_aaaa 提取
|
||||
match = re.search(r'var player_aaaa\s*=\s*({.*?});', html, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
js_str = match.group(1)
|
||||
js_str = re.sub(r'(\w+):', r'"\1":', js_str)
|
||||
player_data = json.loads(js_str)
|
||||
if player_data.get('url'):
|
||||
return {"parse": 0, "playUrl": player_data['url']}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2:从 iframe 提取(关键修复)
|
||||
# 匹配 id="playleft" 的 td 中的 iframe
|
||||
iframe_match = re.search(r'<td[^>]*id="playleft"[^>]*>.*?<iframe[^>]+src="([^"]+)"', html, re.DOTALL)
|
||||
if iframe_match:
|
||||
iframe_url = iframe_match.group(1)
|
||||
# 提取 url 参数
|
||||
m3u8_match = re.search(r'[?&]url=([^&]+)', iframe_url)
|
||||
if m3u8_match:
|
||||
m3u8_url = urllib.parse.unquote(m3u8_match.group(1))
|
||||
return {"parse": 0, "playUrl": m3u8_url}
|
||||
# 如果 iframe 本身就是 m3u8
|
||||
if '.m3u8' in iframe_url:
|
||||
return {"parse": 0, "playUrl": iframe_url}
|
||||
|
||||
# 方法3:直接查找 iframe
|
||||
iframe_match2 = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
if iframe_match2:
|
||||
iframe_url = iframe_match2.group(1)
|
||||
m3u8_match = re.search(r'[?&]url=([^&]+)', iframe_url)
|
||||
if m3u8_match:
|
||||
m3u8_url = urllib.parse.unquote(m3u8_match.group(1))
|
||||
return {"parse": 0, "playUrl": m3u8_url}
|
||||
|
||||
# 方法4:直接查找 m3u8
|
||||
m3u8 = re.search(r'https?://[^"\']+\.m3u8[^"\']*', html)
|
||||
if m3u8:
|
||||
return {"parse": 0, "playUrl": m3u8.group(0)}
|
||||
|
||||
# 方法5:让系统解析
|
||||
return {"parse": 1, "url": play_url}
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", ""]
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 0,
|
||||
quickSearch: 0,
|
||||
title: '麻豆免费在线播放',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
import json, re, sys
|
||||
from urllib import parse as urlparse
|
||||
import requests as req
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
class Spider(BaseSpider):
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend='{}'):
|
||||
self.debug = False
|
||||
self.name = '麻豆免费在线播放'
|
||||
self.home_url = 'https://c-you.hair'
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36'
|
||||
}
|
||||
self.extend = extend
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def liveContent(self, url):
|
||||
return {'list': [], 'code': 0, 'error': 0}
|
||||
|
||||
# ---------- 首页分类 ----------
|
||||
def homeContent(self, filterable=False):
|
||||
result = {
|
||||
'class': [
|
||||
{'type_name': '国产精品', 'type_id': '6'},
|
||||
{'type_name': '中文字幕', 'type_id': '7'},
|
||||
{'type_name': '伦理影片', 'type_id': '8'},
|
||||
{'type_name': '自拍偷拍', 'type_id': '9'},
|
||||
{'type_name': '口交视频', 'type_id': '10'},
|
||||
{'type_name': '日韩无码', 'type_id': '11'},
|
||||
{'type_name': '制服诱惑', 'type_id': '12'},
|
||||
{'type_name': '国产色情', 'type_id': '13'},
|
||||
],
|
||||
'filters': {},
|
||||
'list': [],
|
||||
'code': 0,
|
||||
'error': 0
|
||||
}
|
||||
return result
|
||||
|
||||
# ---------- 首页推荐 ----------
|
||||
def homeVideoContent(self):
|
||||
result = {'list': [], 'code': 0, 'error': 0}
|
||||
try:
|
||||
r = req.get(self.home_url + '/', headers=self.headers, timeout=10)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
items = doc('.col-md-3.resent-grid.recommended-grid')
|
||||
for item in items.items():
|
||||
a = item('.resent-grid-img a')
|
||||
href = a.attr('href') or ''
|
||||
title = item('h5 a.title').text().strip()
|
||||
img = item('.resent-grid-img img').attr('data-original') or ''
|
||||
views = item('.views-info span').text().strip()
|
||||
if href and title:
|
||||
result['list'].append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': views + '观看' if views else '',
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return result
|
||||
|
||||
# ---------- 一级分类列表 ----------
|
||||
def categoryContent(self, tid, pg, filterable, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': pg, 'limit': 90, 'total': 999999, 'code': 0, 'error': 0}
|
||||
try:
|
||||
if int(pg) <= 1:
|
||||
url = f'{self.home_url}/vodtype/{tid}.html'
|
||||
else:
|
||||
url = f'{self.home_url}/vodtype/{tid}-{pg}.html'
|
||||
r = req.get(url, headers=self.headers, timeout=10)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
# 获取总页数
|
||||
page_info = doc('.pager .active a').text().strip()
|
||||
if '/' in page_info:
|
||||
total_pages = page_info.split('/')[-1].strip()
|
||||
result['pagecount'] = int(total_pages) if total_pages.isdigit() else pg
|
||||
else:
|
||||
result['pagecount'] = 9999
|
||||
items = doc('.col-md-3.resent-grid.recommended-grid')
|
||||
for item in items.items():
|
||||
a = item('.resent-grid-img a')
|
||||
href = a.attr('href') or ''
|
||||
title = item('h5 a.title').text().strip()
|
||||
img = item('.resent-grid-img img').attr('data-original') or ''
|
||||
views = item('.views-info span').text().strip()
|
||||
if href and title:
|
||||
result['list'].append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': views + '观看' if views else '',
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
return result
|
||||
|
||||
# ---------- 二级详情 ----------
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'code': 0, 'error': 0}
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
url = self.home_url + vod_id
|
||||
r = req.get(url, headers=self.headers, timeout=10)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
title = doc('.song-info h3').text().strip()
|
||||
img = doc('.video-grid img').attr('src') or ''
|
||||
# 影片介绍块
|
||||
info_text = doc('#myList li p').text().strip()
|
||||
# 提取发布时间和时长
|
||||
pub_time = ''
|
||||
duration = ''
|
||||
pub_match = re.search(r'发布时间:([\d\-]+)', info_text)
|
||||
if pub_match:
|
||||
pub_time = pub_match.group(1)
|
||||
dur_match = re.search(r'视频时长:(\S+)', info_text)
|
||||
if dur_match:
|
||||
duration = dur_match.group(1)
|
||||
# 分类标签
|
||||
category = ''
|
||||
cat_match = re.search(r'影片分类:(\S+)', info_text)
|
||||
if cat_match:
|
||||
category = cat_match.group(1).strip()
|
||||
# 简介内容
|
||||
content = info_text.split('')[-1].strip() if '' in info_text else ''
|
||||
if not content:
|
||||
# 尝试取最后一行非标签内容
|
||||
lines = [l.strip() for l in info_text.split('\n') if l.strip()]
|
||||
content = lines[-1] if lines else ''
|
||||
# 播放链接
|
||||
play_url = doc('a[href*="/vodplay/"]').attr('href') or ''
|
||||
play_from = '在线播放'
|
||||
play_url_formatted = ''
|
||||
if play_url:
|
||||
play_from = '在线播放'
|
||||
play_url_formatted = f'播放${play_url}'
|
||||
result['list'].append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title if title else '',
|
||||
'vod_pic': img,
|
||||
'type_name': category,
|
||||
'vod_year': pub_time[:4] if len(pub_time) >= 4 else '',
|
||||
'vod_area': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_remarks': duration,
|
||||
'vod_content': content,
|
||||
'vod_play_from': play_from,
|
||||
'vod_play_url': play_url_formatted,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
# ---------- 搜索 ----------
|
||||
def searchContent(self, key, quick=False, pg=1):
|
||||
result = {'list': [], 'code': 0, 'error': 0}
|
||||
try:
|
||||
if int(pg) <= 1:
|
||||
url = f'{self.home_url}/vodsearch/{urlparse.quote(key)}-------------.html'
|
||||
else:
|
||||
url = f'{self.home_url}/vodsearch/{urlparse.quote(key)}-------------{pg}---.html'
|
||||
r = req.get(url, headers=self.headers, timeout=10)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
items = doc('.col-md-3.resent-grid.recommended-grid')
|
||||
for item in items.items():
|
||||
a = item('.resent-grid-img a')
|
||||
href = a.attr('href') or ''
|
||||
title = item('h5 a.title').text().strip()
|
||||
img = item('.resent-grid-img img').attr('data-original') or ''
|
||||
views = item('.views-info span').text().strip()
|
||||
if href and title:
|
||||
result['list'].append({
|
||||
'vod_id': href,
|
||||
'vod_name': title,
|
||||
'vod_pic': img,
|
||||
'vod_remarks': views + '观看' if views else '',
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
# ---------- 播放解析 ----------
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
result = {
|
||||
'parse': 1,
|
||||
'playUrl': '',
|
||||
'url': '',
|
||||
'header': {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'
|
||||
}
|
||||
}
|
||||
try:
|
||||
play_url = self.home_url + pid
|
||||
r = req.get(play_url, headers=self.headers, timeout=10)
|
||||
r.encoding = 'utf-8'
|
||||
html = r.text
|
||||
# 尝试提取 player_data 或其他已知变量
|
||||
player_match = re.search(r'var\s+player_data\s*=\s*({.*?});', html, re.DOTALL)
|
||||
if player_match:
|
||||
player_json = json.loads(player_match.group(1))
|
||||
real_url = player_json.get('url', '')
|
||||
if real_url:
|
||||
result['url'] = real_url
|
||||
result['parse'] = 0
|
||||
return result
|
||||
# 尝试匹配常见 iframe
|
||||
iframe_match = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
if iframe_match:
|
||||
result['url'] = iframe_match.group(1)
|
||||
result['parse'] = 1
|
||||
return result
|
||||
# 尝试匹配 video 标签
|
||||
video_match = re.search(r'<video[^>]+src="([^"]+)"', html)
|
||||
if video_match:
|
||||
result['url'] = video_match.group(1)
|
||||
result['parse'] = 0
|
||||
return result
|
||||
# 尝试匹配 source 标签
|
||||
source_match = re.search(r'<source[^>]+src="([^"]+)"', html)
|
||||
if source_match:
|
||||
result['url'] = source_match.group(1)
|
||||
result['parse'] = 0
|
||||
return result
|
||||
# 兜底:嗅探模式,把播放页URL交给壳子嗅探
|
||||
result['url'] = play_url
|
||||
result['parse'] = 1
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [404, 'text/plain', 'Not Found']
|
||||
|
||||
def destroy(self):
|
||||
return '正在Destroy'
|
||||
Reference in New Issue
Block a user