修复
This commit is contained in:
+171
@@ -1,7 +1,9 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qsl, quote, urljoin, urlsplit
|
from urllib.parse import parse_qsl, quote, urljoin, urlsplit
|
||||||
|
|
||||||
from lxml import html as lxml_html
|
from lxml import html as lxml_html
|
||||||
@@ -493,6 +495,154 @@ class Spider(BaseSpider):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
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):
|
def playerContent(self, flag, id, vipFlags):
|
||||||
encoded = self._encode_site_path(id)
|
encoded = self._encode_site_path(id)
|
||||||
parsed = urlsplit(self._decode_site_path(encoded))
|
parsed = urlsplit(self._decode_site_path(encoded))
|
||||||
@@ -522,6 +672,27 @@ class Spider(BaseSpider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
html_text = self._request_html(page_url)
|
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)
|
root = self._parse_html(html_text)
|
||||||
iframe = self._first_xpath_attr(
|
iframe = self._first_xpath_attr(
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -229,6 +229,41 @@ class Test4KVMSpider(unittest.TestCase):
|
|||||||
self.assertEqual(video_result["parse"], 0)
|
self.assertEqual(video_result["parse"], 0)
|
||||||
self.assertEqual(video_result["url"], "https://www.4kvm.org/stream.m3u8")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user