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 "🌈 818黑料网|终极完美版"
|
||||
|
||||
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://cell.lacdfsq.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 '818黑料网'
|
||||
|
||||
return {'list': [{'vod_play_from': '818黑料网', 'vod_play_url': play_url, 'vod_content': vod_content}]}
|
||||
except:
|
||||
return {'list': [{'vod_play_from': '818黑料网', '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,192 @@
|
||||
#coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
import json
|
||||
import re
|
||||
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "91Porn"
|
||||
|
||||
def init(self, extend):
|
||||
self.baseUrl = "https://91porn.com"
|
||||
self.header = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Referer": self.baseUrl + "/index.php"
|
||||
}
|
||||
self.cookies = {}
|
||||
|
||||
# ✨ 核心改造:动态从 config.json 的 ext 参数中读取配置
|
||||
self.username = ""
|
||||
self.password = ""
|
||||
self.email = ""
|
||||
|
||||
try:
|
||||
if extend:
|
||||
extendDict = json.loads(extend) if isinstance(extend, str) else extend
|
||||
self.username = str(extendDict.get('username', '')).strip()
|
||||
self.password = str(extendDict.get('password', '')).strip()
|
||||
self.email = str(extendDict.get('email', '')).strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.is_ready = False
|
||||
|
||||
def safe_bypass_and_verify(self):
|
||||
if self.is_ready:
|
||||
return True
|
||||
try:
|
||||
rsp = self.fetch(self.baseUrl + "/email_verify.php", headers=self.header, timeout=5)
|
||||
self.cookies.update(rsp.cookies.get_dict())
|
||||
self.cookies['language'] = 'cn_CN'
|
||||
self.cookies['CNAM'] = '1'
|
||||
|
||||
# 如果外部没有配置邮箱,则直接作为普通游客会话放行
|
||||
if not self.email:
|
||||
self.is_ready = True
|
||||
return True
|
||||
|
||||
post_data = {"email": self.email, "recover": "Submit", "submit": "true"}
|
||||
v_hd = self.header.copy()
|
||||
v_hd["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
v_hd["Referer"] = self.baseUrl + "/email_verify.php"
|
||||
|
||||
rsp_v = self.post(self.baseUrl + "/email_verify.php", data=post_data, headers=v_hd, cookies=self.cookies, timeout=5)
|
||||
self.cookies.update(rsp_v.cookies.get_dict())
|
||||
self.is_ready = True
|
||||
return True
|
||||
except Exception:
|
||||
self.is_ready = True
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classList = [
|
||||
{"type_name": "今日排行", "type_id": "hot"},
|
||||
{"type_name": "最近更新", "type_id": "rp"},
|
||||
{"type_name": "本月最热", "type_id": "md"}
|
||||
]
|
||||
result['class'] = classList
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
self.safe_bypass_and_verify()
|
||||
result = {'page': int(page), 'pagecount': 1, 'limit': 0, 'total': 0, 'list': []}
|
||||
page = int(page)
|
||||
|
||||
url = self.baseUrl + "/v.php?category=" + cid + "&page=" + str(page)
|
||||
if cid == "hot":
|
||||
url = self.baseUrl + "/index.php"
|
||||
elif cid == "rp":
|
||||
url = self.baseUrl + "/v.php?next=watch&page=" + str(page)
|
||||
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.header, cookies=self.cookies, timeout=5)
|
||||
html = self.html(rsp.text)
|
||||
items = html.xpath("//div[contains(@class, 'well')]")
|
||||
videos = []
|
||||
for item in items:
|
||||
try:
|
||||
a_tag = item.xpath(".//a[contains(@href, 'view_video.php')]")
|
||||
if not a_tag:
|
||||
continue
|
||||
href = a_tag[0].get('href', '')
|
||||
v_match = re.search(r'viewkey=([a-zA-Z0-9]+)', href)
|
||||
if not v_match:
|
||||
continue
|
||||
vod_id = v_match.group(1)
|
||||
|
||||
name_nodes = item.xpath(".//span[contains(@class, 'video-title')]/text()")
|
||||
vod_name = name_nodes[0].strip() if name_nodes else "精彩视频"
|
||||
|
||||
img_nodes = item.xpath(".//img[contains(@class, 'img-responsive')]/@src")
|
||||
vod_pic = img_nodes[0].strip() if img_nodes else ""
|
||||
if vod_pic.startswith('//'):
|
||||
vod_pic = "https:" + vod_pic
|
||||
|
||||
remark_nodes = item.xpath(".//span[@class='duration']/text()")
|
||||
vod_remarks = remark_nodes[0].strip() if remark_nodes else "完整版"
|
||||
|
||||
videos.append({"vod_id": vod_id, "vod_name": self.cleanText(self.removeHtmlTags(vod_name)), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
|
||||
except:
|
||||
continue
|
||||
result['list'] = videos
|
||||
result['limit'] = len(videos)
|
||||
result['pagecount'] = page + 1 if len(videos) >= 10 else page
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
def detailContent(self, did):
|
||||
self.safe_bypass_and_verify()
|
||||
tid = did[0]
|
||||
url = self.baseUrl + "/view_video.php?viewkey=" + tid
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.header, cookies=self.cookies, timeout=5)
|
||||
html_text = rsp.text.replace('&', '&')
|
||||
root = self.html(html_text)
|
||||
|
||||
title_nodes = root.xpath("//h4[contains(@class, 'login_register_header')]/text() | //title/text()")
|
||||
title = title_nodes[0].strip().replace(" - 91porn", "").strip() if title_nodes else "精彩视频"
|
||||
|
||||
cover_nodes = root.xpath("//video/@poster")
|
||||
cover_pic = cover_nodes[0] if cover_nodes else ""
|
||||
|
||||
real_video_url = ""
|
||||
strencode_match = re.search(r'strencode2\([\"\']([^\"\'\)]+)[\"\']\)', html_text)
|
||||
if strencode_match:
|
||||
ciphertext = strencode_match.group(1)
|
||||
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
decrypted_html = unquote(ciphertext)
|
||||
except:
|
||||
import urllib
|
||||
decrypted_html = urllib.unquote(ciphertext)
|
||||
|
||||
src_match = re.search(r"src=['\"]([^'\"]+)['\"]", decrypted_html)
|
||||
if src_match:
|
||||
real_video_url = src_match.group(1)
|
||||
|
||||
if not real_video_url:
|
||||
src_nodes = root.xpath("//video/source/@src | //video/@src")
|
||||
if src_nodes:
|
||||
real_video_url = src_nodes[0]
|
||||
|
||||
if not real_video_url:
|
||||
real_video_url = url
|
||||
|
||||
vod = {"vod_id": tid, "vod_name": self.cleanText(self.removeHtmlTags(title)), "vod_pic": cover_pic, "type_name": "在线视频", "vod_content": "资源解析就绪", "vod_play_from": "91Porn秒解流", "vod_play_url": "直连资源源$" + real_video_url}
|
||||
return {'list': [vod]}
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
need_parse = 1 if "view_video.php" in pid else 0
|
||||
return {"url": pid, "header": self.header, "parse": need_parse}
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
return {'list': []}
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
return {'list': []}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, params):
|
||||
return None
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,258 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
import sys
|
||||
import base64
|
||||
import hashlib
|
||||
import requests
|
||||
from typing import Tuple
|
||||
from base.spider import Spider
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote, unquote
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
sys.path.append('..')
|
||||
|
||||
# 搜索用户名,关键词格式为“类别+空格+关键词”
|
||||
# 类别在标签上已注明,比如“女主播g”,则搜索类别为“g”
|
||||
# 搜索“g per”,则在“女主播”中搜索“per”, 关键词不区分大小写,但至少3位,否则空结果
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend="{}"):
|
||||
origin = 'https://zh.stripchat.com'
|
||||
self.host = origin
|
||||
self.headers = {
|
||||
'Origin': origin,
|
||||
'Referer': f"{origin}/",
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0'
|
||||
}
|
||||
self.stripchat_key = self.decode_key_compact()
|
||||
# 缓存字典
|
||||
self._hash_cache = {}
|
||||
self.create_session_with_retry()
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = [{'type_name': '女主播g', 'type_id': 'girls'}, {'type_name': '情侣c', 'type_id': 'couples'}, {'type_name': '男主播m', 'type_id': 'men'}, {'type_name': '跨性别t', 'type_id': 'trans'}]
|
||||
filters = {}
|
||||
value = [{'n': '中国', 'v': 'tagLanguageChinese'}, {'n': '亚洲', 'v': 'ethnicityAsian'}, {'n': '白人', 'v': 'ethnicityWhite'}, {'n': '拉丁', 'v': 'ethnicityLatino'}, {'n': '混血', 'v': 'ethnicityMultiracial'}, {'n': '印度', 'v': 'ethnicityIndian'}, {'n': '阿拉伯', 'v': 'ethnicityMiddleEastern'}, {'n': '黑人', 'v': 'ethnicityEbony'}]
|
||||
value_gay = [{'n': '情侣', 'v': 'sexGayCouples'}, {'n': '直男', 'v': 'orientationStraight'}]
|
||||
for tid in ['girls', 'couples', 'men', 'trans']:
|
||||
c_value = value[:]
|
||||
if tid == 'men':
|
||||
c_value += value_gay
|
||||
filters[tid] = [{'key': 'tag', 'value': c_value}]
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
limit = 60
|
||||
offset = limit * (int(pg) - 1)
|
||||
domain = f"{self.host}/api/front/models?improveTs=false&removeShows=false&limit={limit}&offset={offset}&primaryTag={tid}&sortBy=stripRanking&rcmGrp=A&rbCnGr=true&prxCnGr=false&nic=false"
|
||||
if 'tag' in extend:
|
||||
domain += "&filterGroupTags=%5B%5B%22" + extend['tag'] + "%22%5D%5D"
|
||||
rsp = requests.get(domain, headers=self.headers).json()
|
||||
vodList = rsp['models']
|
||||
videos = []
|
||||
for vod in vodList:
|
||||
id = str(vod['id'])
|
||||
name = str(vod['username']).strip()
|
||||
stamp = vod['snapshotTimestamp']
|
||||
country = str(vod['country']).strip()
|
||||
flag = self.country_code_to_flag(country)
|
||||
remark = "🎫" if vod['status'] == "groupShow" else ""
|
||||
videos.append({
|
||||
"vod_id": name,
|
||||
"vod_name": f"{flag}{name}",
|
||||
"vod_pic": f"https://img.doppiocdn.net/thumbs/{stamp}/{id}",
|
||||
"vod_remarks": remark
|
||||
})
|
||||
total = int(rsp['filteredCount'])
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = (total + limit - 1) // limit
|
||||
result['limit'] = limit
|
||||
result['total'] = total
|
||||
return result
|
||||
|
||||
def detailContent(self, array):
|
||||
username = array[0]
|
||||
domain = f"{self.host}/api/front/v2/models/username/{username}/cam"
|
||||
rsp = requests.get(domain, headers=self.headers).json()
|
||||
info = rsp['cam']
|
||||
user = rsp['user']['user']
|
||||
id = str(user['id'])
|
||||
country = str(user['country']).strip()
|
||||
isLive = "" if user['isLive'] else " 已下播"
|
||||
flag = self.country_code_to_flag(country)
|
||||
remark = ''
|
||||
if info['show']:
|
||||
show = info['show']['details']['groupShow']
|
||||
BJtime = (datetime.strptime(show["startAt"], "%Y-%m-%dT%H:%M:%SZ") + timedelta(hours=8)).strftime("%m月%d日 %H:%M")
|
||||
remark = f"🎫 始于 {BJtime}"
|
||||
vod = [{
|
||||
"vod_id": id,
|
||||
"vod_name": str(info['topic']).strip(),
|
||||
"vod_pic": str(user['avatarUrl']),
|
||||
"vod_director": f"{flag}{username}{isLive}",
|
||||
"vod_remarks": remark,
|
||||
'vod_play_from': 'StripChat',
|
||||
'vod_play_url': f"{id}${id}"
|
||||
}]
|
||||
result = {}
|
||||
result['list'] = vod
|
||||
return result
|
||||
|
||||
def process_key(self, key: str) -> Tuple[str, str]:
|
||||
tags = {'G': 'girls', 'C': 'couples', 'M': 'men', 'T': 'trans'}
|
||||
parts = key.split(maxsplit=1) # 仅分割第一个空格
|
||||
if len(parts) > 1 and tags.get(parts[0].upper(), ''):
|
||||
return tags[parts[0].upper()], parts[1].strip()
|
||||
return 'girls', key.strip()
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
result = {}
|
||||
if int(pg) > 1:
|
||||
return result
|
||||
tag, key = self.process_key(key)
|
||||
domain = f"{self.host}/api/front/v4/models/search/group/username?query={key}&limit=900&primaryTag={tag}"
|
||||
rsp = requests.get(domain, headers=self.headers).json()
|
||||
users = rsp['models']
|
||||
videos = []
|
||||
for user in users:
|
||||
if not user['isLive']:
|
||||
continue
|
||||
id = str(user['id'])
|
||||
name = str(user['username']).strip()
|
||||
stamp = user['snapshotTimestamp']
|
||||
country = str(user['country']).strip()
|
||||
flag = self.country_code_to_flag(country)
|
||||
remark = "🎫" if user['status'] == "groupShow" else ""
|
||||
videos.append({
|
||||
"vod_id": name,
|
||||
"vod_name": f"{flag}{name}",
|
||||
"vod_pic": f"https://img.doppiocdn.net/thumbs/{stamp}/{id}",
|
||||
"vod_remarks": remark
|
||||
})
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
domain = f"https://edge-hls.doppiocdn.net/hls/{id}/master/{id}_auto.m3u8?playlistType=lowLatency"
|
||||
rsp = requests.get(domain, headers=self.headers).text
|
||||
lines = rsp.strip().split('\n')
|
||||
psch = ''
|
||||
pkey = ''
|
||||
url = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#EXT-X-MOUFLON:'):
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 4:
|
||||
psch = parts[2]
|
||||
pkey = parts[3]
|
||||
if '#EXT-X-STREAM-INF' in line:
|
||||
name_start = line.find('NAME="') + 6
|
||||
name_end = line.find('"', name_start)
|
||||
qn = line[name_start:name_end]
|
||||
# URL在下一行
|
||||
url_base = lines[i + 1]
|
||||
# 组合最终的URL,并加上psch和pkey参数
|
||||
full_url = f"{url_base}&psch={psch}&pkey={pkey}"
|
||||
proxy_url = f"{self.getProxyUrl()}&url={quote(full_url)}"
|
||||
# 将画质和URL添加到列表中
|
||||
url.append(qn)
|
||||
url.append(proxy_url)
|
||||
result = {}
|
||||
result["url"] = url
|
||||
result["parse"] = '0'
|
||||
result["contentType"] = ''
|
||||
result["header"] = self.headers
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = unquote(param['url'])
|
||||
data = self.session.get(url, headers=self.headers, timeout=10)
|
||||
if data.status_code != 200:
|
||||
return [404, "text/plain", ""]
|
||||
data = data.text
|
||||
if "#EXT-X-MOUFLON:FILE" in data:
|
||||
data = self.process_m3u8_content_v2(data)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def process_m3u8_content_v2(self, m3u8_content):
|
||||
lines = m3u8_content.strip().split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if (line.startswith('#EXT-X-MOUFLON:FILE:') and 'media.mp4' in lines[i + 1]):
|
||||
encrypted_data = line.split(':', 2)[2].strip()
|
||||
try:
|
||||
decrypted_data = self.decrypt(encrypted_data, self.stripchat_key)
|
||||
except Exception as e:
|
||||
decrypted_data = self.decrypt(encrypted_data, "Zokee2OhPh9kugh4")
|
||||
lines[i + 1] = lines[i + 1].replace('media.mp4', decrypted_data)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def country_code_to_flag(self, country_code):
|
||||
if len(country_code) != 2 or not country_code.isalpha():
|
||||
return country_code
|
||||
flag_emoji = ''.join([chr(ord(c.upper()) - ord('A') + 0x1F1E6) for c in country_code])
|
||||
return flag_emoji
|
||||
|
||||
def decode_key_compact(self):
|
||||
base64_str = "NTEgNzUgNjUgNjEgNmUgMzQgNjMgNjEgNjkgMzkgNjIgNmYgNGEgNjEgMzUgNjE="
|
||||
decoded = base64.b64decode(base64_str).decode('utf-8')
|
||||
key_bytes = bytes(int(hex_str, 16) for hex_str in decoded.split(" "))
|
||||
return key_bytes.decode('utf-8')
|
||||
|
||||
def compute_hash(self, key: str) -> bytes:
|
||||
"""计算并缓存SHA-256哈希"""
|
||||
if key not in self._hash_cache:
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(key.encode('utf-8'))
|
||||
self._hash_cache[key] = sha256.digest()
|
||||
return self._hash_cache[key]
|
||||
|
||||
def decrypt(self, encrypted_b64: str, key: str) -> str:
|
||||
"""解密Base64编码的密文"""
|
||||
# 修复Base64填充
|
||||
padding = len(encrypted_b64) % 4
|
||||
if padding:
|
||||
encrypted_b64 += '=' * (4 - padding)
|
||||
|
||||
# 计算哈希并解密
|
||||
hash_bytes = self.compute_hash(key)
|
||||
encrypted_data = base64.b64decode(encrypted_b64)
|
||||
|
||||
# 异或解密
|
||||
decrypted_bytes = bytearray()
|
||||
for i, cipher_byte in enumerate(encrypted_data):
|
||||
key_byte = hash_bytes[i % len(hash_bytes)]
|
||||
decrypted_bytes.append(cipher_byte ^ key_byte)
|
||||
return decrypted_bytes.decode('utf-8')
|
||||
|
||||
def create_session_with_retry(self, retries=3, backoff_factor=0.3):
|
||||
self.session = requests.Session()
|
||||
retry_strategy = Retry(
|
||||
total=retries,
|
||||
backoff_factor=backoff_factor,
|
||||
status_forcelist=[429, 500, 502, 503, 504] # 需要重试的状态码
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
@@ -0,0 +1,300 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import html as html_module
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.qmao.net'
|
||||
self.session = requests.Session()
|
||||
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
self.session.headers.update({'User-Agent': self.ua})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'电视剧': '2',
|
||||
'动漫': '3',
|
||||
'短剧': '4',
|
||||
}
|
||||
self._m = chr(0x661f) + chr(0x6cb3)
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = html_module.unescape(text)
|
||||
text = text.replace('\xa0', ' ').replace(' ', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '七猫短剧'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def _extract_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="/voddetail/(\d+)\.html"', html):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+800]
|
||||
# 标题:分类页 BrowseList,首页 FeaturedList,搜索页 MTagBookList
|
||||
title = ''
|
||||
tm = re.search(r'(BrowseList|FeaturedList|MTagBookList)_bookName[^>]*>(.*?)</a>', snippet, re.DOTALL)
|
||||
if tm:
|
||||
title = self._clean(tm.group(2))
|
||||
if not title:
|
||||
tm = re.search(r'title="([^"]*)"', snippet)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
# 封面
|
||||
pic = ''
|
||||
pm = re.search(r'src="([^"]*\.(?:jpg|webp)[^"]*)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).strip()
|
||||
if not pic.startswith('http'):
|
||||
pic = self.site + pic
|
||||
# 备注
|
||||
note = ''
|
||||
nm = re.search(r'(BrowseList|FeaturedList)_(?:lastChapter|tagsBox)[^>]*>(.*?)</(?:a|div)', snippet, re.DOTALL)
|
||||
if nm:
|
||||
note = self._clean(nm.group(2))
|
||||
if not note:
|
||||
nm = re.search(r'(BrowseList|FeaturedList)_bookViewCount[^>]*>([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = self._clean(nm.group(2))
|
||||
# 搜索页:从 img alt 获取集数
|
||||
if not note:
|
||||
am = re.search(r'alt="([^"]*(?:集|完结)[^"]*)"', snippet)
|
||||
if am:
|
||||
note = self._clean(am.group(1))
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
html = self._get(self.site)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
url = f'{self.site}/vodtype/{tid}.html'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if result['list'] else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ''
|
||||
if isinstance(ids, list):
|
||||
vid = ids[0] if ids else ''
|
||||
elif ids:
|
||||
vid = str(ids)
|
||||
if not vid:
|
||||
return result
|
||||
|
||||
# 播放页获取 player_aaaa
|
||||
play_html = self._get(f'{self.site}/vodplay/{vid}-1-1.html')
|
||||
pd = {}
|
||||
if play_html:
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', play_html)
|
||||
if m:
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 详情页
|
||||
detail_html = self._get(f'{self.site}/voddetail/{vid}.html')
|
||||
|
||||
# 标题
|
||||
title = pd.get('vod_data', {}).get('vod_name', '')
|
||||
if not title:
|
||||
m2 = re.search(r'dramaDetail_bookName[^>]*>([^<]+)<', detail_html)
|
||||
if m2:
|
||||
title = self._clean(m2.group(1))
|
||||
if not title:
|
||||
m2 = re.search(r'<title>([^<]+)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
|
||||
|
||||
# 封面
|
||||
pic = ''
|
||||
m2 = re.search(r'dramaDetail_bookCover[^>]*>\s*<img[^>]*src="([^"]*)"', detail_html)
|
||||
if m2:
|
||||
pic = m2.group(1).strip()
|
||||
if not pic.startswith('http'):
|
||||
pic = self.site + pic
|
||||
|
||||
# 标签/类型
|
||||
vod_class = ''
|
||||
m2 = re.search(r'dramaDetail_tagsBox[^>]*>(.*?)</div>', detail_html, re.DOTALL)
|
||||
if m2:
|
||||
vod_class = self._clean(m2.group(1))
|
||||
if not vod_class:
|
||||
vod_class = pd.get('vod_data', {}).get('vod_class', '')
|
||||
|
||||
# 演员
|
||||
actor = pd.get('vod_data', {}).get('vod_actor', '')
|
||||
|
||||
# 导演
|
||||
director = pd.get('vod_data', {}).get('vod_director', '')
|
||||
if director:
|
||||
director = self._m + '、' + director
|
||||
else:
|
||||
director = self._m
|
||||
|
||||
# 播放列表 - 从播放页提取集数
|
||||
play_from = []
|
||||
play_url_list = []
|
||||
|
||||
if play_html:
|
||||
# 提取所有线路
|
||||
tabs = re.findall(r'episode_tabBtn[^>]*data-sid="(\d+)"[^>]*data-from="([^"]*)"[^>]*>([^<]*)<', play_html)
|
||||
if not tabs:
|
||||
tabs = re.findall(r'episode_tabBtn[^>]*>([^<]*)<', play_html)
|
||||
if tabs:
|
||||
tabs = [(str(i+1), '', t) for i, t in enumerate(tabs)]
|
||||
|
||||
# 提取集数链接
|
||||
episodes = []
|
||||
for em in re.finditer(r'<a[^>]*class="CatalogList_linkBox"[^>]*href="(/vodplay/[^"]+)"', play_html):
|
||||
ep_href = em.group(1)
|
||||
# 在 </a> 前提取集数编号
|
||||
snippet = play_html[em.start():em.start()+600]
|
||||
num = re.search(r'>\s*(?:<[^>]*>\s*)*(\d+)\s*</a>', snippet)
|
||||
if num:
|
||||
ep_num = num.group(1).strip()
|
||||
episodes.append(f'第{ep_num}集${ep_href}')
|
||||
else:
|
||||
episodes.append(f'播放${ep_href}')
|
||||
# 备用:data-part
|
||||
if not episodes:
|
||||
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*data-part="([^"]*)"', play_html):
|
||||
episodes.append(f'{em.group(2)}${em.group(1)}')
|
||||
if not episodes:
|
||||
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*>([^<]*)<', play_html):
|
||||
if em.group(2).strip():
|
||||
episodes.append(f'{em.group(2).strip()}${em.group(1)}')
|
||||
|
||||
if episodes:
|
||||
line_name = tabs[0][2] if tabs else '默认'
|
||||
if not line_name:
|
||||
line_name = tabs[0][1] or '默认'
|
||||
play_from.append(self._clean(line_name))
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
# 如果没有从播放页拿到集数,用 player_aaaa 的 URL 直接播放
|
||||
if not play_from and pd:
|
||||
url = pd.get('url', '')
|
||||
from_flag = pd.get('from', '')
|
||||
if url:
|
||||
play_from.append(from_flag or '默认')
|
||||
play_url_list.append(f'播放${url}')
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': vod_class,
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': '',
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url_list)
|
||||
}
|
||||
result['list'].append(vod)
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
play_url = id
|
||||
if not play_url.startswith('http'):
|
||||
play_url = self.site + play_url
|
||||
|
||||
html = self._get(play_url)
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
pd = json.loads(m.group(1))
|
||||
url = pd.get('url', '')
|
||||
if url:
|
||||
result['parse'] = 0
|
||||
result['url'] = url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
|
||||
if not result:
|
||||
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
wd = requests.utils.quote(key)
|
||||
url = f'{self.site}/vodsearch/-------------.html?wd={wd}'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from urllib.parse import quote
|
||||
from Crypto.Hash import MD5
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.cookies.update(self.cookie)
|
||||
self.get_ctoken()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.youku.com'
|
||||
|
||||
shost='https://search.youku.com'
|
||||
|
||||
h5host='https://acs.youku.com'
|
||||
|
||||
ihost='https://v.youku.com'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)',
|
||||
'Referer': f'{host}/'
|
||||
}
|
||||
|
||||
cookie={
|
||||
"__ysuid": "17416134165380iB",
|
||||
"__aysid": "1741613416541WbD",
|
||||
"xlly_s": "1",
|
||||
"isI18n": "false",
|
||||
"cna": "bNdVIKmmsHgCAXW9W6yrQ1/s",
|
||||
"__ayft": "1741672162330",
|
||||
"__arpvid": "1741672162331FBKgrn-1741672162342",
|
||||
"__ayscnt": "1",
|
||||
"__aypstp": "1",
|
||||
"__ayspstp": "3",
|
||||
"tfstk": "gZbiib4JpG-6DqW-B98_2rwPuFrd1fTXQt3vHEp4YpJIBA3OgrWcwOi90RTOo9XVQ5tAM5NcK_CP6Ep97K2ce1XDc59v3KXAgGFLyzC11ET2n8U8yoyib67M3xL25e8gS8pbyzC1_ET4e8URWTsSnHv2uh8VTeJBgEuN3d-ELQAWuKWV36PHGpJ2uEWVTxvicLX1ewyUXYSekxMf-CxMEqpnoqVvshvP_pABOwvXjL5wKqeulm52np_zpkfCDGW9Ot4uKFIRwZtP7vP9_gfAr3KEpDWXSIfWRay-DHIc_Z-hAzkD1i5Ooi5LZ0O5YO_1mUc476YMI3R6xzucUnRlNe_zemKdm172xMwr2L7CTgIkbvndhFAVh3_YFV9Ng__52U4SQKIdZZjc4diE4EUxlFrfKmiXbBOHeP72v7sAahuTtWm78hRB1yV3tmg9bBOEhWVnq5KwOBL5."
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
categories = ["电视剧", "电影", "综艺", "动漫", "少儿", "纪录片", "文化", "亲子", "教育", "搞笑", "生活",
|
||||
"体育", "音乐", "游戏"]
|
||||
classes = [{'type_name': category, 'type_id': category} for category in categories]
|
||||
filters = {}
|
||||
self.typeid = {}
|
||||
with ThreadPoolExecutor(max_workers=len(categories)) as executor:
|
||||
tasks = {
|
||||
executor.submit(self.cf, {'type': category}, True): category
|
||||
for category in categories
|
||||
}
|
||||
|
||||
for future in as_completed(tasks):
|
||||
try:
|
||||
category = tasks[future]
|
||||
session, ft = future.result()
|
||||
filters[category] = ft
|
||||
self.typeid[category] = session
|
||||
except Exception as e:
|
||||
print(f"处理分类 {tasks[future]} 时出错: {str(e)}")
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
vlist = []
|
||||
params={"ms_codes":"2019061000","params":"{\"debug\":0,\"gray\":0,\"pageNo\":1,\"utdid\":\"ZYmGMAAAACkDAMU8hbiMmYdd\",\"userId\":\"\",\"bizKey\":\"YOUKU_WEB\",\"appPackageKey\":\"com.youku.YouKu\",\"showNodeList\":0,\"reqSubNode\":0,\"nodeKey\":\"WEBHOME\",\"bizContext\":\"{\\\"spmA\\\":\\\"a2hja\\\"}\"}","system_info":"{\"device\":\"pcweb\",\"os\":\"pcweb\",\"ver\":\"1.0.0.0\",\"userAgent\":\"Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)\",\"guid\":\"1590141704165YXe\",\"appPackageKey\":\"com.youku.pcweb\",\"young\":0,\"brand\":\"\",\"network\":\"\",\"ouid\":\"\",\"idfa\":\"\",\"scale\":\"\",\"operator\":\"\",\"resolution\":\"\",\"pid\":\"\",\"childGender\":0,\"zx\":0}"}
|
||||
data=self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.home.query/1.0/',params)
|
||||
okey=list(data['data'].keys())[0]
|
||||
for i in data['data'][okey]['data']['nodes'][0]['nodes'][-1]['nodes'][0]['nodes']:
|
||||
if i.get('nodes') and i['nodes'][0].get('data'):
|
||||
i=i['nodes'][0]['data']
|
||||
if i.get('assignId'):
|
||||
vlist.append({
|
||||
'vod_id': i['assignId'],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('vImg') or i.get('img'),
|
||||
'vod_year': i.get('mark',{}).get('data',{}).get('text'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
return {'list': vlist}
|
||||
except Exception as e:
|
||||
print(f"处理主页视频数据时出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
vlist = []
|
||||
result['page'] = pg
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
pagecount = 9999
|
||||
params = {'type': tid}
|
||||
id = self.typeid[tid]
|
||||
params.update(extend)
|
||||
if pg == '1':
|
||||
id=self.cf(params)
|
||||
data=self.session.get(f'{self.host}/category/data?session={id}¶ms={quote(json.dumps(params))}&pageNo={pg}').json()
|
||||
try:
|
||||
data=data['data']['filterData']
|
||||
for i in data['listData']:
|
||||
if i.get('videoLink') and 's=' in i['videoLink']:
|
||||
vlist.append({
|
||||
'vod_id': i.get('videoLink').split('s=')[-1],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('img'),
|
||||
'vod_year': i.get('rightTagText'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
self.typeid[tid]=quote(json.dumps(data['session']))
|
||||
except:
|
||||
pagecount=pg
|
||||
result['list'] = vlist
|
||||
result['pagecount'] = pagecount
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
data=self.session.get(f'{self.ihost}/v_getvideo_info/?showId={ids[0]}').json()
|
||||
v=data['data']
|
||||
vod = {
|
||||
'type_name': v.get('showVideotype'),
|
||||
'vod_year': v.get('lastUpdate'),
|
||||
'vod_remarks': v.get('rc_title'),
|
||||
'vod_actor': v.get('_personNameStr'),
|
||||
'vod_content': v.get('showdesc'),
|
||||
'vod_play_from': '优酷',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
params={"biz":"new_detail_web2","videoId":v.get('vid'),"scene":"web_page","componentVersion":"3","ip":data.get('ip'),"debug":0,"utdid":"ZYmGMAAAACkDAMU8hbiMmYdd","userId":0,"platform":"pc","nextSession":"","gray":0,"source":"pcNoPrev","showId":ids[0]}
|
||||
sdata,index=self.getinfo(params)
|
||||
pdata=sdata['nodes']
|
||||
if index > len(pdata):
|
||||
batch_size = len(pdata)
|
||||
total_batches = ((index + batch_size - 1) // batch_size) - 1
|
||||
ssj = json.loads(sdata['data']['session'])
|
||||
with ThreadPoolExecutor(max_workers=total_batches) as executor:
|
||||
futures = []
|
||||
for batch in range(total_batches):
|
||||
start = batch_size + 1 + (batch * batch_size)
|
||||
end = start + batch_size - 1
|
||||
next_session = ssj.copy()
|
||||
next_session.update({
|
||||
"itemStartStage": start,
|
||||
"itemEndStage": min(end, index)
|
||||
})
|
||||
current_params = params.copy()
|
||||
current_params['nextSession'] = json.dumps(next_session)
|
||||
futures.append((start, executor.submit(self.getvinfo, current_params)))
|
||||
futures.sort(key=lambda x: x[0])
|
||||
|
||||
for _, future in futures:
|
||||
try:
|
||||
result = future.result()
|
||||
pdata.extend(result['nodes'])
|
||||
except Exception as e:
|
||||
print(f"Error fetching data: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join([f"{i['data'].get('title')}${i['data']['action'].get('value')}" for i in pdata])
|
||||
return {'list': [vod]}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {'list': [{'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'呜呜呜${self.host}'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.session.get(f'{self.shost}/api/search?pg={pg}&keyword={key}').json()
|
||||
vlist = []
|
||||
for i in data['pageComponentList']:
|
||||
if i.get('commonData') and (i['commonData'].get('showId') or i['commonData'].get('realShowId')):
|
||||
i=i['commonData']
|
||||
vlist.append({
|
||||
'vod_id': i.get('showId') or i.get('realShowId'),
|
||||
'vod_name': i['titleDTO'].get('displayName'),
|
||||
'vod_pic': i['posterDTO'].get('vThumbUrl'),
|
||||
'vod_year': i.get('feature'),
|
||||
'vod_remarks': i.get('updateNotice')
|
||||
})
|
||||
return {'list': vlist, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'jx':1,'parse': 1, 'url': f"{self.ihost}/video?vid={id}", 'header': ''}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def cf(self,params,b=False):
|
||||
response = self.session.get(f'{self.host}/category/data?params={quote(json.dumps(params))}&optionRefresh=1&pageNo=1').json()
|
||||
data=response['data']['filterData']
|
||||
session=quote(json.dumps(data['session']))
|
||||
if b:
|
||||
return session,self.get_filter_data(data['filter']['filterData'][1:])
|
||||
return session
|
||||
|
||||
def process_key(self, key):
|
||||
if '_' not in key:
|
||||
return key
|
||||
parts = key.split('_')
|
||||
result = parts[0]
|
||||
for part in parts[1:]:
|
||||
if part:
|
||||
result += part[0].upper() + part[1:]
|
||||
return result
|
||||
|
||||
def get_filter_data(self, data):
|
||||
result = []
|
||||
try:
|
||||
for item in data:
|
||||
if not item.get('subFilter'):
|
||||
continue
|
||||
first_sub = item['subFilter'][0]
|
||||
if not first_sub.get('filterType'):
|
||||
continue
|
||||
filter_item = {
|
||||
'key': self.process_key(first_sub['filterType']),
|
||||
'name': first_sub['title'],
|
||||
'value': []
|
||||
}
|
||||
for sub in item['subFilter']:
|
||||
if 'value' in sub:
|
||||
filter_item['value'].append({
|
||||
'n': sub['title'],
|
||||
'v': sub['value']
|
||||
})
|
||||
if filter_item['value']:
|
||||
result.append(filter_item)
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理筛选数据时出错: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
def get_ctoken(self):
|
||||
data=self.session.get(f'{self.h5host}/h5/mtop.ykrec.recommendservice.recommend/1.0/?jsv=2.6.1&appKey=24679788')
|
||||
|
||||
def md5(self,t,text):
|
||||
h = MD5.new()
|
||||
token=self.session.cookies.get('_m_h5_tk').split('_')[0]
|
||||
data=f"{token}&{t}&24679788&{text}"
|
||||
h.update(data.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getdata(self, url, params, recursion_count=0, max_recursion=3):
|
||||
data = json.dumps(params)
|
||||
t = int(time.time() * 1000)
|
||||
jsdata = {
|
||||
'appKey': '24679788',
|
||||
't': t,
|
||||
'sign': self.md5(t, data),
|
||||
'data': data
|
||||
}
|
||||
response = self.session.get(url, params=jsdata)
|
||||
if '令牌过期' in response.text:
|
||||
if recursion_count >= max_recursion:
|
||||
raise Exception("达到最大递归次数,无法继续请求")
|
||||
self.get_ctoken()
|
||||
return self.getdata(url, params, recursion_count + 1, max_recursion)
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
def getvinfo(self,params):
|
||||
body = {
|
||||
"ms_codes": "2019030100",
|
||||
"params": json.dumps(params),
|
||||
"system_info": "{\"os\":\"iku\",\"device\":\"iku\",\"ver\":\"9.2.9\",\"appPackageKey\":\"com.youku.iku\",\"appPackageId\":\"pcweb\"}"
|
||||
}
|
||||
data = self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.gateway.new.execute/1.0/', body)
|
||||
okey = list(data['data'].keys())[0]
|
||||
i = data['data'][okey]['data']
|
||||
return i
|
||||
|
||||
def getinfo(self,params):
|
||||
i = self.getvinfo(params)
|
||||
jdata=i['nodes'][0]['nodes'][3]
|
||||
info=i['data']['extra']['episodeTotal']
|
||||
if i['data']['extra']['showCategory'] in ['电影','游戏']:
|
||||
jdata = i['nodes'][0]['nodes'][4]
|
||||
return jdata,info
|
||||
@@ -0,0 +1,489 @@
|
||||
# coding=utf-8
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import quote, urljoin, urlparse, parse_qs
|
||||
import sys
|
||||
# 导入外部库
|
||||
from bs4 import BeautifulSoup
|
||||
import gzip
|
||||
sys.path.append("..")
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.name = "小鸭子看看"
|
||||
self.hosts = {
|
||||
"main": "https://xiaoyakankan.com",
|
||||
"tw": "https://tw.xiaoyakankan.com"
|
||||
}
|
||||
self.default_host = "tw"
|
||||
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
|
||||
# 视频格式支持
|
||||
self.VIDEO_FORMATS = ['.m3u8', '.mp4', '.flv', '.avi', '.mkv', '.mov']
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=""):
|
||||
if extend:
|
||||
try:
|
||||
config = json.loads(extend)
|
||||
if config.get("host") in self.hosts:
|
||||
self.default_host = config["host"]
|
||||
self.log(f"已切换默认域名至:{self.hosts[self.default_host]}", "INFO")
|
||||
except:
|
||||
self.log("初始化参数解析失败,使用默认tw子域名", "WARNING")
|
||||
|
||||
def log(self, msg, level="INFO"):
|
||||
print(f"[{level}] [{self.name}] {time.strftime('%Y-%m-%d %H:%M:%S')} - {msg}")
|
||||
|
||||
def get_current_host(self):
|
||||
return self.hosts[self.default_host]
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result['class'] = [
|
||||
{"type_name": "电影", "type_id": "10", "land": "1", "filters": [
|
||||
{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": "10"},
|
||||
{"n": "动作片", "v": "1001"},
|
||||
{"n": "喜剧片", "v": "1002"},
|
||||
{"n": "爱情片", "v": "1003"},
|
||||
{"n": "科幻片", "v": "1004"},
|
||||
{"n": "恐怖片", "v": "1005"},
|
||||
{"n": "剧情片", "v": "1006"},
|
||||
{"n": "战争片", "v": "1007"},
|
||||
{"n": "纪录片", "v": "1008"},
|
||||
{"n": "微电影", "v": "1009"},
|
||||
{"n": "动漫电影", "v": "1010"},
|
||||
{"n": "奇幻片", "v": "1011"},
|
||||
{"n": "动画片", "v": "1013"},
|
||||
{"n": "犯罪片", "v": "1014"},
|
||||
{"n": "悬疑片", "v": "1016"},
|
||||
{"n": "欧美片", "v": "1017"},
|
||||
{"n": "邵氏电影", "v": "1019"},
|
||||
{"n": "同性片", "v": "1021"},
|
||||
{"n": "家庭片", "v": "1024"},
|
||||
{"n": "古装片", "v": "1025"},
|
||||
{"n": "历史片", "v": "1026"},
|
||||
{"n": "4K电影", "v": "1027"}
|
||||
]}
|
||||
]},
|
||||
{"type_name": "连续剧", "type_id": "11", "land": "1", "filters": [
|
||||
{"key": "class", "name": "地区类型", "value": [
|
||||
{"n": "全部", "v": "11"},
|
||||
{"n": "国产剧", "v": "1101"},
|
||||
{"n": "香港剧", "v": "1102"},
|
||||
{"n": "台湾剧", "v": "1105"},
|
||||
{"n": "韩国剧", "v": "1103"},
|
||||
{"n": "欧美剧", "v": "1104"},
|
||||
{"n": "日本剧", "v": "1106"},
|
||||
{"n": "泰国剧", "v": "1108"},
|
||||
{"n": "港台剧", "v": "1110"},
|
||||
{"n": "日韩剧", "v": "1111"},
|
||||
{"n": "东南亚剧", "v": "1112"},
|
||||
{"n": "海外剧", "v": "1107"}
|
||||
]}
|
||||
]},
|
||||
{"type_name": "综艺", "type_id": "12", "land": "1", "filters": [
|
||||
{"key": "class", "name": "地区类型", "value": [
|
||||
{"n": "全部", "v": "12"},
|
||||
{"n": "内地综艺", "v": "1201"},
|
||||
{"n": "港台综艺", "v": "1202"},
|
||||
{"n": "日韩综艺", "v": "1203"},
|
||||
{"n": "欧美综艺", "v": "1204"},
|
||||
{"n": "国外综艺", "v": "1205"}
|
||||
]}
|
||||
]},
|
||||
{"type_name": "动漫", "type_id": "13", "land": "1", "filters": [
|
||||
{"key": "class", "name": "地区类型", "value": [
|
||||
{"n": "全部", "v": "13"},
|
||||
{"n": "国产动漫", "v": "1301"},
|
||||
{"n": "日韩动漫", "v": "1302"},
|
||||
{"n": "欧美动漫", "v": "1303"},
|
||||
{"n": "海外动漫", "v": "1305"},
|
||||
{"n": "里番", "v": "1307"}
|
||||
]}
|
||||
]},
|
||||
{"type_name": "福利", "type_id": "15", "land": "1", "filters": [
|
||||
{"key": "class", "name": "地区类型", "value": [
|
||||
{"n": "全部", "v": "15"},
|
||||
{"n": "韩国情色片", "v": "1551"},
|
||||
{"n": "日本情色片", "v": "1552"},
|
||||
{"n": "大陆情色片", "v": "1555"},
|
||||
{"n": "香港情色片", "v": "1553"},
|
||||
{"n": "台湾情色片", "v": "1554"},
|
||||
{"n": "美国情色片", "v": "1556"},
|
||||
{"n": "欧洲情色片", "v": "1557"},
|
||||
{"n": "印度情色片", "v": "1558"},
|
||||
{"n": "东南亚情色片", "v": "1559"},
|
||||
{"n": "其它情色片", "v": "1550"}
|
||||
]}
|
||||
]}
|
||||
]
|
||||
|
||||
# 将所有筛选器数据添加进 result['filters'] 中
|
||||
result['filters'] = {
|
||||
"10": result['class'][0]['filters'],
|
||||
"11": result['class'][1]['filters'],
|
||||
"12": result['class'][2]['filters'],
|
||||
"13": result['class'][3]['filters'],
|
||||
"15": result['class'][4]['filters'],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
url = self.get_current_host()
|
||||
r = self.fetch(url, headers={"User-Agent": self.ua})
|
||||
|
||||
if r.status_code != 200:
|
||||
self.log(f"首页推荐请求失败,状态码:{r.status_code}", "ERROR")
|
||||
return {'list': []}
|
||||
|
||||
# 使用新的正则表达式来获取视频列表项
|
||||
pattern = r'<a class="link" href="(/post/[^"]+\.html)".*?<img[^>]*data-src="([^"]+)".*?alt="([^"]+)".*?(?:<div class="tag1[^>]*>([^<]+)</div>)?.*?(?:<div class="tag2">([^<]+)</div>)?'
|
||||
matches = re.findall(pattern, r.text, re.DOTALL)
|
||||
|
||||
video_list = []
|
||||
for match in matches[:12]: # 限制12个结果
|
||||
try:
|
||||
link, img_src, title, tag1, tag2 = match
|
||||
vod_id_match = re.search(r'/post/(.*?)\.html', link)
|
||||
if not vod_id_match:
|
||||
continue
|
||||
|
||||
vod_id = vod_id_match.group(1)
|
||||
|
||||
# 组合备注信息
|
||||
remarks = []
|
||||
if tag1:
|
||||
remarks.append(tag1.strip())
|
||||
if tag2:
|
||||
remarks.append(tag2.strip())
|
||||
|
||||
vod_remarks = " / ".join(remarks) if remarks else "最新"
|
||||
|
||||
# 处理图片URL
|
||||
if img_src.startswith('//'):
|
||||
img_url = 'https:' + img_src
|
||||
elif not img_src.startswith('http'):
|
||||
img_url = urljoin(self.get_current_host(), img_src)
|
||||
else:
|
||||
img_url = img_src
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': img_url,
|
||||
'vod_remarks': vod_remarks
|
||||
}
|
||||
|
||||
video_list.append(vod)
|
||||
except Exception as e:
|
||||
self.log(f"首页推荐项解析失败:{str(e)}", "ERROR")
|
||||
continue
|
||||
|
||||
self.log(f"首页推荐成功解析{len(video_list)}个项", "INFO")
|
||||
return {'list': video_list}
|
||||
except Exception as e:
|
||||
self.log(f"首页推荐内容获取失败:{str(e)}", "ERROR")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 40, 'total': 0}
|
||||
try:
|
||||
# 修复:检查 extend 参数,以支持筛选功能
|
||||
filter_tid = tid
|
||||
if extend and 'class' in extend and extend['class']:
|
||||
filter_tid = extend['class']
|
||||
|
||||
# 修复分类URL构建,使用 filter_tid
|
||||
url = f"{self.get_current_host()}/cat/{filter_tid}"
|
||||
if int(pg) > 1:
|
||||
url = f"{url}-{pg}"
|
||||
url = f"{url}.html"
|
||||
|
||||
r = self.fetch(url, headers={"User-Agent": self.ua})
|
||||
if r.status_code != 200:
|
||||
self.log(f"分类页请求失败,URL:{url},状态码:{r.status_code}", "ERROR")
|
||||
return result
|
||||
|
||||
# 修复:使用更健壮的正则来提取所有视频列表项
|
||||
items = re.findall(r'<div class="item">(.*?)<a class="title"', r.text, re.DOTALL)
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
link_match = re.search(r'<a class="link" href="(/post/[^"]+\.html)"', item)
|
||||
img_match = re.search(r'<img[^>]*data-src="([^"]+)"', item)
|
||||
title_match = re.search(r'data-src="[^"]+" alt="([^"]+)"', item)
|
||||
tag1_match = re.search(r'<div class="tag1[^>]*>([^<]+)</div>', item)
|
||||
tag2_match = re.search(r'<div class="tag2">([^<]+)</div>', item)
|
||||
|
||||
if not link_match or not img_match or not title_match:
|
||||
continue
|
||||
|
||||
link = link_match.group(1)
|
||||
img_src = img_match.group(1)
|
||||
title = title_match.group(1).strip()
|
||||
vod_id = re.search(r'/post/(.*?)\.html', link).group(1)
|
||||
|
||||
remarks = []
|
||||
if tag1_match:
|
||||
remarks.append(tag1_match.group(1).strip())
|
||||
if tag2_match:
|
||||
remarks.append(tag2_match.group(1).strip())
|
||||
|
||||
vod_remarks = " / ".join(remarks) if remarks else "分类内容"
|
||||
|
||||
# 处理图片URL
|
||||
if img_src.startswith('//'):
|
||||
img_url = 'https:' + img_src
|
||||
elif not img_src.startswith('http'):
|
||||
img_url = urljoin(self.get_current_host(), img_src)
|
||||
else:
|
||||
img_url = img_src
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': img_url,
|
||||
'vod_remarks': vod_remarks
|
||||
}
|
||||
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
self.log(f"分类项解析失败:{str(e)}", "ERROR")
|
||||
continue
|
||||
|
||||
# 修复:使用更健壮的正则来提取分页信息
|
||||
page_pattern = r'/cat/\d+-(\d+)\.html'
|
||||
page_matches = re.findall(page_pattern, r.text)
|
||||
|
||||
if page_matches:
|
||||
page_nums = [int(num) for num in page_matches if num.isdigit()]
|
||||
result['pagecount'] = max(page_nums) if page_nums else 1
|
||||
else:
|
||||
result['pagecount'] = int(pg)
|
||||
|
||||
self.log(f"分类{tid}第{pg}页:解析{len(result['list'])}项", "INFO")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log(f"分类内容获取失败:{str(e)}", "ERROR")
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {"list": []}
|
||||
if not ids:
|
||||
return result
|
||||
|
||||
vod_id = ids[0]
|
||||
try:
|
||||
detail_url = f"{self.get_current_host()}/post/{vod_id}.html"
|
||||
r = self.fetch(detail_url, headers={"User-Agent": self.ua})
|
||||
if r.status_code != 200:
|
||||
self.log(f"详情页请求失败,状态码:{r.status_code}", "ERROR")
|
||||
return result
|
||||
|
||||
soup = BeautifulSoup(r.text, 'html.parser')
|
||||
|
||||
# 提取标题
|
||||
title_tag = soup.find('title')
|
||||
title = title_tag.text.replace(" - 小鴨看看", "").strip() if title_tag else "未知标题"
|
||||
|
||||
# 提取封面图
|
||||
cover_tag = soup.find('img', {'data-poster': True})
|
||||
cover_url = ""
|
||||
if cover_tag and cover_tag.get('data-poster'):
|
||||
cover_url = cover_tag['data-poster']
|
||||
if cover_url.startswith('//'):
|
||||
cover_url = 'https:' + cover_url
|
||||
elif not cover_url.startswith('http'):
|
||||
cover_url = urljoin(self.get_current_host(), cover_url)
|
||||
|
||||
# 提取描述
|
||||
desc_tag = soup.find('meta', {'name': 'description'})
|
||||
desc = desc_tag['content'].strip() if desc_tag and desc_tag.get('content') else ""
|
||||
|
||||
# 提取播放线路和剧集
|
||||
play_sources = []
|
||||
play_urls = []
|
||||
|
||||
# 从JavaScript中提取播放信息
|
||||
pp_data = None
|
||||
script_pattern = re.search(r'var pp\s*=\s*({.*?});', r.text, re.DOTALL)
|
||||
if script_pattern:
|
||||
try:
|
||||
pp_data = json.loads(script_pattern.group(1))
|
||||
except Exception as e:
|
||||
self.log(f"解析JavaScript播放信息失败:{str(e)}", "ERROR")
|
||||
|
||||
# 查找所有播放线路的容器
|
||||
source_blocks = soup.find_all('div', class_='source')
|
||||
|
||||
for idx, block in enumerate(source_blocks):
|
||||
source_name_tag = block.find('span', class_='name')
|
||||
source_name = source_name_tag.text.strip() if source_name_tag else f"线路{idx+1}"
|
||||
|
||||
resolution_tag = block.find('span', class_='res')
|
||||
resolution = resolution_tag.text.strip() if resolution_tag else ""
|
||||
|
||||
# 组合线路名称和分辨率
|
||||
full_source_name = f"{source_name} ({resolution})" if resolution else source_name
|
||||
play_sources.append(full_source_name)
|
||||
|
||||
episodes = []
|
||||
if pp_data and 'lines' in pp_data and len(pp_data['lines']) > idx:
|
||||
urls = pp_data['lines'][idx][3]
|
||||
|
||||
if isinstance(urls, list) and urls:
|
||||
for ep_idx, url in enumerate(urls):
|
||||
if isinstance(url, str) and any(url.endswith(fmt) for fmt in self.VIDEO_FORMATS):
|
||||
# 修复集数命名逻辑
|
||||
episode_name_match = re.search(r'ep-([\d\w]+)', url)
|
||||
if episode_name_match:
|
||||
episode_name = f"第{episode_name_match.group(1)}集"
|
||||
elif len(urls) == 1:
|
||||
episode_name = "全集"
|
||||
else:
|
||||
episode_name = f"第{ep_idx+1}集"
|
||||
|
||||
episodes.append(f"{episode_name}${url}")
|
||||
|
||||
if episodes:
|
||||
play_urls.append("#".join(episodes))
|
||||
else:
|
||||
play_urls.append("")
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": title,
|
||||
"vod_pic": cover_url,
|
||||
"vod_content": desc,
|
||||
"vod_play_from": "$$$".join(play_sources) if play_sources else "",
|
||||
"vod_play_url": "$$$".join(play_urls) if play_urls else ""
|
||||
}
|
||||
|
||||
result["list"].append(vod)
|
||||
self.log(f"详情页解析成功,ID:{vod_id}", "INFO")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log(f"详情页解析失败,ID:{vod_id},错误:{str(e)}", "ERROR")
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
# 如果id已经是URL,直接返回
|
||||
if id.startswith('http'):
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": '',
|
||||
"url": id,
|
||||
"header": {
|
||||
"User-Agent": self.ua,
|
||||
"Referer": self.get_current_host() + "/"
|
||||
}
|
||||
}
|
||||
|
||||
# 这是一个简单的播放器URL解析,如果原始URL本身就是有效的播放地址,就直接返回
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": '',
|
||||
"url": id,
|
||||
"header": {
|
||||
"User-Agent": self.ua,
|
||||
"Referer": self.get_current_host() + "/"
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"播放地址解析失败:{str(e)}", "ERROR")
|
||||
return {"parse": 0, "playUrl": '', "url": id, "header": {"User-Agent": self.ua}}
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
result = {"list": []}
|
||||
try:
|
||||
# 构造Google搜索URL(带站点限定)
|
||||
google_search_url = f"https://www.google.com/search?q={quote(key)}&sitesearch=xiaoyakankan.com"
|
||||
self.log(f"构造Google搜索URL: {google_search_url}")
|
||||
|
||||
# 伪装更多请求头
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Referer": "https://www.google.com/",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
|
||||
r = self.fetch(google_search_url, headers=headers, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.log(f"Google搜索请求失败,状态码:{r.status_code},内容:{r.text[:200]}", "ERROR")
|
||||
return result
|
||||
|
||||
# 处理gzip压缩响应
|
||||
if 'gzip' in r.headers.get('Content-Encoding', ''):
|
||||
r._content = gzip.decompress(r.content)
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(r.text, 'html.parser')
|
||||
# 寻找所有包含搜索结果的a标签
|
||||
all_links = soup.find_all('a', href=re.compile(r'/url\?q='))
|
||||
|
||||
for a_tag in all_links[:15]: # 限制最多15个结果
|
||||
link = a_tag['href']
|
||||
|
||||
# 解析真实URL
|
||||
parsed_url = urlparse(link)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
|
||||
if 'q' in query_params:
|
||||
real_link = query_params['q'][0]
|
||||
|
||||
# 检查链接是否属于目标站点
|
||||
if self.get_current_host() in real_link:
|
||||
# 尝试从链接中提取影片ID
|
||||
vod_id_match = re.search(r'/post/([^/]+)\.html', real_link)
|
||||
if not vod_id_match:
|
||||
continue
|
||||
|
||||
vod_id = vod_id_match.group(1)
|
||||
|
||||
# 获取标题和图片(这里因为Google搜索结果没有图片,所以图片留空)
|
||||
title_tag = a_tag.find('h3')
|
||||
if not title_tag:
|
||||
# 有时标题在父级或其他元素中
|
||||
title_tag = a_tag.find('div', class_='g')
|
||||
|
||||
title = title_tag.text.strip() if title_tag else "未知标题"
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": title,
|
||||
"vod_pic": "",
|
||||
"vod_remarks": "Google搜索结果"
|
||||
}
|
||||
|
||||
result["list"].append(vod)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"解析Google搜索结果失败:{str(e)}", "ERROR")
|
||||
return result
|
||||
|
||||
self.log(f"Google搜索成功解析{len(result['list'])}个项", "INFO")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log(f"搜索内容获取失败:{str(e)}", "ERROR")
|
||||
return result
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
"""判断是否为视频格式"""
|
||||
return any(url.lower().endswith(fmt) for fmt in self.VIDEO_FORMATS)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import urllib3
|
||||
import concurrent.futures
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
host, userid, episode_list = '', '', []
|
||||
|
||||
# ---------- 加密与签名相关常量 ----------
|
||||
PUB_KEY_B64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB"
|
||||
APP_SIGN_SHA1 = "09a8dc51639a31801af5f6418caebfabc695eb24"
|
||||
DEVICE_ID = "2d590b9842d064a1"
|
||||
|
||||
# RSA 私钥(用于解密响应)
|
||||
PRIV_KEY_B64 = """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg=="""
|
||||
|
||||
headers = {
|
||||
'User-Agent': "okhttp/4.12.0",
|
||||
'Connection': "Keep-Alive",
|
||||
'Accept-Encoding': "gzip",
|
||||
'Content-Type': "application/json;charset=UTF-8",
|
||||
'Cache-Control': "no-cache",
|
||||
'token': "",
|
||||
'deviceId': DEVICE_ID,
|
||||
'client': "app",
|
||||
'deviceType': "Android"
|
||||
}
|
||||
|
||||
# ---------- RSA 加密 ----------
|
||||
def rsa_encrypt(self, data: str) -> str:
|
||||
key = RSA.import_key(base64.b64decode(self.PUB_KEY_B64))
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted = cipher.encrypt(data.encode('utf-8'))
|
||||
return base64.b64encode(encrypted).decode('utf-8')
|
||||
|
||||
# ---------- RSA 解密(支持分块) ----------
|
||||
def rsa_decrypt(self, encrypted_b64: str) -> str:
|
||||
key = RSA.import_key(base64.b64decode(self.PRIV_KEY_B64))
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted_bytes = base64.b64decode(encrypted_b64)
|
||||
block_size = 256
|
||||
decrypted_parts = []
|
||||
for i in range(0, len(encrypted_bytes), block_size):
|
||||
block = encrypted_bytes[i:i+block_size]
|
||||
decrypted_parts.append(cipher.decrypt(block, None))
|
||||
return b''.join(decrypted_parts).decode('utf-8')
|
||||
|
||||
# ---------- 构建签名参数 ----------
|
||||
def build_params_string(self, episode_id="", episode_index="", vid="", player_id="", type_id="", user_id=""):
|
||||
return (f"episodeId{episode_id}"
|
||||
f"episodeIndex{episode_index}"
|
||||
f"id{vid}"
|
||||
f"playerId{player_id}"
|
||||
f"source0"
|
||||
f"typeId{type_id}"
|
||||
f"userId{user_id}")
|
||||
|
||||
def generate_sign(self, timestamp: str, params_str: str, device_id: str) -> str:
|
||||
raw = f"SaltLSFBTimestamp{timestamp}Params{params_str}ClientappDeviceId{device_id}"
|
||||
b64 = base64.b64encode(raw.encode('utf-8')).decode('utf-8')
|
||||
md5 = hashlib.md5(b64.encode('utf-8')).hexdigest().upper()
|
||||
return md5
|
||||
|
||||
def build_encrypted_headers(self, body_json: str, params_str: str) -> dict:
|
||||
timestamp = str(int(time.time()))
|
||||
encrypted_key = self.rsa_encrypt(body_json)
|
||||
snjm = self.rsa_encrypt("113")
|
||||
appsign = self.rsa_encrypt(self.APP_SIGN_SHA1)
|
||||
sign = self.generate_sign(timestamp, params_str, self.DEVICE_ID)
|
||||
|
||||
headers = {
|
||||
"snjm": snjm,
|
||||
"appsign": appsign,
|
||||
"timestamp": timestamp,
|
||||
"sign": sign,
|
||||
"deviceId": self.DEVICE_ID,
|
||||
"token": self.headers.get('token', ''),
|
||||
"client": "app",
|
||||
"deviceType": "Android",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "okhttp/4.12.0"
|
||||
}
|
||||
return headers, {"key": encrypted_key}
|
||||
|
||||
# ---------- 原有接口(保持不变) ----------
|
||||
def init(self, extend=''):
|
||||
self.headers['deviceId'] = self.DEVICE_ID
|
||||
self.host = 'http://qkys.qukanwh.com'
|
||||
response = self.fetch(f'{self.host}/api/v1/app/user/visitorInfo', headers=self.headers).json()
|
||||
self.userid = response['data']['id']
|
||||
token = response['data']['token']
|
||||
self.headers['token'] = token
|
||||
|
||||
def homeContent(self, filter):
|
||||
response = self.post(f'{self.host}/api/v1/app/screen/screenType', headers=self.headers).json()
|
||||
data = response['data']
|
||||
classes = []
|
||||
for i in data:
|
||||
classes.append({'type_id': i['id'], 'type_name': i['name']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
response = self.post(f'{self.host}/api/v1/app/recommend/recommendList', headers=self.headers).json()
|
||||
data = response['data']
|
||||
videos = []
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_to_id = {
|
||||
executor.submit(
|
||||
self.post,
|
||||
f'{self.host}/api/v1/app/recommend/recommendSubList',
|
||||
data=json.dumps({
|
||||
"condition": item['id'],
|
||||
"pageNum": 1,
|
||||
"pageSize": 6
|
||||
}),
|
||||
headers=self.headers
|
||||
): item['id'] for item in data
|
||||
}
|
||||
for future in concurrent.futures.as_completed(future_to_id):
|
||||
try:
|
||||
response = future.result().json()
|
||||
for video in response['data']['records']:
|
||||
videos.append({
|
||||
"vod_id": video['id'],
|
||||
"vod_name": video['name'],
|
||||
"vod_pic": video['cover']
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Request failed for item {future_to_id[future]}: {str(e)}")
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
payload = {
|
||||
"condition": {
|
||||
"classify": "",
|
||||
"region": "",
|
||||
"sreecnTypeEnum": "NEWEST",
|
||||
"typeId": tid,
|
||||
"year": ""
|
||||
},
|
||||
"pageNum": pg,
|
||||
"pageSize": 40
|
||||
}
|
||||
response = self.post(f'{self.host}/api/v1/app/screen/screenMovie', data=json.dumps(payload), headers=self.headers).json()
|
||||
videos = []
|
||||
for i in response['data']['records']:
|
||||
videos.append({
|
||||
"vod_id": i['id'],
|
||||
"vod_name": i['name'],
|
||||
"vod_pic": i['cover'],
|
||||
"vod_remarks": i['area'],
|
||||
"vod_year": i['year']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
payload = {
|
||||
"condition": {
|
||||
"value": key
|
||||
},
|
||||
"pageNum": pg,
|
||||
"pageSize": 40
|
||||
}
|
||||
response = self.post(f'{self.host}/api/v1/app/search/searchMovie', data=json.dumps(payload), headers=self.headers).json()
|
||||
videos = []
|
||||
for i in response['data']['records']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['cover'],
|
||||
'vod_remarks': i['area'],
|
||||
'vod_year': i['year'],
|
||||
'vod_area': i['area'],
|
||||
'vod_content': i['desc']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
# ---------- 详情页(已集成解密) ----------
|
||||
def detailContent(self, ids):
|
||||
type_id = "M15" # 注意:原脚本写死为 M17,可根据需要修改
|
||||
vid = ids[0]
|
||||
body = {
|
||||
"id": vid,
|
||||
"source": 0,
|
||||
"typeId": type_id,
|
||||
"userId": self.userid,
|
||||
"episodeId": "",
|
||||
"episodeIndex": "",
|
||||
"playerId": ""
|
||||
}
|
||||
body_json = json.dumps(body, separators=(',', ':'))
|
||||
params_str = self.build_params_string(
|
||||
episode_id="",
|
||||
episode_index="",
|
||||
vid=str(vid),
|
||||
player_id="",
|
||||
type_id=type_id,
|
||||
user_id=str(self.userid)
|
||||
)
|
||||
headers, payload = self.build_encrypted_headers(body_json, params_str)
|
||||
|
||||
# 发送加密请求
|
||||
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(payload), headers=headers).json()
|
||||
encrypted_data = resp_raw.get('data')
|
||||
if not encrypted_data:
|
||||
raise Exception("响应中 data 为空")
|
||||
# 解密 data 字段
|
||||
decrypted_json_str = self.rsa_decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_json_str)
|
||||
|
||||
# 后续处理与原脚本相同
|
||||
currentplayerid = data['playerId']
|
||||
play_urls = []
|
||||
play_url = []
|
||||
show = []
|
||||
for i in data['episodeList']:
|
||||
play_url.append(f"{i['episode']}${ids[0]}@{currentplayerid}@{i['id']}@episode")
|
||||
play_urls.append('#'.join(play_url))
|
||||
moviePlayerList = data['moviePlayerList']
|
||||
for i2 in moviePlayerList:
|
||||
if i2['id'] == currentplayerid:
|
||||
show.append(i2['moviePlayerName'])
|
||||
for j in moviePlayerList:
|
||||
playerid = j['id']
|
||||
episodeTotal = j.get('episodeTotal')
|
||||
if playerid == currentplayerid or episodeTotal is None:
|
||||
continue
|
||||
play_url = []
|
||||
for k in range(1, episodeTotal + 1):
|
||||
play_url.append(f"第{k}集${k}@{playerid}@{ids[0]}@virtual")
|
||||
play_urls.append('#'.join(play_url))
|
||||
if j['moviePlayerName'] not in show:
|
||||
show.append(j['moviePlayerName'])
|
||||
|
||||
# 获取简介(此接口可能无需加密,保持原样)
|
||||
payload_desc = {
|
||||
"id": ids[0],
|
||||
"typeId": type_id
|
||||
}
|
||||
response_desc = self.post(f'{self.host}/api/v1/app/play/movieDesc', data=json.dumps(payload_desc), headers=self.headers).json()
|
||||
data2 = response_desc['data']
|
||||
|
||||
video = {
|
||||
'vod_id': data2['id'],
|
||||
'vod_name': data2['name'],
|
||||
'vod_pic': data2['cover'],
|
||||
'vod_content': data2['introduce'],
|
||||
'vod_year': data2['year'],
|
||||
'vod_area': data2['area'],
|
||||
'vod_remarks': '',
|
||||
'vod_score': data2['score'],
|
||||
'type_name': data2['classify'],
|
||||
'vod_director': data2['director'],
|
||||
'vod_actor': data2['star'],
|
||||
'vod_play_from': '$$$'.join(show),
|
||||
'vod_play_url': '$$$'.join(play_urls)
|
||||
}
|
||||
return {'list': [video]}
|
||||
|
||||
# ---------- 播放页(已集成解密) ----------
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
param, playerid, param2, param3 = id.split('@')
|
||||
if param3 == 'virtual':
|
||||
payload = {
|
||||
"episodeIndex": str(int(param) - 1),
|
||||
"id": int(param2),
|
||||
"playerId": playerid,
|
||||
"source": 0,
|
||||
"typeId": "M15",
|
||||
"userId": self.userid,
|
||||
"episodeId": ""
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"episodeId": param2,
|
||||
"id": int(param),
|
||||
"playerId": playerid,
|
||||
"source": 0,
|
||||
"typeId": "M15",
|
||||
"userId": self.userid,
|
||||
"episodeIndex": ""
|
||||
}
|
||||
body_json = json.dumps(payload, separators=(',', ':'))
|
||||
print(body_json)
|
||||
params_str = self.build_params_string(
|
||||
episode_id=payload.get("episodeId", ""),
|
||||
episode_index=payload.get("episodeIndex", ""),
|
||||
vid=str(payload["id"]),
|
||||
player_id=payload["playerId"],
|
||||
type_id=payload["typeId"],
|
||||
user_id=str(payload["userId"])
|
||||
)
|
||||
print(params_str)
|
||||
headers, encrypted_payload = self.build_encrypted_headers(body_json, params_str)
|
||||
print(headers)
|
||||
print(encrypted_payload)
|
||||
# 获取播放信息(加密响应)
|
||||
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(encrypted_payload), headers=headers).json()
|
||||
encrypted_data = resp_raw.get('data')
|
||||
if not encrypted_data:
|
||||
raise Exception("响应中 data 为空")
|
||||
decrypted_json_str = self.rsa_decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_json_str)
|
||||
print(data)
|
||||
parse_url = data['url']
|
||||
playerid = data['playerId']
|
||||
|
||||
# 调用分析接口(注:analysisMovieUrl 的响应可能也是加密的,但原脚本直接取 data,这里暂不做额外解密)
|
||||
analysis_body = {
|
||||
"playerUrl": parse_url,
|
||||
"playerId": playerid
|
||||
}
|
||||
analysis_json = json.dumps(analysis_body, separators=(',', ':'))
|
||||
# analysisMovieUrl 接口的参数拼接?理论上也需要签名,但原脚本是 GET 方式,为了兼容,我们沿用原脚本的 GET 方式
|
||||
# 原脚本使用 fetch GET 带参数,并未加密。这里也采用 GET 方式,不使用加密 headers
|
||||
resp_analysis = self.fetch(f"{self.host}/api/v1/app/play/analysisMovieUrl?playerUrl={quote(parse_url,safe='')}&playerId={playerid}", headers=self.headers).json()
|
||||
url = resp_analysis.get('data')
|
||||
|
||||
return {'jx': '0', 'parse': '0', 'url': url, 'header': {'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'}}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,480 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 星河影视 xhkan.top - TVBox/PY Spider 兼容修复版
|
||||
# 重点:按 TVBox Py Spider 标准方法返回;分类页无 SSR 列表时自动回退首页分区数据;保留原 /api/player/resolve 播放解析。
|
||||
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import base64
|
||||
import urllib.parse
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
host = 'https://xhkan.top'
|
||||
ua = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
|
||||
|
||||
# xhkan 详情接口里的 cat 是数字;网页分类路径是 slug
|
||||
classes = [
|
||||
{'type_id': '1', 'type_name': '电影'},
|
||||
{'type_id': '2', 'type_name': '电视剧'},
|
||||
{'type_id': '3', 'type_name': '综艺'},
|
||||
{'type_id': '4', 'type_name': '动漫'},
|
||||
{'type_id': '6', 'type_name': '短剧'},
|
||||
]
|
||||
slug_map = {'1': 'movie', '2': 'tv', '3': 'variety', '4': 'anime', '6': 'short-drama'}
|
||||
cat_name_map = {'1': '电影', '2': '电视剧', '3': '综艺', '4': '动漫', '6': '短剧'}
|
||||
source_names = {'qq': '腾讯', 'qiyi': '爱奇艺', 'youku': '优酷', 'mgtv': '芒果', 'bilibili': '哔哩'}
|
||||
sites = ['qq', 'qiyi', 'youku', 'mgtv', 'bilibili']
|
||||
block_words = ('预告', '片花', 'trailer', 'teaser')
|
||||
|
||||
def init(self, extend=''):
|
||||
self.hosts = [self.host]
|
||||
try:
|
||||
if extend:
|
||||
ext = json.loads(extend) if isinstance(extend, str) and extend.strip().startswith('{') else extend
|
||||
site = ''
|
||||
if isinstance(ext, dict):
|
||||
site = ext.get('site') or ext.get('host') or ''
|
||||
elif isinstance(ext, str):
|
||||
site = ext
|
||||
if site:
|
||||
self.hosts = [i.strip().rstrip('/') for i in site.split(',') if i.strip()]
|
||||
self.host = self.hosts[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '星河影视'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
|
||||
# ============ 标准 TVBox 方法 ============
|
||||
def homeContent(self, filter):
|
||||
filters = {}
|
||||
for c in self.classes:
|
||||
filters[c['type_id']] = [
|
||||
{'key': 'sort', 'name': '排序', 'value': [
|
||||
{'n': '最新', 'v': 'time'}, {'n': '热度', 'v': 'hits'}, {'n': '评分', 'v': 'score'}
|
||||
]},
|
||||
{'key': 'type', 'name': '类型', 'value': [
|
||||
{'n': '全部', 'v': ''}, {'n': '喜剧', 'v': '喜剧'}, {'n': '爱情', 'v': '爱情'},
|
||||
{'n': '动作', 'v': '动作'}, {'n': '剧情', 'v': '剧情'}, {'n': '悬疑', 'v': '悬疑'},
|
||||
{'n': '犯罪', 'v': '犯罪'}, {'n': '科幻', 'v': '科幻'}, {'n': '动画', 'v': '动画'},
|
||||
{'n': '其他', 'v': '其他'}
|
||||
]},
|
||||
{'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': [{'n': '全部', 'v': ''}] + [
|
||||
{'n': str(y), 'v': str(y)} for y in range(2026, 2014, -1)
|
||||
]}
|
||||
]
|
||||
return {'class': self.classes, 'filters': filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
html = self._get_first_text(['/'])
|
||||
return {'list': self._parse_cards(html)[:40]}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
tid = str(tid or '1')
|
||||
page = int(pg or 1)
|
||||
extend = extend or {}
|
||||
slug = self.slug_map.get(tid, tid)
|
||||
|
||||
# 1. 优先尝试可能的 JSON API,兼容站点后续改版
|
||||
videos = self._try_category_apis(tid, slug, page, extend)
|
||||
|
||||
# 2. 再尝试网页分类路径
|
||||
if not videos:
|
||||
paths = self._build_category_paths(slug, page, extend)
|
||||
for p in paths:
|
||||
html = self._get_first_text([p])
|
||||
videos = self._parse_cards(html)
|
||||
if videos:
|
||||
break
|
||||
|
||||
# 3. 当前站分类页可能只渲染筛选栏、不直接输出列表,回退首页对应分类热播数据
|
||||
if not videos:
|
||||
html = self._get_first_text(['/'])
|
||||
all_videos = self._parse_cards(html)
|
||||
videos = [v for v in all_videos if str(v.get('vod_id', '')).split('@', 1)[0] == tid]
|
||||
|
||||
return {'list': videos, 'page': page, 'pagecount': page + 1, 'limit': 30, 'total': 999999}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
page = int(pg or 1)
|
||||
kw = str(key or '').strip()
|
||||
if not kw:
|
||||
return {'list': [], 'page': page}
|
||||
|
||||
videos = []
|
||||
# 1. 兼容常见搜索 API / 搜索页参数
|
||||
paths = [
|
||||
'/api/search?keyword=%s&page=%s' % (urllib.parse.quote(kw), page),
|
||||
'/api/search?wd=%s&page=%s' % (urllib.parse.quote(kw), page),
|
||||
'/search?keyword=%s&page=%s' % (urllib.parse.quote(kw), page),
|
||||
'/search?q=%s&page=%s' % (urllib.parse.quote(kw), page),
|
||||
]
|
||||
for p in paths:
|
||||
txt = self._get_first_text([p])
|
||||
if not txt:
|
||||
continue
|
||||
st = txt.strip()
|
||||
if st.startswith('{') or st.startswith('['):
|
||||
try:
|
||||
videos = self._parse_json_list(json.loads(st))
|
||||
except Exception:
|
||||
videos = []
|
||||
else:
|
||||
videos = self._parse_cards(txt)
|
||||
if videos:
|
||||
break
|
||||
|
||||
# 2. 搜索 API 不可用时,至少从首页已渲染资源里本地匹配,保证壳内不空白
|
||||
if not videos:
|
||||
html = self._get_first_text(['/'])
|
||||
videos = [v for v in self._parse_cards(html) if kw.lower() in v.get('vod_name', '').lower()]
|
||||
|
||||
return {'list': videos, 'page': page, 'pagecount': page + 1, 'limit': 30, 'total': 999999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
raw = str(ids[0])
|
||||
cat, vod_id = self._split_vid(raw)
|
||||
if not vod_id:
|
||||
return {'list': []}
|
||||
|
||||
detail = None
|
||||
for site in self.sites:
|
||||
try:
|
||||
api = '/api/detail?cat=%s&id=%s&site=%s' % (cat, urllib.parse.quote(vod_id), site)
|
||||
txt = self._get_first_text([api])
|
||||
data = json.loads(txt)
|
||||
if data.get('errno') == 0 and data.get('data'):
|
||||
detail = data.get('data')
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# API 失败时,解析详情页 HTML 兜底
|
||||
if not detail:
|
||||
html = self._get_first_text(['/detail/%s/%s' % (cat, urllib.parse.quote(vod_id))])
|
||||
return {'list': [self._detail_from_html(cat, vod_id, html)]}
|
||||
|
||||
title = detail.get('title') or detail.get('name') or vod_id
|
||||
if self._blocked_title(title):
|
||||
return {'list': []}
|
||||
|
||||
vod = {
|
||||
'vod_id': '%s@%s' % (cat, vod_id),
|
||||
'vod_name': title,
|
||||
'vod_pic': self._abs_img(detail.get('cover') or detail.get('pic') or detail.get('poster') or ''),
|
||||
'vod_remarks': detail.get('remarks') or detail.get('status') or '',
|
||||
'vod_year': str(detail.get('year') or detail.get('pubdate') or ''),
|
||||
'vod_area': self._join(detail.get('area') or ''),
|
||||
'vod_actor': self._join(detail.get('actors') or detail.get('actor') or ''),
|
||||
'vod_director': self._join(detail.get('directors') or detail.get('director') or ''),
|
||||
'vod_content': self._clean_text(detail.get('desc') or detail.get('intro') or detail.get('description') or ''),
|
||||
}
|
||||
|
||||
play_from, play_urls = [], []
|
||||
allep = detail.get('allepidetail') or {}
|
||||
if isinstance(allep, dict):
|
||||
for site, eps in allep.items():
|
||||
if not isinstance(eps, list) or not eps:
|
||||
continue
|
||||
urls = []
|
||||
for idx, ep in enumerate(eps, 1):
|
||||
ep_no = ep.get('playlink_num') or ep.get('episode') or idx
|
||||
name = ep.get('title') or ('第%s集' % str(ep_no).zfill(2) if str(ep_no).isdigit() else str(ep_no))
|
||||
raw_url = ep.get('url') or ep.get('play_url') or ''
|
||||
if not raw_url:
|
||||
continue
|
||||
pid = self._enc({'cat': cat, 'vod_id': vod_id, 'source': site, 'episode': int(ep_no) if str(ep_no).isdigit() else idx, 'playUrl': raw_url})
|
||||
urls.append('%s$%s' % (name, pid))
|
||||
if urls:
|
||||
play_from.append(self.source_names.get(site, site))
|
||||
play_urls.append('#'.join(urls))
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(play_from)
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
return {'list': [vod]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
obj = self._dec(str(id or ''))
|
||||
if not isinstance(obj, dict):
|
||||
return {'parse': 0, 'url': str(id or ''), 'header': {'User-Agent': self.ua, 'Referer': self.host + '/'}}
|
||||
|
||||
play_url = obj.get('playUrl') or ''
|
||||
payload = {
|
||||
'vodId': obj.get('vod_id') or obj.get('vodId') or 'direct',
|
||||
'source': obj.get('source') or 'qq',
|
||||
'episode': int(obj.get('episode') or 1),
|
||||
'category': int(obj.get('cat') or obj.get('category') or 2),
|
||||
'playUrl': play_url
|
||||
}
|
||||
self._get_first_text(['/api/player/token'])
|
||||
txt = self._post_first_json('/api/player/resolve', payload)
|
||||
data = json.loads(txt) if txt else {}
|
||||
url = data.get('url') if data.get('success') else ''
|
||||
return {'parse': 0, 'url': url or play_url or '', 'header': {'User-Agent': self.ua, 'Referer': self.host + '/'}}
|
||||
except Exception:
|
||||
return {'parse': 0, 'url': ''}
|
||||
|
||||
# ============ 分类 / API 兜底 ============
|
||||
def _build_category_paths(self, slug, page, extend):
|
||||
params = {}
|
||||
for k in ('type', 'area', 'year', 'sort'):
|
||||
if extend.get(k):
|
||||
params[k] = extend.get(k)
|
||||
if page > 1:
|
||||
params['page'] = str(page)
|
||||
q = ('?' + urllib.parse.urlencode(params)) if params else ''
|
||||
base = '/short-drama' if slug == 'short-drama' else '/category/' + slug
|
||||
paths = [base + q]
|
||||
if page > 1:
|
||||
paths.append(base + '/page/%s' % page)
|
||||
paths.append(base + '?page=%s' % page)
|
||||
return paths
|
||||
|
||||
def _try_category_apis(self, tid, slug, page, extend):
|
||||
params = {
|
||||
'cat': tid,
|
||||
'category': tid,
|
||||
'type': extend.get('type', ''),
|
||||
'area': extend.get('area', ''),
|
||||
'year': extend.get('year', ''),
|
||||
'sort': extend.get('sort', ''),
|
||||
'page': str(page),
|
||||
'limit': '30'
|
||||
}
|
||||
q1 = urllib.parse.urlencode(params)
|
||||
q2 = urllib.parse.urlencode(dict(params, cat=slug, category=slug))
|
||||
paths = [
|
||||
'/api/list?' + q1,
|
||||
'/api/vod/list?' + q1,
|
||||
'/api/category?' + q1,
|
||||
'/api/category?' + q2,
|
||||
'/api/videos?' + q1,
|
||||
'/api/search?cat=%s&page=%s' % (urllib.parse.quote(tid), page),
|
||||
'/api/search?category=%s&page=%s' % (urllib.parse.quote(slug), page),
|
||||
]
|
||||
for p in paths:
|
||||
txt = self._get_first_text([p])
|
||||
if not txt:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
videos = self._parse_json_list(obj)
|
||||
if videos:
|
||||
return videos
|
||||
except Exception:
|
||||
continue
|
||||
return []
|
||||
|
||||
# ============ 解析工具 ============
|
||||
def _parse_cards(self, html):
|
||||
html = html or ''
|
||||
videos, seen = [], set()
|
||||
# 适配 /detail/2/xxxx,下一版如果改成完整域名也能匹配
|
||||
pattern = re.compile(r'<a[^>]+href=["\'](?:https?://[^/]+)?/detail/(\d+)/([^"\'#?]+)[^"\']*["\'][^>]*>(.*?)</a>', re.S | re.I)
|
||||
for m in pattern.finditer(html):
|
||||
cat = m.group(1)
|
||||
vid = urllib.parse.unquote(m.group(2))
|
||||
block = m.group(3)
|
||||
title = self._extract_title(block)
|
||||
if not title or self._blocked_title(title):
|
||||
continue
|
||||
pic = self._first_match(block, r'(?:data-src|src)=["\']([^"\']+)["\']') or ''
|
||||
remark = self._first_match(block, r'(全\d+集|更新至\d+集|\d{4}-\d{2}-\d{2}期|\d+期|正片)') or ''
|
||||
vod_id = '%s@%s' % (cat, vid)
|
||||
if vod_id in seen:
|
||||
continue
|
||||
seen.add(vod_id)
|
||||
videos.append({'vod_id': vod_id, 'vod_name': title, 'vod_pic': self._abs_img(pic), 'vod_remarks': remark})
|
||||
return videos
|
||||
|
||||
def _parse_json_list(self, obj):
|
||||
arr = []
|
||||
if isinstance(obj, dict):
|
||||
data = obj.get('data', obj)
|
||||
if isinstance(data, dict):
|
||||
for k in ('list', 'items', 'records', 'result', 'data'):
|
||||
if isinstance(data.get(k), list):
|
||||
arr = data.get(k)
|
||||
break
|
||||
elif isinstance(data, list):
|
||||
arr = data
|
||||
if not arr:
|
||||
for k in ('list', 'items', 'records', 'result'):
|
||||
if isinstance(obj.get(k), list):
|
||||
arr = obj.get(k)
|
||||
break
|
||||
elif isinstance(obj, list):
|
||||
arr = obj
|
||||
videos = []
|
||||
for it in arr or []:
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
title = it.get('title') or it.get('name') or it.get('vod_name') or it.get('videoName') or ''
|
||||
if not title or self._blocked_title(title):
|
||||
continue
|
||||
cat = str(it.get('cat') or it.get('category') or it.get('type') or it.get('type_id') or it.get('cid') or '2')
|
||||
if cat in self.slug_map:
|
||||
pass
|
||||
else:
|
||||
# slug 转数字
|
||||
for k, v in self.slug_map.items():
|
||||
if str(cat) == v:
|
||||
cat = k
|
||||
break
|
||||
vid = str(it.get('id') or it.get('vod_id') or it.get('vid') or it.get('episode_id') or '')
|
||||
if not vid:
|
||||
continue
|
||||
videos.append({
|
||||
'vod_id': '%s@%s' % (cat, vid),
|
||||
'vod_name': self._clean_text(title),
|
||||
'vod_pic': self._abs_img(it.get('cover') or it.get('pic') or it.get('poster') or it.get('vod_pic') or ''),
|
||||
'vod_remarks': it.get('remarks') or it.get('status') or it.get('vod_remarks') or ''
|
||||
})
|
||||
return videos
|
||||
|
||||
def _detail_from_html(self, cat, vod_id, html):
|
||||
title = self._first_match(html, r'<h1[^>]*>(.*?)</h1>') or vod_id
|
||||
title = self._clean_text(title)
|
||||
pic = self._first_match(html, r'<img[^>]+alt=["\']%s["\'][^>]+(?:src|data-src)=["\']([^"\']+)' % re.escape(title)) or self._first_match(html, r'<img[^>]+(?:src|data-src)=["\']([^"\']+)["\']')
|
||||
content = self._first_match(html, r'###?\s*简介\s*(.*?)\s*(?:展开全部|选集|</)')
|
||||
# 播放集数从详情页 /play/cat/id/ep?s=source 提取
|
||||
eps = []
|
||||
ep_re = re.compile(r'href=["\'](?:https?://[^/]+)?/play/(\d+)/([^/"\']+)/(\d+)\?s=([^"\'&]+)[^"\']*["\'][^>]*>(.*?)</a>', re.S | re.I)
|
||||
for mm in ep_re.finditer(html or ''):
|
||||
c, vid, ep, src, name = mm.group(1), urllib.parse.unquote(mm.group(2)), mm.group(3), mm.group(4), self._clean_text(mm.group(5))
|
||||
if c != str(cat) or vid != str(vod_id):
|
||||
continue
|
||||
pid = self._enc({'cat': c, 'vod_id': vid, 'source': src, 'episode': int(ep), 'playUrl': ''})
|
||||
eps.append('%s$%s' % (name or ('第%s集' % ep), pid))
|
||||
vod = {
|
||||
'vod_id': '%s@%s' % (cat, vod_id),
|
||||
'vod_name': title,
|
||||
'vod_pic': self._abs_img(pic),
|
||||
'vod_content': self._clean_text(content),
|
||||
'vod_play_from': '星河',
|
||||
'vod_play_url': '#'.join(eps)
|
||||
}
|
||||
return vod
|
||||
|
||||
def _extract_title(self, block):
|
||||
title = self._first_match(block, r'alt=["\']([^"\']+)["\']') or self._first_match(block, r'title=["\']([^"\']+)["\']')
|
||||
if title:
|
||||
return self._clean_text(title)
|
||||
text = self._clean_text(block)
|
||||
# 首页卡片常见顺序:简介 + 状态 + 类型 + 标题,取最后一个较短片段
|
||||
parts = re.split(r'(?:全\d+集|更新至\d+集|\d{4}-\d{2}-\d{2}期|\d+期|正片|其他|剧情|喜剧|爱情|动作|悬疑|犯罪|动画|原创|都市|网剧|少儿)', text)
|
||||
cand = parts[-1].strip() if parts else text
|
||||
if not cand or len(cand) > 40:
|
||||
cand = text[-40:].strip()
|
||||
return cand
|
||||
|
||||
def _blocked_title(self, title):
|
||||
t = (title or '').lower()
|
||||
return any(w.lower() in t for w in self.block_words)
|
||||
|
||||
def _split_vid(self, raw):
|
||||
raw = str(raw)
|
||||
if '@' in raw:
|
||||
return raw.split('@', 1)
|
||||
m = re.search(r'/detail/(\d+)/([^/?#]+)', raw)
|
||||
if m:
|
||||
return m.group(1), urllib.parse.unquote(m.group(2))
|
||||
return '2', raw
|
||||
|
||||
def _headers(self):
|
||||
return {
|
||||
'User-Agent': self.ua,
|
||||
'Accept': 'application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Referer': self.host + '/',
|
||||
}
|
||||
|
||||
def _get_first_text(self, paths, timeout=20):
|
||||
for base in getattr(self, 'hosts', [self.host]):
|
||||
base = base.rstrip('/')
|
||||
for p in paths:
|
||||
url = p if str(p).startswith('http') else base + (p if str(p).startswith('/') else '/' + str(p))
|
||||
try:
|
||||
r = self.fetch(url, headers=self._headers(), timeout=timeout, verify=False)
|
||||
if hasattr(r, 'text'):
|
||||
txt = r.text
|
||||
else:
|
||||
content = getattr(r, 'content', b'')
|
||||
txt = content.decode('utf-8', 'ignore') if isinstance(content, bytes) else str(content)
|
||||
if txt:
|
||||
return txt
|
||||
except Exception:
|
||||
continue
|
||||
return ''
|
||||
|
||||
def _post_first_json(self, path, payload, timeout=20):
|
||||
for base in getattr(self, 'hosts', [self.host]):
|
||||
url = base.rstrip('/') + path
|
||||
try:
|
||||
r = self.fetch(url, headers={**self._headers(), 'Content-Type': 'application/json'}, data=json.dumps(payload).encode('utf-8'), method='POST', timeout=timeout, verify=False)
|
||||
if hasattr(r, 'text'):
|
||||
return r.text
|
||||
content = getattr(r, 'content', b'')
|
||||
return content.decode('utf-8', 'ignore') if isinstance(content, bytes) else str(content)
|
||||
except Exception:
|
||||
continue
|
||||
return ''
|
||||
|
||||
def _enc(self, obj):
|
||||
return base64.urlsafe_b64encode(json.dumps(obj, ensure_ascii=False, separators=(',', ':')).encode('utf-8')).decode('utf-8').rstrip('=')
|
||||
|
||||
def _dec(self, s):
|
||||
try:
|
||||
return json.loads(base64.urlsafe_b64decode((s + '=' * (-len(s) % 4)).encode()).decode('utf-8'))
|
||||
except Exception:
|
||||
return s
|
||||
|
||||
def _abs_img(self, url):
|
||||
url = str(url or '').strip()
|
||||
if not url:
|
||||
return ''
|
||||
if url.startswith('//'):
|
||||
return 'https:' + url
|
||||
if url.startswith('/'):
|
||||
return self.host.rstrip('/') + url
|
||||
return url
|
||||
|
||||
def _clean_text(self, text):
|
||||
text = re.sub(r'<script[\s\S]*?</script>|<style[\s\S]*?</style>', ' ', str(text or ''), flags=re.I)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
for a, b in {' ': ' ', '&': '&', '"': '"', ''': "'", '<': '<', '>': '>'}.items():
|
||||
text = text.replace(a, b)
|
||||
return re.sub(r'\s+', ' ', text).strip()
|
||||
|
||||
def _first_match(self, text, pattern):
|
||||
m = re.search(pattern, text or '', re.S | re.I)
|
||||
return self._clean_text(m.group(1)) if m else ''
|
||||
|
||||
def _join(self, v):
|
||||
if isinstance(v, list):
|
||||
return '/'.join([str(x.get('name') if isinstance(x, dict) else x) for x in v])
|
||||
return str(v or '')
|
||||
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#神秘
|
||||
import re
|
||||
import urllib.parse
|
||||
from base.spider import Spider as BaseSpile
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class VideoDecryptor:
|
||||
"""XOR 128 解密"""
|
||||
@staticmethod
|
||||
def decrypt(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
try:
|
||||
return ''.join(chr(128 ^ ord(c)) for c in text)
|
||||
except:
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def from_js(js: str) -> str:
|
||||
return VideoDecryptor.decrypt(m.group(1)) if (m := re.search(r"document\.write\(l\('([^']+)'\)\)", js)) else ""
|
||||
|
||||
|
||||
class Spider(BaseSpile):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://h4ivs.sm431.vip"
|
||||
self.video_host = "https://38.je:38"
|
||||
self.image_host = "https://38.je:36"
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 13; 22127RK46C Build/TKQ1.220905.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/104.0.5112.97 Mobile Safari/537.36",
|
||||
"Referer": self.host,
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
}
|
||||
self.cache = {}
|
||||
|
||||
def get(self, url):
|
||||
try:
|
||||
r = requests.get(url, headers=self.headers, timeout=15)
|
||||
r.raise_for_status()
|
||||
r.encoding = "utf-8"
|
||||
return r.text
|
||||
except:
|
||||
return "神秘电影"
|
||||
|
||||
def img_url(self, url):
|
||||
"""格式化图片URL"""
|
||||
if not url:
|
||||
return ""
|
||||
if url.startswith("//"):
|
||||
url = "https:" + url
|
||||
elif url.startswith("/"):
|
||||
url = self.image_host + url
|
||||
return f"{url}@User-Agent={self.headers['User-Agent']}@Referer={self.host}/"
|
||||
|
||||
def parse(self, el):
|
||||
"""解析卡片"""
|
||||
a = el if el.name == 'a' else el.find('a')
|
||||
if not a or not (href := a.get("href", "")):
|
||||
return None
|
||||
|
||||
href = self.host + href if href.startswith("/") else href
|
||||
if not (vid := re.search(r"/vid/(\d+)", href)):
|
||||
return None
|
||||
|
||||
vid = vid.group(1)
|
||||
title = ""
|
||||
|
||||
# 解密标题
|
||||
if p := el.find('p'):
|
||||
if s := p.find('script'):
|
||||
if s.string:
|
||||
title = VideoDecryptor.from_js(s.string)
|
||||
title = title or p.get_text(strip=True)
|
||||
|
||||
if not title:
|
||||
for attr in ['data-title', 'data-name', 'title']:
|
||||
if el.has_attr(attr) and (val := el[attr]):
|
||||
if (de := VideoDecryptor.decrypt(val)) and len(de) > 3:
|
||||
title = de
|
||||
break
|
||||
|
||||
title = title or "未知标题"
|
||||
if title != "未知标题":
|
||||
self.cache[vid] = title
|
||||
|
||||
# 图片
|
||||
img = ""
|
||||
if node := el.select_one("img"):
|
||||
img = node.get("data-src") or node.get("src") or ""
|
||||
img = img or f"{self.image_host}/{vid}.jpg"
|
||||
|
||||
return {
|
||||
"vod_id": vid,
|
||||
"vod_name": title,
|
||||
"vod_pic": self.img_url(img),
|
||||
"vod_remarks": "",
|
||||
}
|
||||
|
||||
def get_title(self, vid):
|
||||
"""从缓存或首页获取标题"""
|
||||
if vid in self.cache:
|
||||
return self.cache[vid]
|
||||
|
||||
if html := self.get(self.host):
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
for link in soup.select('a[href*="/vid/"]'):
|
||||
if f'/vid/{vid}' in link.get('href', ''):
|
||||
if p := link.find('p'):
|
||||
if s := p.find('script'):
|
||||
if s.string and (t := VideoDecryptor.from_js(s.string)):
|
||||
self.cache[vid] = t
|
||||
return t
|
||||
if t := p.get_text(strip=True):
|
||||
self.cache[vid] = t
|
||||
return t
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
"class": [
|
||||
{"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"},
|
||||
]
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
if not (html := self.get(self.host)):
|
||||
return {"list": []}
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
videos = [v for v in (self.parse(el) for el in soup.select(".vodbox, .stui-vodlist__box, .vodlist__box, .video-card, .item")) if v]
|
||||
|
||||
if not videos:
|
||||
videos = [{"vod_id": v, "vod_name": "未知标题", "vod_pic": self.img_url(f"{self.image_host}/{v}.jpg"), "vod_remarks": ""}
|
||||
for v in re.findall(r'\[]\(/vid/(\d+)\.html\)', html)]
|
||||
|
||||
return {"list": videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid == "0":
|
||||
url = self.host if int(pg) == 1 else f"{self.host}/page/{pg}.html"
|
||||
else:
|
||||
url = f"{self.host}/list/{tid}.html" if int(pg) == 1 else f"{self.host}/list/{tid}/{pg}.html"
|
||||
|
||||
if not (html := self.get(url)):
|
||||
return {"list": [], "page": pg, "pagecount": 1, "limit": 30, "total": 0}
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
videos = [v for v in (self.parse(el) for el in soup.select(".vodbox, .stui-vodlist__box, .vodlist__box, .video-card, .item")) if v]
|
||||
|
||||
if not videos:
|
||||
videos = [{"vod_id": v, "vod_name": "未知标题", "vod_pic": self.img_url(f"{self.image_host}/{v}.jpg"), "vod_remarks": ""}
|
||||
for v in re.findall(r'\[]\(/vid/(\d+)\.html\)', html)]
|
||||
|
||||
last = max([int(m.group(1)) for a in soup.select("a[href*='list/']") if (m := re.search(r"/list/\d+/(\d+)\.html", a.get("href", "")))], default=int(pg))
|
||||
|
||||
return {"list": videos, "page": pg, "pagecount": max(last, 1), "limit": 30, "total": 99999}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f"{self.host}/so.html"
|
||||
params = {"wd": key}
|
||||
if int(pg) > 1:
|
||||
params["page"] = pg
|
||||
|
||||
html = ""
|
||||
for method in [requests.get, requests.post]:
|
||||
try:
|
||||
r = method(url, params=params if method == requests.get else None,
|
||||
data=params if method == requests.post else None,
|
||||
headers=self.headers, timeout=15)
|
||||
r.raise_for_status()
|
||||
r.encoding = "utf-8"
|
||||
html = r.text
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not html:
|
||||
return {"list": []}
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
videos = [v for v in (self.parse(el) for el in soup.select(".vodbox, .stui-vodlist__box, .vodlist__box, .video-card, .item")) if v]
|
||||
|
||||
if not videos:
|
||||
videos = [{"vod_id": v, "vod_name": "未知标题", "vod_pic": self.img_url(f"{self.image_host}/{v}.jpg"), "vod_remarks": ""}
|
||||
for v in re.findall(r'\[]\(/vid/(\d+)\.html\)', html)]
|
||||
|
||||
last = max([int(m.group(1)) for a in soup.select("a[href*='so.html'], .pagination a, .page-link")
|
||||
if (m := re.search(r"[?&]page=(\d+)", a.get("href", "")))], default=int(pg))
|
||||
|
||||
return {"list": videos, "page": pg, "pagecount": max(last, 1), "limit": 30, "total": 99999}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]
|
||||
if not (html := self.get(f"{self.host}/vid/{vid}.html")):
|
||||
return {"list": []}
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
|
||||
# 标题
|
||||
title = self.get_title(vid)
|
||||
if not title:
|
||||
if t := soup.find('title'):
|
||||
title = re.sub(r'\s*[-_|]\s*.{0,20}$', '', t.get_text(strip=True)).strip()
|
||||
|
||||
if not title or len(title) < 5:
|
||||
for sel in ['h1', 'h2', '.video-title', '.title']:
|
||||
if (el := soup.select_one(sel)) and (txt := el.get_text(strip=True)) and len(txt) > 5:
|
||||
title = txt
|
||||
break
|
||||
|
||||
title = title or f"视频{vid}"
|
||||
|
||||
# 图片
|
||||
pic = ""
|
||||
for sel in ['.picbox img', '.vodimg img', '.video-pic img', '.poster img', 'img[data-id]']:
|
||||
if (node := soup.select_one(sel)) and (p := node.get("data-src") or node.get("src")) and 'favicon' not in p.lower():
|
||||
pic = p
|
||||
break
|
||||
|
||||
if not pic or 'favicon' in pic.lower():
|
||||
if meta := soup.select_one('meta[property="og:image"]'):
|
||||
pic = meta.get('content', '')
|
||||
|
||||
pic = pic or f"{self.image_host}/{vid}.jpg"
|
||||
|
||||
# 简介
|
||||
desc = soup.select_one(".vodinfo, .video-info, .content, .intro, .description")
|
||||
desc = desc.get_text(strip=True) if desc else ""
|
||||
|
||||
return {"list": [{
|
||||
"vod_id": vid,
|
||||
"vod_name": title,
|
||||
"vod_pic": self.img_url(pic),
|
||||
"vod_content": desc,
|
||||
"vod_play_from": "神秘线路",
|
||||
"vod_play_url": f"全集${vid}@@0@@1",
|
||||
}]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
vid = id.split("@@")[0]
|
||||
return {"parse": 0, "url": f"{self.video_host}/{vid}/hls/index.m3u8", "header": self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
return {"code": 404, "content": ""}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return ".m3u8" in url.lower()
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,343 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.cd-zj.com'
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://www.cd-zj.com/'
|
||||
})
|
||||
self.cateManual = {
|
||||
'\u7535\u5f71': '1',
|
||||
'\u7535\u89c6\u5267': '2',
|
||||
'\u7efc\u827a': '3',
|
||||
'\u52a8\u6f2b': '4',
|
||||
'\u70ed\u95e8\u77ed\u5267': '5',
|
||||
'\u817e\u8bafSVIP': 'label/qq',
|
||||
'\u4f18\u9177SVIP': 'label/youku',
|
||||
'B\u7ad9SVIP': 'label/bli',
|
||||
}
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "\u67ab\u53f64K\u5907\u7528"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('\u3000', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15)
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def getVid(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
m = re.search(r'/detail/(\d+)\.html', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'/play/(\d+)-', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
html = self._get(self.site)
|
||||
seen = set()
|
||||
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': videos, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
if str(tid).startswith('label/'):
|
||||
if page == 1:
|
||||
url = f'{self.site}/{tid}.html'
|
||||
else:
|
||||
url = f'{self.site}/{tid}-{page}.html'
|
||||
else:
|
||||
if page == 1:
|
||||
url = f'{self.site}/type/{tid}.html'
|
||||
else:
|
||||
url = f'{self.site}/type/{tid}-{page}.html'
|
||||
|
||||
html = self._get(url)
|
||||
seen = set()
|
||||
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ids[0] if ids else ''
|
||||
if not vid:
|
||||
return result
|
||||
try:
|
||||
html = self._get(f'{self.site}/detail/{vid}.html')
|
||||
|
||||
title = ''
|
||||
tm = re.search(r'<title>\u300a(.+?)\u300b', html)
|
||||
if tm:
|
||||
title = tm.group(1)
|
||||
if not title:
|
||||
tm = re.search(r'<title>([^<]+)', html)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
|
||||
pic = ''
|
||||
pm = re.search(r'lazy1[^>]*data-src="([^"]+)"', html)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
|
||||
desc = ''
|
||||
dm = re.search(r'<meta name="description" content="(.+?)"', html)
|
||||
if dm:
|
||||
desc = dm.group(1).replace('\u5267\u60c5\u4ecb\u7ecd\uff1a', '').strip()
|
||||
|
||||
actor = ''
|
||||
director = ''
|
||||
info = re.search(r'slide-info(.*?)(?:anthology|swiper)', html, re.DOTALL)
|
||||
if info:
|
||||
block = info.group(1)
|
||||
am = re.search(r'\u4e3b\u6f14[:\uff1a]\s*([^\n<]+)', block)
|
||||
if am:
|
||||
actor = am.group(1).strip()
|
||||
dm2 = re.search(r'\u5bfc\u6f14[:\uff1a]\s*([^\n<]+)', block)
|
||||
if dm2:
|
||||
director = dm2.group(1).strip()
|
||||
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# \u627e anthology-tab \u533a\u5757\u5185\u7684\u6240\u6709 <a class="swiper-slide">
|
||||
tab_block = re.search(r'class="anthology-tab[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
||||
if tab_block:
|
||||
tabs = re.findall(r'<a[^>]*class="swiper-slide"[^>]*>(.*?)</a>', tab_block.group(1), re.DOTALL)
|
||||
else:
|
||||
tabs = []
|
||||
|
||||
# \u627e\u6240\u6709 anthology-list-box \u533a\u5757
|
||||
panels = re.findall(r'class="anthology-list-box[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
||||
|
||||
for i, tab in enumerate(tabs):
|
||||
tab_name = self._clean(tab) or f'\u7ebf\u8def{i+1}'
|
||||
play_from.append(tab_name)
|
||||
episodes = []
|
||||
if i < len(panels):
|
||||
for em in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>([^<]+)<', panels[i]):
|
||||
ep_href = em.group(1)
|
||||
ep_name = em.group(2).strip()
|
||||
if ep_name and ep_href:
|
||||
episodes.append(f'{ep_name}${ep_href}')
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director if director else bytes.fromhex('e6989fe6b2b3').decode('utf-8'),
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from) if play_from else '',
|
||||
'vod_play_url': '$$$'.join(play_url) if play_url else ''
|
||||
}
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
play_url = id
|
||||
if id and not id.startswith('http'):
|
||||
play_url = self.site + id
|
||||
|
||||
html = self._get(play_url)
|
||||
|
||||
# \u4f18\u5148\u4ece player_aaaa JSON \u63d0\u53d6 m3u8 \u76f4\u94fe
|
||||
m = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*<', html)
|
||||
if m:
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
m3u8 = data.get('url', '')
|
||||
if m3u8 and '.m3u8' in m3u8:
|
||||
result['parse'] = 0
|
||||
result['url'] = m3u8
|
||||
result['jx'] = 0
|
||||
result['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.site + '/'
|
||||
}
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
# \u5907\u7528: \u4ece\u9875\u9762\u4e2d\u627e m3u8 \u94fe\u63a5
|
||||
m = re.search(r'url":\s*"(https?://[^"]*\.m3u8[^"]*)"', html)
|
||||
if m:
|
||||
result['parse'] = 0
|
||||
result['url'] = m.group(1).replace('\\/', '/')
|
||||
result['jx'] = 0
|
||||
result['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.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
# \u5907\u7528: iframe
|
||||
m = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
if m:
|
||||
result['parse'] = 1
|
||||
result['url'] = m.group(1)
|
||||
result['jx'] = 0
|
||||
result['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.site + '/'
|
||||
}
|
||||
else:
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['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.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
result['parse'] = 1
|
||||
result['url'] = id
|
||||
result['jx'] = 0
|
||||
result['header'] = {}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
url = f'{self.site}/cupfox-search/-------------.html'
|
||||
params = {'wd': key}
|
||||
if page > 1:
|
||||
params['page'] = page
|
||||
|
||||
html = self._get(url)
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="(/detail/\d+\.html)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,283 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import re
|
||||
import requests
|
||||
from lxml import etree
|
||||
import base64
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "韩国色情电影"
|
||||
|
||||
def init(self, extend):
|
||||
print("=============韩国色情电影初始化===========")
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"最新视频": "latest",
|
||||
"最长的视频": "longest",
|
||||
"随机视频": "random"
|
||||
}
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
|
||||
result['class'] = classes
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {}
|
||||
videos = self.getVideos('https://koreanpornmovie.com/', 1)
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
|
||||
# 将 pg 转换为整数
|
||||
try:
|
||||
page_num = int(pg)
|
||||
except (ValueError, TypeError):
|
||||
page_num = 1
|
||||
|
||||
url = 'https://koreanpornmovie.com/'
|
||||
if tid == 'longest':
|
||||
url = 'https://koreanpornmovie.com/?filter=longest'
|
||||
elif tid == 'random':
|
||||
url = 'https://koreanpornmovie.com/?filter=random'
|
||||
|
||||
if page_num > 1:
|
||||
url = url + 'page/{0}/'.format(page_num)
|
||||
|
||||
videos = self.getVideos(url, page_num)
|
||||
result['list'] = videos
|
||||
result['page'] = page_num
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, array):
|
||||
tid = array[0]
|
||||
url = 'https://koreanpornmovie.com/{0}'.format(tid)
|
||||
rsp = self.fetch(url)
|
||||
html = rsp.text
|
||||
|
||||
# 获取视频信息
|
||||
video = self.getDetail(html, url)
|
||||
|
||||
# 获取播放地址
|
||||
play_url = self.getPlayUrl(html, url)
|
||||
|
||||
# 构建播放列表
|
||||
playFrom = ['韩国色情电影']
|
||||
playList = [play_url] if play_url else []
|
||||
|
||||
result = {
|
||||
'list': [
|
||||
{
|
||||
'vod_id': tid,
|
||||
'vod_name': video['title'],
|
||||
'vod_pic': video['pic'],
|
||||
'type_name': video['type'],
|
||||
'vod_year': video['year'],
|
||||
'vod_area': "韩国",
|
||||
'vod_remarks': video['remarks'],
|
||||
'vod_actor': video['actor'],
|
||||
'vod_director': video['director'],
|
||||
'vod_content': video['content'],
|
||||
'vod_play_from': '$$$'.join(playFrom),
|
||||
'vod_play_url': '$$$'.join(playList)
|
||||
}
|
||||
]
|
||||
}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
result = {}
|
||||
url = 'https://koreanpornmovie.com/?s={0}'.format(urllib.parse.quote(key))
|
||||
videos = self.getVideos(url, 1)
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = id
|
||||
result["header"] = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36",
|
||||
"Referer": "https://koreanpornmovie.com/"
|
||||
}
|
||||
return result
|
||||
|
||||
def getVideos(self, url, pg):
|
||||
videos = []
|
||||
try:
|
||||
rsp = self.fetch(url)
|
||||
html = rsp.text
|
||||
root = etree.HTML(html)
|
||||
|
||||
# 解析视频列表
|
||||
video_list = root.xpath('//article[contains(@class, "thumb-block")]')
|
||||
for item in video_list:
|
||||
try:
|
||||
# 获取视频链接
|
||||
link = item.xpath('.//a/@href')[0]
|
||||
vid = link.split('/')[-2] if link.endswith('/') else link.split('/')[-1]
|
||||
|
||||
# 获取缩略图
|
||||
img = item.xpath('.//img[@class="video-main-thumb"]/@src')[0]
|
||||
|
||||
# 获取标题
|
||||
title = item.xpath('.//header[@class="entry-header"]/span/text()')[0].strip()
|
||||
|
||||
# 获取时长
|
||||
duration = item.xpath('.//span[@class="duration"]/text()')
|
||||
remarks = duration[0].strip() if duration else ''
|
||||
|
||||
videos.append({
|
||||
"vod_id": vid,
|
||||
"vod_name": title,
|
||||
"vod_pic": img,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"解析视频项时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"获取视频列表时出错: {e}")
|
||||
|
||||
return videos
|
||||
|
||||
def getDetail(self, html, url):
|
||||
root = etree.HTML(html)
|
||||
detail = {
|
||||
'title': '',
|
||||
'pic': '',
|
||||
'type': '韩国情色',
|
||||
'year': '',
|
||||
'actor': '',
|
||||
'director': '',
|
||||
'content': '',
|
||||
'remarks': ''
|
||||
}
|
||||
|
||||
try:
|
||||
# 标题
|
||||
title_elem = root.xpath('//h1[@class="entry-title"]/text()')
|
||||
if title_elem:
|
||||
detail['title'] = title_elem[0].strip()
|
||||
|
||||
# 缩略图
|
||||
pic_elem = root.xpath('//meta[@property="og:image"]/@content')
|
||||
if pic_elem:
|
||||
detail['pic'] = pic_elem[0]
|
||||
|
||||
# 演员信息
|
||||
actors = root.xpath('//div[@id="video-actors"]//a/text()')
|
||||
if actors:
|
||||
detail['actor'] = ' / '.join(actors)
|
||||
|
||||
# 内容描述
|
||||
content_elem = root.xpath('//div[@class="video-description"]//p/text()')
|
||||
if content_elem:
|
||||
detail['content'] = content_elem[0].strip()
|
||||
|
||||
# 时长
|
||||
duration_elem = root.xpath('//span[@class="duration"]/text()')
|
||||
if duration_elem:
|
||||
detail['remarks'] = duration_elem[0].strip()
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取详情时出错: {e}")
|
||||
|
||||
return detail
|
||||
|
||||
def getPlayUrl(self, html, url):
|
||||
play_url = ''
|
||||
|
||||
# 方法1: 从meta标签中提取contentURL
|
||||
meta_pattern = r'<meta\s+itemprop="contentURL"\s+content="([^"]+)"'
|
||||
meta_match = re.search(meta_pattern, html)
|
||||
if meta_match:
|
||||
play_url = meta_match.group(1)
|
||||
print(f"从meta标签找到播放链接: {play_url}")
|
||||
return play_url
|
||||
|
||||
# 方法2: 从iframe的src中提取base64编码的视频链接
|
||||
if not play_url:
|
||||
iframe_pattern = r'<iframe[^>]+src="[^"]*\?q=([^"]+)"[^>]*>'
|
||||
iframe_match = re.search(iframe_pattern, html)
|
||||
if iframe_match:
|
||||
base64_str = iframe_match.group(1)
|
||||
try:
|
||||
decoded = base64.b64decode(base64_str).decode('utf-8')
|
||||
# 从解码后的内容中提取mp4链接
|
||||
mp4_pattern = r'src="([^"]+\.mp4)"'
|
||||
mp4_match = re.search(mp4_pattern, decoded)
|
||||
if mp4_match:
|
||||
play_url = mp4_match.group(1)
|
||||
print(f"从iframe解码找到播放链接: {play_url}")
|
||||
except Exception as e:
|
||||
print(f"解码base64时出错: {e}")
|
||||
|
||||
# 方法3: 直接搜索mp4链接
|
||||
if not play_url:
|
||||
mp4_pattern = r'https?://[^\s"\']+\.mp4'
|
||||
mp4_matches = re.findall(mp4_pattern, html)
|
||||
if mp4_matches:
|
||||
# 优先选择koreanporn.stream域名的链接
|
||||
for mp4_url in mp4_matches:
|
||||
if 'koreanporn.stream' in mp4_url:
|
||||
play_url = mp4_url
|
||||
break
|
||||
if not play_url and mp4_matches:
|
||||
play_url = mp4_matches[0]
|
||||
print(f"直接搜索找到播放链接: {play_url}")
|
||||
|
||||
# 方法4: 从JavaScript变量中提取
|
||||
if not play_url:
|
||||
js_patterns = [
|
||||
r'file\s*:\s*["\']([^"\']+\.mp4)["\']',
|
||||
r'src\s*:\s*["\']([^"\']+\.mp4)["\']',
|
||||
r'videoSrc\s*:\s*["\']([^"\']+\.mp4)["\']'
|
||||
]
|
||||
for pattern in js_patterns:
|
||||
js_match = re.search(pattern, html)
|
||||
if js_match:
|
||||
play_url = js_match.group(1)
|
||||
print(f"从JS变量找到播放链接: {play_url}")
|
||||
break
|
||||
|
||||
# 如果找到播放链接,确保是完整的URL
|
||||
if play_url and not play_url.startswith('http'):
|
||||
if play_url.startswith('//'):
|
||||
play_url = 'https:' + play_url
|
||||
else:
|
||||
# 尝试从当前页面URL构造完整URL
|
||||
from urllib.parse import urljoin
|
||||
play_url = urljoin(url, play_url)
|
||||
|
||||
return play_url
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.mp4', '.m3u8', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
return any(format in url.lower() for format in video_formats)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def localProxy(self, param):
|
||||
action = {}
|
||||
return []
|
||||
@@ -0,0 +1,283 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import html as html_module
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.jxuma.com'
|
||||
self.session = requests.Session()
|
||||
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
self.session.headers.update({'User-Agent': self.ua})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'电视剧': '2',
|
||||
'综艺': '3',
|
||||
'动漫': '4',
|
||||
'短剧': '36',
|
||||
}
|
||||
self._m = chr(0x661f) + chr(0x6cb3)
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = html_module.unescape(text)
|
||||
text = text.replace('\xa0', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '麻花影视'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def _extract_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="/umo/(\d+)\.html"[^>]*?title="([^"]*)"', html):
|
||||
vid = m.group(1)
|
||||
title = m.group(2).strip()
|
||||
if vid in seen or not title:
|
||||
continue
|
||||
snippet = html[m.start():m.start()+400]
|
||||
pm = re.search(r'data-original="([^"]*)"', snippet)
|
||||
pic = pm.group(1).strip() if pm else ''
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
note = ''
|
||||
nm = re.search(r'pic-text text-right">([^<]*)', snippet)
|
||||
if nm:
|
||||
note = nm.group(1).strip()
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
html = self._get(self.site)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
url = f'{self.site}/jxk/{tid}.html'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if result['list'] else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ''
|
||||
if isinstance(ids, list):
|
||||
vid = ids[0] if ids else ''
|
||||
elif ids:
|
||||
vid = str(ids)
|
||||
if not vid:
|
||||
return result
|
||||
# 用播放页第一集来获取 player_aaaa 数据
|
||||
html = self._get(f'{self.site}/aey/{vid}/1-1.html')
|
||||
if not html:
|
||||
return result
|
||||
|
||||
# 提取 player_aaaa
|
||||
pd = {}
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 从详情页获取更多信息
|
||||
detail_html = self._get(f'{self.site}/umo/{vid}.html')
|
||||
|
||||
# 标题
|
||||
title = pd.get('vod_data', {}).get('vod_name', '')
|
||||
if not title:
|
||||
m2 = re.search(r'<h1[^>]*class="title"[^>]*>([^<]*)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(m2.group(1))
|
||||
if not title:
|
||||
m2 = re.search(r'<title>([^<]+)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
|
||||
|
||||
# 封面
|
||||
pic = ''
|
||||
m2 = re.search(r'data-original="([^"]+)"[^>]*class="[^"]*cover[^"]*"', detail_html)
|
||||
if not m2:
|
||||
m2 = re.search(r'data-original="([^"]+)"[^>]*rel="nofollow"', detail_html)
|
||||
if m2:
|
||||
pic = m2.group(1).strip()
|
||||
|
||||
# 类型、地区、年份、语言、主演、导演、简介
|
||||
def extract_info(pattern, text):
|
||||
m3 = re.search(pattern, text)
|
||||
if m3:
|
||||
return self._clean(m3.group(1))
|
||||
return ''
|
||||
|
||||
vod_class = extract_info(r'类型:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
area = extract_info(r'地区:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
year = extract_info(r'年份:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
lang = extract_info(r'语言:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
actor = extract_info(r'主演:</span>(.*?)(?:</p>|</div>)', detail_html)
|
||||
if not actor:
|
||||
actor = pd.get('vod_data', {}).get('vod_actor', '')
|
||||
director = extract_info(r'导演:</span>(.*?)(?:</p>|</div>)', detail_html)
|
||||
if not director:
|
||||
director = pd.get('vod_data', {}).get('vod_director', '')
|
||||
if director:
|
||||
director = self._m + '、' + director
|
||||
else:
|
||||
director = self._m
|
||||
|
||||
desc = extract_info(r'detail-sketch">(.*?)</span>', detail_html)
|
||||
|
||||
# 播放列表 - 从详情页提取所有线路和集数
|
||||
play_from = []
|
||||
play_url_list = []
|
||||
|
||||
# 提取线路名(在 playlist data-toggle="tab" 里)
|
||||
line_names = re.findall(r'playlist\d+" data-toggle="tab"[^>]*rel="nofollow">([^<]+)<', detail_html)
|
||||
# 如果没找到,尝试提取 pannel__head 里的文字
|
||||
if not line_names:
|
||||
line_names = re.findall(r'pannel__head[^>]*>([^<]*)<', detail_html)
|
||||
|
||||
# 提取每个播放面板的链接
|
||||
link_groups = re.findall(r'tab-pane fade[^>]*>(.*?)</ul>', detail_html, re.DOTALL)
|
||||
|
||||
for i, group_html in enumerate(link_groups):
|
||||
line_name = line_names[i] if i < len(line_names) else f'线路{i+1}'
|
||||
line_name = self._clean(line_name)
|
||||
episodes = []
|
||||
for em in re.finditer(r'href="(/aey/\d+/(\d+-\d+)\.html)"[^>]*>([^<]*)<', group_html):
|
||||
ep_href = em.group(1)
|
||||
ep_label = em.group(3).strip()
|
||||
if ep_label and ep_href:
|
||||
episodes.append(f'{ep_label}${ep_href}')
|
||||
if episodes:
|
||||
play_from.append(line_name)
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
# 把华为云排到第一个(1080p)
|
||||
for i, name in enumerate(play_from):
|
||||
if '华为' in name and i > 0:
|
||||
play_from.insert(0, play_from.pop(i))
|
||||
play_url_list.insert(0, play_url_list.pop(i))
|
||||
break
|
||||
|
||||
# 备用:如果详情页没有找到播放列表,直接用播放页的 URL
|
||||
if not play_from and pd:
|
||||
url = pd.get('url', '')
|
||||
from_flag = pd.get('from', '')
|
||||
if url:
|
||||
play_from.append(from_flag or '线路①')
|
||||
play_url_list.append(f'播放${url}')
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': vod_class,
|
||||
'vod_year': year,
|
||||
'vod_area': area,
|
||||
'vod_lang': lang,
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url_list)
|
||||
}
|
||||
result['list'].append(vod)
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
# id 格式: /aey/119317/1-1.html
|
||||
play_url = id
|
||||
if not play_url.startswith('http'):
|
||||
play_url = self.site + play_url
|
||||
|
||||
html = self._get(play_url)
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
pd = json.loads(m.group(1))
|
||||
url = pd.get('url', '')
|
||||
if url:
|
||||
result['parse'] = 0
|
||||
result['url'] = url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
# 备用:嗅探
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
|
||||
if not result:
|
||||
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
wd = requests.utils.quote(key)
|
||||
url = f'{self.site}/search/-------------.html?wd={wd}'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,278 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json,re,sys,base64,requests,threading,time,random,colorsys
|
||||
from Crypto.Cipher import AES
|
||||
from pyquery import PyQuery as pq
|
||||
from urllib.parse import quote, unquote
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
SELECTORS=['.video-item','.video-list .item','.list-item','.post-item']
|
||||
def getName(self):return"黑料不打烊"
|
||||
def init(self,extend=""):pass
|
||||
def homeContent(self,filter):
|
||||
cateManual={"最新黑料":"hlcg","今日热瓜":"jrrs","每日TOP10":"mrrb","反差女友":"fczq","校园黑料":"xycg","网红黑料":"whhl","明星丑闻":"mxcw","原创社区":"ycsq","推特社区":"ttsq","社会新闻":"shxw","官场爆料":"gchl","影视短剧":"ysdj","全球奇闻":"qqqw","黑料课堂":"hlkt","每日大赛":"mrds","激情小说":"jqxs","桃图杂志":"ttzz","深夜综艺":"syzy","独家爆料":"djbl"}
|
||||
return{'class':[{'type_name':k,'type_id':v}for k,v in cateManual.items()]}
|
||||
def homeVideoContent(self):return{}
|
||||
def categoryContent(self,tid,pg,filter,extend):
|
||||
url=f'https://heiliao.com/{tid}/'if int(pg)==1 else f'https://heiliao.com/{tid}/page/{pg}/'
|
||||
videos=self.get_list(url)
|
||||
return{'list':videos,'page':pg,'pagecount':9999,'limit':90,'total':999999}
|
||||
def fetch_and_decrypt_image(self,url):
|
||||
try:
|
||||
if url.startswith('//'):url='https:'+url
|
||||
elif url.startswith('/'):url='https://heiliao.com'+url
|
||||
r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36','Referer':'https://heiliao.com/'},timeout=15,verify=False)
|
||||
if r.status_code!=200:return b''
|
||||
return AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content)
|
||||
except: return b''
|
||||
def _extract_img_from_onload(self,node):
|
||||
try:
|
||||
m=re.search(r"load(?:Share)?Img\s*\([^,]+,\s*['\"]([^'\"]+)['\"]",(node.attr('onload')or''))
|
||||
return m.group(1)if m else''
|
||||
except:return''
|
||||
def _should_decrypt(self,url:str)->bool:
|
||||
u=(url or'').lower();return any(x in u for x in['pic.gylhaa.cn','new.slfpld.cn','/upload_01/','/upload/'])
|
||||
def _abs(self,u:str)->str:
|
||||
if not u:return''
|
||||
if u.startswith('//'):return'https:'+u
|
||||
if u.startswith('/'):return'https://heiliao.com'+u
|
||||
return u
|
||||
def e64(self,s:str)->str:
|
||||
try:return base64.b64encode((s or'').encode()).decode()
|
||||
except:return''
|
||||
def d64(self,s:str)->str:
|
||||
try:return base64.b64decode((s or'').encode()).decode()
|
||||
except:return''
|
||||
def _img(self,img_node):
|
||||
u=''if img_node is None else(img_node.attr('src')or img_node.attr('data-src')or'')
|
||||
enc=''if img_node is None else self._extract_img_from_onload(img_node)
|
||||
t=enc or u
|
||||
return f"{self.getProxyUrl()}&url={self.e64(t)}&type=hlimg"if t and(enc or self._should_decrypt(t))else self._abs(t)
|
||||
def _parse_items(self,root):
|
||||
vids=[]
|
||||
for sel in self.SELECTORS:
|
||||
for it in root(sel).items():
|
||||
title=it.find('.title, h3, h4, .video-title').text()
|
||||
if not title:continue
|
||||
link=it.find('a').attr('href')
|
||||
if not link:continue
|
||||
vids.append({'vod_id':self._abs(link),'vod_name':title,'vod_pic':self._img(it.find('img')),'vod_remarks':it.find('.date, .time, .remarks, .duration').text()or''})
|
||||
if vids:break
|
||||
return vids
|
||||
def detailContent(self,array):
|
||||
tid=array[0];url=tid if tid.startswith('http')else f'https://heiliao.com{tid}'
|
||||
rsp=self.fetch(url)
|
||||
if not rsp:return{'list':[]}
|
||||
rsp.encoding='utf-8';html_text=rsp.text
|
||||
try:root_text=pq(html_text)
|
||||
except:root_text=None
|
||||
try:root_content=pq(rsp.content)
|
||||
except:root_content=None
|
||||
title=(root_text('title').text()if root_text else'')or''
|
||||
if' - 黑料网'in title:title=title.replace(' - 黑料网','')
|
||||
pic=''
|
||||
if root_text:
|
||||
og=root_text('meta[property="og:image"]').attr('content')
|
||||
if og and(og.endswith('.png')or og.endswith('.jpg')or og.endswith('.jpeg')):pic=og
|
||||
else:pic=self._img(root_text('.video-item-img img'))
|
||||
detail=''
|
||||
if root_text:
|
||||
detail=root_text('meta[name="description"]').attr('content')or''
|
||||
if not detail:detail=root_text('.content').text()[:200]
|
||||
play_from,play_url=[],[]
|
||||
if root_content:
|
||||
for i,p in enumerate(root_content('.dplayer').items()):
|
||||
c=p.attr('config')
|
||||
if not c:continue
|
||||
try:s=(c.replace('"','"').replace('"','"').replace('&','&').replace('&','&').replace('<','<').replace('<','<').replace('>','>').replace('>','>'));u=(json.loads(s).get('video',{})or{}).get('url','')
|
||||
except:m=re.search(r'"url"\s*:\s*"([^"]+)"',c);u=m.group(1)if m else''
|
||||
if u:
|
||||
u=u.replace('\\/','/');u=self._abs(u)
|
||||
# Extract article ID for danmaku
|
||||
article_id = self._extract_article_id(tid)
|
||||
if article_id:
|
||||
play_from.append(f'视频{i+1}');play_url.append(f"{article_id}_dm_{u}")
|
||||
else:
|
||||
play_from.append(f'视频{i+1}');play_url.append(u)
|
||||
if not play_url:
|
||||
for pat in[r'https://hls\.[^"\']+\.m3u8[^"\']*',r'https://[^"\']+\.m3u8\?auth_key=[^"\']+',r'//hls\.[^"\']+\.m3u8[^"\']*']:
|
||||
for u in re.findall(pat,html_text):
|
||||
u=self._abs(u)
|
||||
article_id = self._extract_article_id(tid)
|
||||
if article_id:
|
||||
play_from.append(f'视频{len(play_from)+1}');play_url.append(f"{article_id}_dm_{u}")
|
||||
else:
|
||||
play_from.append(f'视频{len(play_from)+1}');play_url.append(u)
|
||||
if len(play_url)>=3:break
|
||||
if play_url:break
|
||||
if not play_url:
|
||||
js_patterns=[r'video[\s\S]{0,500}?url[\s"\'`:=]+([^"\'`\s]+)',r'videoUrl[\s"\'`:=]+([^"\'`\s]+)',r'src[\s"\'`:=]+([^"\'`\s]+\.m3u8[^"\'`\s]*)']
|
||||
for pattern in js_patterns:
|
||||
js_urls=re.findall(pattern,html_text)
|
||||
for js_url in js_urls:
|
||||
if'.m3u8'in js_url:
|
||||
if js_url.startswith('//'):js_url='https:'+js_url
|
||||
elif js_url.startswith('/'):js_url='https://heiliao.com'+js_url
|
||||
elif not js_url.startswith('http'):js_url='https://'+js_url
|
||||
article_id = self._extract_article_id(tid)
|
||||
if article_id:
|
||||
play_from.append(f'视频{len(play_from)+1}');play_url.append(f"{article_id}_dm_{js_url}")
|
||||
else:
|
||||
play_from.append(f'视频{len(play_from)+1}');play_url.append(js_url)
|
||||
if len(play_url)>=3:break
|
||||
if play_url:break
|
||||
if not play_url:
|
||||
article_id = self._extract_article_id(tid)
|
||||
example_url = "https://hls.obmoti.cn/videos5/b9699667fbbffcd464f8874395b91c81/b9699667fbbffcd464f8874395b91c81.m3u8?auth_key=1760372539-68ed273b94e7a-0-3a53bc0df110c5f149b7d374122ef1ed&v=2"
|
||||
if article_id:
|
||||
play_from.append('示例视频');play_url.append(f"{article_id}_dm_{example_url}")
|
||||
else:
|
||||
play_from.append('示例视频');play_url.append(example_url)
|
||||
return{'list':[{'vod_id':tid,'vod_name':title,'vod_pic':pic,'vod_content':detail,'vod_play_from':'$$$'.join(play_from),'vod_play_url':'$$$'.join(play_url)}]}
|
||||
def searchContent(self,key,quick,pg="1"):
|
||||
rsp=self.fetch(f'https://heiliao.com/index/search?word={key}')
|
||||
if not rsp:return{'list':[]}
|
||||
return{'list':self._parse_items(pq(rsp.text))}
|
||||
def playerContent(self,flag,id,vipFlags):
|
||||
# Check if this is a danmaku-enabled video
|
||||
if '_dm_' in id:
|
||||
aid, pid = id.split('_dm_', 1)
|
||||
p = 0 if re.search(r'\.(m3u8|mp4|flv|ts|mkv|mov|avi|webm)', pid) else 1
|
||||
if not p:
|
||||
pid = f"{self.getProxyUrl()}&pdid={quote(id)}&type=m3u8"
|
||||
return {'parse': p, 'url': pid, 'header': {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}}
|
||||
else:
|
||||
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/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}}
|
||||
def get_list(self,url):
|
||||
rsp=self.fetch(url)
|
||||
return[]if not rsp else self._parse_items(pq(rsp.text))
|
||||
def fetch(self,url,params=None,cookies=None,headers=None,timeout=5,verify=True,stream=False,allow_redirects=True):
|
||||
h=headers or{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}
|
||||
return super().fetch(url,params=params,cookies=cookies,headers=h,timeout=timeout,verify=verify,stream=stream,allow_redirects=allow_redirects)
|
||||
def localProxy(self,param):
|
||||
try:
|
||||
xtype = param.get('type', '')
|
||||
if xtype == 'hlimg':
|
||||
url=self.d64(param.get('url'))
|
||||
if url.startswith('//'):url='https:'+url
|
||||
elif url.startswith('/'):url='https://heiliao.com'+url
|
||||
r=requests.get(url,headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"},timeout=15,verify=False)
|
||||
if r.status_code!=200:return[404,'text/plain','']
|
||||
b=AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content)
|
||||
ct='image/jpeg'
|
||||
if b.startswith(b'\x89PNG'):ct='image/png'
|
||||
elif b.startswith(b'GIF8'):ct='image/gif'
|
||||
return[200,ct,b]
|
||||
elif xtype == 'm3u8':
|
||||
# Handle danmaku-enabled video
|
||||
path, url = unquote(param['pdid']).split('_dm_', 1)
|
||||
data = requests.get(url, headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}, timeout=10).text
|
||||
lines = data.strip().split('\n')
|
||||
times = 0.0
|
||||
for i in lines:
|
||||
if i.startswith('#EXTINF:'):
|
||||
times += float(i.split(':')[-1].replace(',', ''))
|
||||
# Start background thread to refresh danmaku
|
||||
thread = threading.Thread(target=self.some_background_task, args=(path, int(times)))
|
||||
thread.start()
|
||||
print('[INFO] 获取视频时长成功', times)
|
||||
return [200, 'text/plain', data]
|
||||
elif xtype == 'hlxdm':
|
||||
# Return danmaku XML for heiliao comments
|
||||
article_id = param.get('path', '')
|
||||
times = int(param.get('times', 0))
|
||||
comments = self._fetch_heiliao_comments(article_id)
|
||||
return self._generate_danmaku_xml(comments, times)
|
||||
except Exception as e:
|
||||
print(f'[ERROR] localProxy: {e}')
|
||||
return[404,'text/plain','']
|
||||
|
||||
def _extract_article_id(self, url):
|
||||
"""Extract article ID from heiliao.com URL"""
|
||||
try:
|
||||
if '/archives/' in url:
|
||||
match = re.search(r'/archives/(\d+)/?', url)
|
||||
return match.group(1) if match else None
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def _fetch_heiliao_comments(self, article_id, max_pages=3):
|
||||
"""Fetch comments from heiliao.com API"""
|
||||
comments = []
|
||||
try:
|
||||
for page in range(1, max_pages + 1):
|
||||
url = f"https://heiliao.com/comments/1/{article_id}/{page}.json"
|
||||
resp = requests.get(url, headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if 'data' in data and 'list' in data['data'] and data['data']['list']:
|
||||
for comment in data['data']['list']:
|
||||
text = comment.get('content', '').strip()
|
||||
if text and len(text) <= 100: # Filter out too long comments
|
||||
comments.append(text)
|
||||
# Also get replies from comments.list
|
||||
if 'comments' in comment and 'list' in comment['comments'] and comment['comments']['list']:
|
||||
for reply in comment['comments']['list']:
|
||||
reply_text = reply.get('content', '').strip()
|
||||
if reply_text and len(reply_text) <= 100:
|
||||
comments.append(reply_text)
|
||||
# Check if there are more pages
|
||||
if not data['data'].get('next', False):
|
||||
break
|
||||
else:
|
||||
break # No more comments
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'[ERROR] _fetch_heiliao_comments: {e}')
|
||||
return comments[:50] # Limit to 50 comments max
|
||||
|
||||
def _generate_danmaku_xml(self, comments, video_duration):
|
||||
"""Generate danmaku XML from comments"""
|
||||
try:
|
||||
total_comments = len(comments)
|
||||
tsrt = f'共有{total_comments}条弹幕来袭!!!'
|
||||
danmu_xml = f'<?xml version="1.0" encoding="UTF-8"?>\n<i>\n\t<chatserver>chat.heiliao.com</chatserver>\n\t<chatid>88888888</chatid>\n\t<mission>0</mission>\n\t<maxlimit>99999</maxlimit>\n\t<state>0</state>\n\t<real_name>0</real_name>\n\t<source>heiliao</source>\n'
|
||||
danmu_xml += f'\t<d p="0,5,25,16711680,0">{tsrt}</d>\n'
|
||||
|
||||
for i, comment in enumerate(comments):
|
||||
# Distribute comments across video duration
|
||||
base_time = (i / total_comments) * video_duration if total_comments > 0 else 0
|
||||
dm_time = base_time + random.uniform(-3, 3)
|
||||
dm_time = round(max(0, min(dm_time, video_duration)), 1)
|
||||
dm_color = self._get_danmaku_color()
|
||||
# Clean comment text
|
||||
dm_text = re.sub(r'[<>&\u0000\b]', '', comment)
|
||||
danmu_xml += f'\t<d p="{dm_time},1,25,{dm_color},0">{dm_text}</d>\n'
|
||||
|
||||
danmu_xml += '</i>'
|
||||
return [200, "text/xml", danmu_xml]
|
||||
except Exception as e:
|
||||
print(f'[ERROR] _generate_danmaku_xml: {e}')
|
||||
return [500, 'text/html', '']
|
||||
|
||||
def _get_danmaku_color(self):
|
||||
"""Get danmaku color (90% white, 10% random)"""
|
||||
if random.random() < 0.1:
|
||||
h = random.random()
|
||||
s = random.uniform(0.7, 1.0)
|
||||
v = random.uniform(0.8, 1.0)
|
||||
r, g, b = colorsys.hsv_to_rgb(h, s, v)
|
||||
r = int(r * 255)
|
||||
g = int(g * 255)
|
||||
b = int(b * 255)
|
||||
return str((r << 16) + (g << 8) + b)
|
||||
else:
|
||||
return '16777215' # White
|
||||
|
||||
def some_background_task(self, article_id, video_duration):
|
||||
"""Background task to refresh danmaku in FongMi"""
|
||||
try:
|
||||
time.sleep(1)
|
||||
danmaku_url = f"{self.getProxyUrl()}&path={quote(article_id)}×={video_duration}&type=hlxdm"
|
||||
self.fetch(f"http://127.0.0.1:9978/action?do=refresh&type=danmaku&path={quote(danmaku_url)}")
|
||||
print(f'[INFO] 弹幕刷新成功: {article_id}')
|
||||
except Exception as e:
|
||||
print(f'[ERROR] some_background_task: {e}')
|
||||
Reference in New Issue
Block a user