import sys
import re
import json
import requests
from urllib.parse import unquote, urljoin, quote
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from base.spider import Spider
requests.packages.urllib3.disable_warnings()
class Spider(Spider):
def getName(self):
return "PPNIX影视"
def init(self, extend=""):
super().init(extend)
self.site_url = "https://www.ppnix.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": self.site_url
}
self.sess = requests.Session()
self.sess.verify = False
def fetch(self, url, headers=None):
try:
h = headers or self.headers
resp = self.sess.get(url, headers=h, timeout=15)
resp.encoding = 'utf-8'
return resp.text if resp.status_code == 200 else ""
except Exception as e:
return ""
def homeContent(self, filter):
return {"class": [
{"type_name": "电影", "type_id": "cn/movie"},
{"type_name": "电视剧", "type_id": "cn/tv"}
]}
def homeVideoContent(self):
html = self.fetch(self.site_url + "/cn/")
return {"list": self.parse_list(html)}
def categoryContent(self, tid, pg, filter, extend):
if int(pg) <= 1:
url = f"{self.site_url}/{tid}/"
else:
url = f"{self.site_url}/{tid}/page/{pg}.html"
html = self.fetch(url)
return {
"list": self.parse_list(html),
"page": pg,
"pagecount": 100,
"limit": 20
}
def detailContent(self, ids):
vod_id = ids[0]
html = self.fetch(self.site_url + vod_id)
if not html:
return {"list": []}
# 1. 提取标题
title_match = re.search(r'
]*>([^<]+)', html)
title = title_match.group(1).strip() if title_match else "未知"
title = re.sub(r'\s*\(?\d{4}\)?.*', '', title).strip()
# 2. 提取图片
pic_match = re.search(r']*>.*?
]+src=["\']([^"\']+)["\']', html, re.S)
pic = pic_match.group(1) if pic_match else ""
if pic.startswith('//'): pic = 'https:' + pic
# 3. 提取简介
desc_match = re.search(r'简介:\s*(.*?)', html, re.S)
desc = desc_match.group(1).strip() if desc_match else ""
# 4. 提取 classid 和 infoid
classid_match = re.search(r'classid\s*=\s*(\d+)', html)
infoid_match = re.search(r'infoid\s*=\s*(\d+)', html)
classid = classid_match.group(1) if classid_match else "1"
infoid = infoid_match.group(1) if infoid_match else ""
if not infoid:
id_nums = re.findall(r'\d+', vod_id)
infoid = id_nums[-1] if id_nums else ""
# 5. 判断类型并生成播放链接
is_movie = '/movie/' in vod_id
lang_path = "/cn" if "/cn/" in vod_id else ""
# 提取 m3u8 数组(画质选项)
m3u8_match = re.search(r'm3u8\s*=\s*(\[[^\]]+\])', html)
play_list = []
if m3u8_match:
try:
m3u8_data = eval(m3u8_match.group(1))
if isinstance(m3u8_data, list):
if len(m3u8_data) > 1 or not is_movie:
# 多集电视剧
for i, quality in enumerate(m3u8_data, 1):
ep_name = f"第{i}集" if not is_movie else f"{quality}版"
play_list.append(f"{ep_name}${lang_path}/play/{infoid}-{classid}-{i}.html")
else:
# 单集电影
play_list.append(f"正片${lang_path}/play/{infoid}-{classid}-1.html")
except:
play_list.append(f"正片${lang_path}/play/{infoid}-{classid}-1.html")
else:
play_list.append(f"正片${lang_path}/play/{infoid}-{classid}-1.html")
play_url = "#".join(play_list) if play_list else ""
return {"list": [{
"vod_id": vod_id,
"vod_name": title,
"vod_pic": pic,
"vod_content": desc,
"vod_play_from": "PPNIX",
"vod_play_url": play_url
}]}
def playerContent(self, flag, id, vipFlags):
"""
获取播放地址
id格式: /cn/play/6276-1-1.html
其中: infoid=6276, classid=1, episode=1
"""
# 1. 解析参数
match = re.search(r'/(\d+)-(\d+)-(\d+)', id)
if not match:
return {"parse": 1, "url": self.site_url + id if id.startswith('/') else id}
infoid = match.group(1)
classid = match.group(2)
episode = match.group(3)
# 2. 构造请求头
player_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": self.site_url + id,
"Origin": self.site_url,
"X-Requested-With": "XMLHttpRequest",
"Connection": "keep-alive"
}
# 3. 尝试API接口获取真实播放地址
api_url = f"https://www.ppnix.com/index.php/api/player?type={classid}&id={infoid}&ep={episode}"
try:
print(f"🎬 请求API: {api_url}")
resp = self.sess.get(api_url, headers=player_headers, timeout=10)
if resp.status_code == 200:
content = resp.text.strip()
print(f"📦 API返回: {content[:200] if content else '空'}")
# 检查是否是m3u8内容
if content and '#EXTM3U' in content:
print(f"✅ 直接获取到m3u8: {api_url}")
return {"parse": 0, "url": api_url, "header": player_headers}
# 检查是否是JSON
if content.startswith('{') or content.startswith('['):
try:
data = json.loads(content)
# 提取视频URL
video_url = None
if isinstance(data, dict):
if 'url' in data and data['url']:
video_url = data['url']
elif 'data' in data and isinstance(data['data'], dict) and 'url' in data['data']:
video_url = data['data']['url']
elif 'video' in data and data['video']:
video_url = data['video']
if video_url:
print(f"✅ 从JSON提取到视频地址: {video_url}")
return {"parse": 0, "url": video_url, "header": player_headers}
except json.JSONDecodeError:
pass
except Exception as e:
print(f"⚠️ API请求异常: {e}")
# 4. 尝试备用API(info/m3u8接口)
backup_apis = [
f"https://www.ppnix.com/info/m3u8/{infoid}/{episode}.m3u8",
f"https://www.ppnix.com/info/m3u8/{infoid}/1080P.m3u8",
f"https://www.ppnix.com/m3u8/{infoid}/{episode}.m3u8",
]
for api in backup_apis:
try:
resp = self.sess.get(api, headers=player_headers, timeout=8)
if resp.status_code == 200 and resp.text and '#EXTM3U' in resp.text:
print(f"✅ 从备用API获取到m3u8: {api}")
return {"parse": 0, "url": api, "header": player_headers}
except:
continue
# 5. 尝试从播放页源码提取
play_page_url = self.site_url + id
print(f"🔄 尝试从播放页提取: {play_page_url}")
play_html = self.fetch(play_page_url, headers=self.headers)
if play_html:
# 查找video标签或配置中的url
patterns = [
r'