This commit is contained in:
Harold
2026-04-19 21:19:25 +08:00
parent f86a4b5b68
commit b975ba51d6
2 changed files with 206 additions and 0 deletions
+171
View File
@@ -1,7 +1,9 @@
# coding=utf-8
import json
import re
import subprocess
import sys
from pathlib import Path
from urllib.parse import parse_qsl, quote, urljoin, urlsplit
from lxml import html as lxml_html
@@ -493,6 +495,154 @@ class Spider(BaseSpider):
except Exception:
return {}
def _extract_nbmovie_context(self, play_page_html, play_slug):
html_text = str(play_page_html or "")
root = self._parse_html(html_text)
play_path = "/" + str(play_slug or "").lstrip("/")
if not play_path.startswith("/play/"):
play_path = "/play/" + play_path.lstrip("/")
dataid = ""
if root is not None:
for node in root.xpath("//a[@href and @dataid]"):
href = self._clean_text(node.get("href", ""))
if href == play_path:
dataid = self._clean_text(node.get("dataid", ""))
break
userlink = ""
userlink_match = re.search(r"userlink:'([^']+)'", html_text)
if userlink_match:
userlink = self._clean_text(userlink_match.group(1))
nbst = ""
if root is not None:
nbst = self._first_xpath_attr(root, ["//meta[@id='nb-st']/@content"])
wasm_js = ""
wasm_bg = ""
if root is not None:
wasm_js = self._first_xpath_attr(root, ["//link[@id='wasm-cfg']/@data-js"])
wasm_bg = self._first_xpath_attr(root, ["//link[@id='wasm-cfg']/@data-bg"])
if not (dataid and userlink and nbst and wasm_js and wasm_bg):
return {}
return {
"dataid": dataid,
"userlink": userlink,
"nbst": nbst,
"wasm_js": self._normalize_url(wasm_js),
"wasm_bg": self._normalize_url(wasm_bg),
}
def _cache_nbmovie_runtime(self, wasm_js_url, wasm_bg_url):
cache_dir = Path("/tmp/4kvm_nbmovie")
cache_dir.mkdir(parents=True, exist_ok=True)
js_name = re.sub(r"[^a-zA-Z0-9._-]", "_", urlsplit(wasm_js_url).path.rsplit("/", 1)[-1] or "nbmovie_wasm.js")
if not js_name.endswith(".mjs"):
js_name = re.sub(r"\.js$", "", js_name) + ".mjs"
wasm_name = re.sub(r"[^a-zA-Z0-9._-]", "_", urlsplit(wasm_bg_url).path.rsplit("/", 1)[-1] or "nbmovie_wasm_bg.wasm")
js_path = cache_dir / js_name
wasm_path = cache_dir / wasm_name
if not js_path.exists():
response = self.fetch(wasm_js_url, headers=self.headers, timeout=15, verify=False)
if response.status_code != 200:
return None, None
js_path.write_text(response.text or "", encoding="utf-8")
if not wasm_path.exists():
response = self.fetch(wasm_bg_url, headers=self.headers, timeout=15, verify=False)
if response.status_code != 200:
return None, None
content = getattr(response, "content", None)
if content is None:
return None, None
wasm_path.write_bytes(content)
return js_path, wasm_path
def _build_nbmovie_api_url(self, play_page_html, play_slug, dataid, quality="1080"):
context = self._extract_nbmovie_context(play_page_html, play_slug)
if not context:
return ""
js_path, wasm_path = self._cache_nbmovie_runtime(context["wasm_js"], context["wasm_bg"])
if not (js_path and wasm_path):
return ""
script = """
import { readFileSync } from "node:fs";
globalThis.HTMLMetaElement = class HTMLMetaElement { constructor(content){ this.content = content; } };
globalThis.Window = class Window {};
const meta = {
"nb-st": new HTMLMetaElement(process.argv[1]),
"nb-plt": new HTMLMetaElement(String(Date.now()))
};
const w = new Window();
w.document = { getElementById(id){ return meta[id] || null; } };
globalThis.window = w;
const mod = await import(process.argv[2]);
mod.initSync({ module: readFileSync(process.argv[3]) });
process.stdout.write(%s + mod.build_play_url(process.argv[4], process.argv[5], process.argv[6], process.argv[7]));
""" % json.dumps(self.host)
try:
result = subprocess.run(
[
"node",
"--input-type=module",
"-e",
script,
context["nbst"],
js_path.resolve().as_uri(),
str(wasm_path),
str(dataid or context["dataid"]),
str(play_slug),
str(quality),
context["userlink"],
],
capture_output=True,
text=True,
timeout=10,
check=False,
)
except Exception:
return ""
if result.returncode != 0:
return ""
return self._clean_text(result.stdout)
def _pick_nbmovie_play_url(self, api_payload):
data = api_payload.get("data", {}) if isinstance(api_payload, dict) else {}
quality_urls = data.get("quality_urls", []) if isinstance(data, dict) else []
if not isinstance(quality_urls, list) or not quality_urls:
return ""
candidates = []
current_quality = data.get("current_quality")
try:
current_index = int(current_quality)
except Exception:
current_index = -1
if 0 <= current_index < len(quality_urls):
candidates.append(quality_urls[current_index])
candidates.extend(quality_urls)
for item in candidates:
if not isinstance(item, dict):
continue
url = self._clean_text(item.get("url", ""))
if not url or url == "1":
continue
return self._normalize_url(url)
return ""
def playerContent(self, flag, id, vipFlags):
encoded = self._encode_site_path(id)
parsed = urlsplit(self._decode_site_path(encoded))
@@ -522,6 +672,27 @@ class Spider(BaseSpider):
}
html_text = self._request_html(page_url)
play_slug = base_id or encoded.split("?", 1)[0]
nbmovie_api_url = self._build_nbmovie_api_url(
play_page_html=html_text,
play_slug=play_slug,
dataid=self._extract_nbmovie_context(html_text, play_slug).get("dataid", ""),
quality="1080",
)
if nbmovie_api_url:
api_headers = dict(self.headers)
api_headers["Referer"] = page_url
api_headers["Accept"] = "application/json,text/plain,*/*"
response = self.fetch(nbmovie_api_url, headers=api_headers, timeout=10, verify=False)
if response.status_code == 200:
play_url = self._pick_nbmovie_play_url(self._parse_json(response.text))
if play_url:
return {
"parse": 0 if self._is_direct_play_url(play_url) else 1,
"url": play_url,
"header": api_headers,
}
root = self._parse_html(html_text)
iframe = self._first_xpath_attr(
root,
+35
View File
@@ -229,6 +229,41 @@ class Test4KVMSpider(unittest.TestCase):
self.assertEqual(video_result["parse"], 0)
self.assertEqual(video_result["url"], "https://www.4kvm.org/stream.m3u8")
@patch.object(Spider, "fetch")
def test_player_content_uses_nbmovie_api_for_current_site_structure(self, mock_fetch):
page_html = """
<html>
<head>
<meta id="nb-st" content="1776604294700" />
</head>
<body>
<nav x-data="{isLoggedIn: false, userlink:'X1VaWUBdUgYJBg4FCgc7IUlfXUlc'}"></nav>
<link id="wasm-cfg" data-js="/static/wasm/nbmovie_wasm.426511b7.js" data-bg="/static/wasm/nbmovie_wasm_bg.d5d51939.wasm" />
<a href="/play/ch43qikut" dataid="33260" data-line="1" data-episode="1">1</a>
</body>
</html>
"""
api_json = """
{
"code": 200,
"data": {
"play_id": "33260",
"current_quality": 1,
"quality_urls": [
{"title": "流畅", "url": "1"},
{"title": "1080P", "url": "https://cdn.example.com/stream/master.m3u8"}
]
}
}
"""
self.spider._build_nbmovie_api_url = lambda **kwargs: "https://www.4kvm.org/video/play?signed=1"
mock_fetch.side_effect = [FakeResponse(page_html), FakeResponse(api_json)]
result = self.spider.playerContent("", "play/ch43qikut", [])
self.assertEqual(result["parse"], 0)
self.assertEqual(result["url"], "https://cdn.example.com/stream/master.m3u8")
if __name__ == "__main__":
unittest.main()