261 lines
10 KiB
Python
261 lines
10 KiB
Python
# coding=utf-8
|
|
import re
|
|
import sys
|
|
from urllib.parse import quote, unquote
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
from base.spider import Spider as BaseSpider
|
|
|
|
sys.path.append("..")
|
|
|
|
|
|
class Spider(BaseSpider):
|
|
def __init__(self):
|
|
self.name = "PPnix[采]"
|
|
self.host = "https://www.ppnix.com"
|
|
self.lang_path = "/cn"
|
|
self.headers = {
|
|
"User-Agent": (
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Chrome/145.0.0.0 Safari/537.36"
|
|
),
|
|
"Referer": self.host + self.lang_path + "/",
|
|
}
|
|
self.classes = [
|
|
{"type_id": "1", "type_name": "电影"},
|
|
{"type_id": "2", "type_name": "电视剧"},
|
|
]
|
|
sort_values = [
|
|
{"n": "按时间", "v": "time"},
|
|
{"n": "按人气", "v": "hits"},
|
|
{"n": "按评分", "v": "score"},
|
|
]
|
|
self.filters = {
|
|
"1": [
|
|
{
|
|
"key": "class",
|
|
"name": "类型",
|
|
"init": "",
|
|
"value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "动作", "v": "动作"},
|
|
{"n": "喜剧", "v": "喜剧"},
|
|
{"n": "剧情", "v": "剧情"},
|
|
],
|
|
},
|
|
{"key": "by", "name": "排序", "init": "time", "value": list(sort_values)},
|
|
],
|
|
"2": [
|
|
{
|
|
"key": "class",
|
|
"name": "类型",
|
|
"init": "",
|
|
"value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "爱情", "v": "爱情"},
|
|
{"n": "古装", "v": "古装"},
|
|
{"n": "悬疑", "v": "悬疑"},
|
|
],
|
|
},
|
|
{"key": "by", "name": "排序", "init": "time", "value": list(sort_values)},
|
|
],
|
|
}
|
|
|
|
def init(self, extend=""):
|
|
return None
|
|
|
|
def getName(self):
|
|
return self.name
|
|
|
|
def homeContent(self, filter):
|
|
return {"class": self.classes, "filters": self.filters}
|
|
|
|
def _build_url(self, value):
|
|
raw = str(value or "").strip()
|
|
if not raw:
|
|
return self.host + self.lang_path + "/"
|
|
if raw.startswith(("http://", "https://")):
|
|
return raw
|
|
if raw.startswith("/"):
|
|
return self.host + raw
|
|
return self.host + "/" + raw
|
|
|
|
def _map_type_slug(self, tid):
|
|
return "tv" if str(tid) == "2" else "movie"
|
|
|
|
def _map_sort(self, value):
|
|
mapping = {"time": "newstime", "hits": "onclick", "score": "rating"}
|
|
return mapping.get(str(value or "time"), "newstime")
|
|
|
|
def _build_category_url(self, tid, pg, extend):
|
|
values = extend if isinstance(extend, dict) else {}
|
|
slug = self._map_type_slug(tid)
|
|
genre = str(values.get("class") or "").strip()
|
|
page = max(int(pg), 1)
|
|
page_part = "" if page <= 1 else str(page - 1)
|
|
sort = self._map_sort(values.get("by", "time"))
|
|
return self._build_url(f"{self.lang_path}/{slug}/{genre}---{page_part}-{sort}.html")
|
|
|
|
def _build_search_url(self, keyword, pg):
|
|
page = max(int(pg), 1)
|
|
suffix = "" if page <= 1 else f"-page-{page}"
|
|
encoded = quote(str(keyword or "").strip())
|
|
return self._build_url(f"{self.lang_path}/search/{encoded}--.html{suffix}")
|
|
|
|
def _text(self, node):
|
|
return node.get_text(" ", strip=True) if node else ""
|
|
|
|
def _request_html(self, path_or_url, referer=None, extra_headers=None):
|
|
target = path_or_url if str(path_or_url).startswith("http") else self._build_url(path_or_url)
|
|
headers = dict(self.headers)
|
|
headers["Referer"] = referer or self.headers["Referer"]
|
|
if isinstance(extra_headers, dict):
|
|
headers.update(extra_headers)
|
|
response = self.fetch(target, headers=headers, timeout=10)
|
|
if response.status_code != 200:
|
|
return ""
|
|
return response.text or ""
|
|
|
|
def _dedupe(self, items):
|
|
seen = set()
|
|
result = []
|
|
for item in items:
|
|
vod_id = str(item.get("vod_id") or "").strip()
|
|
if not vod_id or vod_id in seen:
|
|
continue
|
|
seen.add(vod_id)
|
|
result.append(item)
|
|
return result
|
|
|
|
def _parse_cards(self, html):
|
|
soup = BeautifulSoup(html or "", "html.parser")
|
|
items = []
|
|
for node in soup.select("li"):
|
|
anchor = node.select_one("a.thumbnail") or node.select_one("h2 a") or node.select_one("a")
|
|
image = node.select_one("img.thumb")
|
|
if not anchor:
|
|
continue
|
|
href = str(anchor.get("href") or "").strip()
|
|
if not re.search(r"/cn/(movie|tv)/\d+\.html", href):
|
|
continue
|
|
vod_name = str((image.get("alt") if image else "") or self._text(anchor)).strip()
|
|
if not vod_name:
|
|
continue
|
|
items.append(
|
|
{
|
|
"vod_id": href.replace("/cn/", "").lstrip("/"),
|
|
"vod_name": vod_name,
|
|
"vod_pic": self._build_url((image.get("src") if image else "") or ""),
|
|
"vod_remarks": self._text(node.select_one("footer .rate") or node.select_one("footer")),
|
|
}
|
|
)
|
|
return items
|
|
|
|
def _extract_m3u8_items(self, html):
|
|
body = str(html or "")
|
|
info_match = re.search(r"infoid\s*=\s*(\d+)", body)
|
|
list_match = re.search(r"m3u8\s*=\s*\[(.*?)\]", body, re.S)
|
|
raw_list = list_match.group(1) if list_match else ""
|
|
items = []
|
|
for single, double in re.findall(r"'([^']*)'|\"([^\"]*)\"", raw_list):
|
|
value = str(single or double or "").strip()
|
|
if value:
|
|
items.append(value)
|
|
return {"info_id": info_match.group(1) if info_match else "", "items": items}
|
|
|
|
def _build_play_id(self, info_id, param):
|
|
return f"{str(info_id or '').strip()}|{quote(str(param or '').strip())}"
|
|
|
|
def _parse_play_id(self, play_id):
|
|
parts = str(play_id or "").split("|", 1)
|
|
if len(parts) != 2:
|
|
return {"info_id": "", "param": ""}
|
|
info_id = parts[0].strip()
|
|
param = unquote(parts[1].strip())
|
|
if not info_id or not param:
|
|
return {"info_id": "", "param": ""}
|
|
return {"info_id": info_id, "param": param}
|
|
|
|
def _extract_excerpt_text(self, soup, label):
|
|
for node in soup.select(".product-excerpt"):
|
|
text = self._text(node)
|
|
if text.startswith(label) or label in text:
|
|
return text.replace(label, "", 1).strip()
|
|
return ""
|
|
|
|
def homeVideoContent(self):
|
|
html = self._request_html(self.lang_path + "/")
|
|
soup = BeautifulSoup(html or "", "html.parser")
|
|
blocks = soup.select(".lists-content ul")
|
|
items = []
|
|
if len(blocks) > 0:
|
|
items.extend(self._parse_cards(str(blocks[0])))
|
|
if len(blocks) > 1:
|
|
items.extend(self._parse_cards(str(blocks[1])))
|
|
return {"list": self._dedupe(items)[:20]}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
page = max(int(pg), 1)
|
|
slug = self._map_type_slug(tid)
|
|
html = self._request_html(self._build_category_url(tid, pg, extend or {}))
|
|
items = [item for item in self._parse_cards(html) if item["vod_id"].startswith(slug + "/")]
|
|
return {"page": page, "limit": 24, "total": page * 24 + len(items), "list": items}
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
page = max(int(pg), 1)
|
|
items = self._parse_cards(self._request_html(self._build_search_url(key, pg)))
|
|
if str(quick).lower() == "true":
|
|
items = items[:10]
|
|
return {"page": page, "limit": 24, "total": page * 24 + len(items), "list": items}
|
|
|
|
def detailContent(self, ids):
|
|
vod_id = str(ids[0] if isinstance(ids, list) and ids else ids or "").strip().lstrip("/")
|
|
if not vod_id:
|
|
return {"list": []}
|
|
html = self._request_html(f"{self.lang_path}/{vod_id}")
|
|
if not html:
|
|
return {"list": []}
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
title_text = self._text(soup.select_one("h1.product-title") or soup.select_one("title"))
|
|
info = self._extract_m3u8_items(html)
|
|
play_urls = []
|
|
for item in info["items"]:
|
|
play_urls.append(f"{item}${self._build_play_id(info['info_id'], item)}")
|
|
actor = self._extract_excerpt_text(soup, "主演:").replace(" / ", ",").replace("/", ",")
|
|
vod_name = re.sub(r"\s*\([^)]*\)\s*$", "", title_text).strip()
|
|
year_match = re.search(r"\((\d{4})\)", title_text)
|
|
return {
|
|
"list": [
|
|
{
|
|
"vod_id": vod_id,
|
|
"vod_name": vod_name,
|
|
"vod_pic": self._build_url((soup.select_one("header.product-header img.thumb") or {}).get("src", "")),
|
|
"type_name": "电视剧" if vod_id.startswith("tv/") else "电影",
|
|
"vod_year": year_match.group(1) if year_match else "",
|
|
"vod_director": self._extract_excerpt_text(soup, "导演:"),
|
|
"vod_actor": actor,
|
|
"vod_content": self._extract_excerpt_text(soup, "简介:"),
|
|
"vod_play_from": "PPnix" if play_urls else "",
|
|
"vod_play_url": "#".join(play_urls),
|
|
}
|
|
]
|
|
}
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
meta = self._parse_play_id(id)
|
|
if not meta["info_id"] or not meta["param"]:
|
|
return {"parse": 1, "jx": 1, "url": str(id or ""), "header": {}}
|
|
encoded = quote(meta["param"])
|
|
return {
|
|
"parse": 0,
|
|
"jx": 0,
|
|
"url": self._build_url(f"/info/m3u8/{meta['info_id']}/{encoded}.m3u8"),
|
|
"header": {
|
|
"Referer": self.host + self.lang_path + "/",
|
|
"Origin": self.host,
|
|
"User-Agent": self.headers["User-Agent"],
|
|
},
|
|
}
|