Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-05 15:15:28 +00:00
parent 020674b170
commit a807d3f6e5
26 changed files with 9220 additions and 4369 deletions
+389
View File
@@ -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('&quot;', '"').replace('&apos;', "'").replace('&amp;', '&')
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'))
+192
View File
@@ -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('&amp;', '&')
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
+258
View File
@@ -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)
+300
View File
@@ -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('&nbsp;', ' ')
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", {}, ""]
+300
View File
@@ -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}&params={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
+489
View File
@@ -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
+347
View File
@@ -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
+480
View File
@@ -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 {'&nbsp;': ' ', '&amp;': '&', '&quot;': '"', '&#39;': "'", '&lt;': '<', '&gt;': '>'}.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 '')
+261
View File
@@ -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
+343
View File
@@ -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('&nbsp;', ' ').replace('&amp;', '&').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('&amp;', '&')
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('&amp;', '&')
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('&amp;', '&')
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('&amp;', '&')
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", {}, ""]
+283
View File
@@ -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 []
+283
View File
@@ -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", {}, ""]
+278
View File
@@ -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('&quot;','"').replace('&#34;','"').replace('&amp;','&').replace('&#38;','&').replace('&lt;','<').replace('&#60;','<').replace('&gt;','>').replace('&#62;','>'));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)}&times={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}')