新韩剧网
This commit is contained in:
Harold
2026-04-24 19:21:45 +08:00
parent 5ca062b58e
commit bbdbb0a471
4 changed files with 933 additions and 37 deletions
+142 -19
View File
@@ -1,7 +1,7 @@
import unittest
from importlib.machinery import SourceFileLoader
from pathlib import Path
from unittest.mock import patch
from unittest.mock import MagicMock, patch
ROOT = Path(__file__).resolve().parents[1]
@@ -63,6 +63,31 @@ class TestLiangGeBTSpider(unittest.TestCase):
],
)
def test_extract_cards_supports_current_play_cards(self):
html = """
<div class="relative group movie-card" data-vod-id="ch42nt5hw">
<a href="/play/ch42nt5hw" class="block">
<div class="relative">
<img data-src="https://img.example/poster.jpg" alt="夜魔侠:重生 第二季" />
<span>更新至6集</span>
</div>
<h3>夜魔侠:重生 第二季</h3>
</a>
</div>
"""
cards = self.spider._extract_cards(html)
self.assertEqual(
cards,
[
{
"vod_id": "/play/ch42nt5hw",
"vod_name": "夜魔侠:重生 第二季",
"vod_pic": "https://img.example/poster.jpg",
"vod_remarks": "更新至6集",
}
],
)
@patch.object(Spider, "_request_html")
def test_home_video_content_requests_home_page(self, mock_request_html):
mock_request_html.return_value = """
@@ -80,53 +105,65 @@ class TestLiangGeBTSpider(unittest.TestCase):
@patch.object(Spider, "_request_html")
def test_category_content_builds_expected_urls(self, mock_request_html):
mock_request_html.return_value = """
<li>
<a href="/movie/301.html" title="分类影片">
<img src="/cate.jpg" />
<div class="movie-card" data-vod-id="ch42nt5hw">
<a href="/play/ch42nt5hw">
<img data-src="/cate.jpg" alt="分类影片" />
<span>更新至6集</span>
<h3>分类影片</h3>
</a>
<h3>分类影片</h3>
<span class="rating">HD</span>
</li>
</div>
"""
page_one = self.spider.categoryContent("meiju", "1", False, {})
page_three = self.spider.categoryContent("movie_bt_tags/xiju", "3", False, {})
self.assertEqual(mock_request_html.call_args_list[0].args[0], "https://www.bttwoo.com/meiju")
self.assertEqual(
mock_request_html.call_args_list[0].args[0],
"https://www.bttwoo.com/filter?classify=2&tvclasses=21",
)
self.assertEqual(
mock_request_html.call_args_list[1].args[0],
"https://www.bttwoo.com/movie_bt_tags/xiju?paged=3",
"https://www.bttwoo.com/filter?classify=1&types=5&page=3",
)
self.assertEqual(page_one["page"], 1)
self.assertEqual(page_one["list"][0]["vod_name"], "分类影片")
self.assertNotIn("pagecount", page_one)
self.assertEqual(page_three["page"], 3)
@patch.object(Spider, "_request_html")
def test_category_content_maps_high_score_movies(self, mock_request_html):
mock_request_html.return_value = ""
self.spider.categoryContent("gf", "2", False, {})
self.assertEqual(
mock_request_html.call_args.args[0],
"https://www.bttwoo.com/filter?classify=1&sort_by=score&order=desc&page=2",
)
@patch.object(Spider, "_request_html")
def test_search_content_builds_query_and_filters_irrelevant_results(self, mock_request_html):
mock_request_html.return_value = """
<li>
<a href="/movie/401.html" title="繁花">
<div class="movie-card" data-vod-id="ch-search-1">
<a href="/play/ch-search-1" title="繁花">
<img src="/match.jpg" />
<h3>繁花</h3>
</a>
<h3>繁花</h3>
</li>
<li>
<a href="/movie/402.html" title="无关结果">
</div>
<div class="movie-card" data-vod-id="ch-search-2">
<a href="/play/ch-search-2" title="无关结果">
<img src="/other.jpg" />
<h3>无关结果</h3>
</a>
<h3>无关结果</h3>
</li>
</div>
"""
result = self.spider.searchContent("繁花", False, "2")
self.assertEqual(
mock_request_html.call_args.args[0],
"https://www.bttwoo.com/xssssearch?q=%E7%B9%81%E8%8A%B1&p=2",
"https://www.bttwoo.com/search?q=%E7%B9%81%E8%8A%B1&page=2",
)
self.assertEqual(result["page"], 2)
self.assertEqual(
result["list"],
[
{
"vod_id": "401",
"vod_id": "/play/ch-search-1",
"vod_name": "繁花",
"vod_pic": "https://www.bttwoo.com/match.jpg",
"vod_remarks": "",
@@ -169,6 +206,45 @@ class TestLiangGeBTSpider(unittest.TestCase):
self.assertEqual(self.spider._decode_play_id(first_id)["pid"], "play-1")
self.assertEqual(self.spider._decode_play_id(first_id)["sid"], "900")
@patch.object(Spider, "_request_html")
def test_detail_content_supports_current_play_pages(self, mock_request_html):
mock_request_html.return_value = """
<html>
<head>
<title>夜魔侠:重生 第二季 - 第1集 -两个BT影视</title>
<meta name="description" content="夜幕降临,魔影共舞。" />
<meta property="og:image" content="https://img.example/poster.jpg" />
</head>
<body>
<div class="movie-poster">
<img src="https://img.example/poster.jpg" />
<h1>夜魔侠:重生 第二季</h1>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div>导演</div><div>贾斯汀·本森 / 艾伦·穆尔黑德</div>
<div>主演</div><div>查理·考克斯 / 文森特·多诺费奥</div>
</div>
<p>夜幕降临,魔影共舞。</p>
<div x-data="episodeManager(1, 1, [{ lineName: '线路一', episodeCount: 2 }])">
<a href="/play/ch42nt5hw">1</a>
<a href="/play/ch42nt5ic">2</a>
</div>
</body>
</html>
"""
result = self.spider.detailContent(["/play/ch42nt5hw"])
vod = result["list"][0]
first_name, first_id = vod["vod_play_url"].split("#")[0].split("$", 1)
self.assertEqual(mock_request_html.call_args.args[0], "https://www.bttwoo.com/play/ch42nt5hw")
self.assertEqual(vod["vod_id"], "/play/ch42nt5hw")
self.assertEqual(vod["vod_name"], "夜魔侠:重生 第二季")
self.assertEqual(vod["vod_pic"], "https://img.example/poster.jpg")
self.assertEqual(vod["vod_content"], "夜幕降临,魔影共舞。")
self.assertEqual(vod["vod_actor"], "查理·考克斯 / 文森特·多诺费奥")
self.assertEqual(vod["vod_director"], "贾斯汀·本森 / 艾伦·穆尔黑德")
self.assertEqual(first_name, "1")
self.assertEqual(self.spider._decode_play_id(first_id)["pid"], "/play/ch42nt5hw")
def test_player_content_passthroughs_direct_media_url(self):
result = self.spider.playerContent("两个BT", "https://media.example/direct.m3u8", {})
self.assertEqual(result["parse"], 0)
@@ -176,6 +252,53 @@ class TestLiangGeBTSpider(unittest.TestCase):
self.assertEqual(result["url"], "https://media.example/direct.m3u8")
self.assertEqual(result["header"]["Referer"], "https://www.bttwoo.com/")
@patch.object(Spider, "_request_html")
def test_player_content_supports_current_play_page_ids(self, mock_request_html):
mock_request_html.return_value = "<html><body>empty</body></html>"
play_id = self.spider._encode_play_id("/play/ch42nt5hw", "/play/ch42nt5hw", "1")
result = self.spider.playerContent("两个BT", play_id, {})
self.assertEqual(mock_request_html.call_args.args[0], "https://www.bttwoo.com/play/ch42nt5hw")
self.assertEqual(result["parse"], 1)
self.assertEqual(result["jx"], 1)
self.assertEqual(result["url"], "https://www.bttwoo.com/play/ch42nt5hw")
@patch.object(Spider, "fetch")
@patch.object(Spider, "_build_wasm_play_api_url")
@patch.object(Spider, "_cache_wasm_assets")
@patch.object(Spider, "_request_html")
def test_player_content_resolves_current_play_page_via_play_api(
self, mock_request_html, mock_cache_wasm_assets, mock_build_wasm_play_api_url, mock_fetch
):
mock_request_html.return_value = """
<html>
<body>
<nav x-data="{isLoggedIn: false, userlink:'X1VaWEZeVwEFCw4FCgc7IUlfXUhe'}"></nav>
<div id="player-error-message-detail" data-v-id="4810"></div>
<div x-data="episodeManager(1, 1, [{ lineName: 'alists', episodeCount: 22 }])">
<a href="/play/ch440i68t" data-line="1" data-episode="1" dataid="33373">1</a>
</div>
</body>
</html>
"""
mock_build_wasm_play_api_url.return_value = "https://www.bttwoo.com/video/play?p=33373"
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = (
'{"code":200,"data":{"quality_urls":[{"url":"https://media.example/final.m3u8"}],"current_quality":0}}'
)
mock_fetch.return_value = mock_response
play_id = self.spider._encode_play_id("/play/ch440i68t", "/play/ch440i68t", "1")
result = self.spider.playerContent("两个BT", play_id, {})
self.assertEqual(mock_request_html.call_args.args[0], "https://www.bttwoo.com/play/ch440i68t")
self.assertEqual(
mock_build_wasm_play_api_url.call_args.args,
("33373", "ch440i68t", "1080", "X1VaWEZeVwEFCw4FCgc7IUlfXUhe"),
)
self.assertEqual(result["parse"], 0)
self.assertEqual(result["jx"], 0)
self.assertEqual(result["url"], "https://media.example/final.m3u8")
self.assertEqual(result["header"]["Referer"], "https://www.bttwoo.com/play/ch440i68t")
@patch.object(Spider, "_request_html")
def test_player_content_extracts_media_from_play_page(self, mock_request_html):
mock_request_html.return_value = """
+235
View File
@@ -0,0 +1,235 @@
import base64
import unittest
from importlib.machinery import SourceFileLoader
from pathlib import Path
from unittest.mock import patch
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
ROOT = Path(__file__).resolve().parents[1]
MODULE = SourceFileLoader("hanju7_spider", str(ROOT / "新韩剧网.py")).load_module()
Spider = MODULE.Spider
def encrypt_player_payload(url):
iv = b"0123456789abcdef"
key = MODULE.PLAYER_AES_KEY.encode("utf-8")
cipher = AES.new(key, AES.MODE_CBC, iv)
body = cipher.encrypt(pad(url.encode("utf-8"), AES.block_size))
return base64.b64encode(iv + body).decode("utf-8")
HOME_HTML = """
<div class="list">
<ul>
<li>
<a href="/detail/abc123.html" title="苦尽柑来遇见你" data-original="//img.example.com/a.jpg">苦尽柑来遇见你</a>
<span>更新至第2集</span>
</li>
<li>
<a href="/detail/def456.html" data-original="/upload/b.jpg">机智住院医生生活</a>
<span>2026-04-24</span>
</li>
</ul>
</div>
"""
LIST_HTML = """
<div class="list">
<ul>
<li>
<a class="tu" href="/detail/xyz001.html" title="电影A" data-original="//img.example.com/m1.jpg"></a>
<span class="tip">HD</span>
</li>
<li>
<a class="tu" href="/detail/xyz002.html" title="电影B"></a>
<span class="tip">完结</span>
</li>
</ul>
</div>
"""
SEARCH_HTML = """
<div class="txt">
<ul>
<li id="t">header</li>
<li>
<p id="name"><a href="/detail/search001.html">新乌托邦(12)</a></p>
<p id="actor">朴正民 / 金智秀</p>
</li>
<li>
<p id="name"><a href="/detail/search002.html">协商的技术</a></p>
<p id="actor">李帝勋</p>
</li>
</ul>
</div>
"""
DETAIL_HTML = """
<div class="detail">
<div class="pic"><img data-original="//img.example.com/poster.jpg" /></div>
<div class="info">
<dl><dd>比天堂还美丽</dd></dl>
<dl><dd>金惠子 / 孙锡久</dd></dl>
<dl><dd>韩国</dd></dl>
<dl><dd>剧情</dd></dl>
<dl><dd>更新至第3集</dd></dl>
<dl><dd>2026</dd></dl>
</div>
</div>
<div class="juqing">一段剧情简介</div>
<div id="playlist">新韩剧线路</div>
<div class="play">
<ul>
<li><a onclick="bf('p001')">第1集</a></li>
<li><a onclick="bf('p002')">第2集</a></li>
</ul>
</div>
"""
HOT_HTML = """
<div class="txt">
<ul>
<li><a href="/detail/h1.html" title="热播1">热播1</a><span>Top1</span></li>
<li><a href="/detail/h2.html" title="热播2">热播2</a><span>Top2</span></li>
<li><a href="/detail/h3.html" title="热播3">热播3</a><span>Top3</span></li>
</ul>
</div>
"""
class FakeResponse:
def __init__(self, text="", status_code=200, headers=None):
self.text = text
self.status_code = status_code
self.headers = headers or {}
self.encoding = "utf-8"
class TestHanJu7Spider(unittest.TestCase):
def setUp(self):
Spider._instance = None
self.spider = Spider()
self.spider.init()
def test_home_content_returns_expected_classes(self):
content = self.spider.homeContent(False)
self.assertEqual(
[item["type_id"] for item in content["class"]],
["1", "3", "4", "hot", "new"],
)
@patch.object(Spider, "_request_html")
def test_home_video_content_parses_list_cards(self, mock_request_html):
mock_request_html.return_value = HOME_HTML
result = self.spider.homeVideoContent()
self.assertEqual(
result["list"],
[
{
"vod_id": "abc123",
"vod_name": "苦尽柑来遇见你",
"vod_pic": "https://img.example.com/a.jpg",
"vod_remarks": "更新至第2集",
},
{
"vod_id": "def456",
"vod_name": "机智住院医生生活",
"vod_pic": "https://www.hanju7.com/upload/b.jpg",
"vod_remarks": "2026-04-24",
},
],
)
@patch.object(Spider, "_request_html")
def test_category_content_uses_list_path_for_regular_category(self, mock_request_html):
mock_request_html.return_value = LIST_HTML
result = self.spider.categoryContent("3", "2", False, {})
self.assertEqual(mock_request_html.call_args.args[0], "https://www.hanju7.com/list/3---1.html")
self.assertEqual(result["page"], 2)
self.assertEqual(result["list"][0]["vod_id"], "xyz001")
self.assertEqual(result["list"][1]["vod_pic"], "https://pics.hanju7.com/pics/xyz002.jpg")
self.assertNotIn("pagecount", result)
@patch.object(Spider, "_request_html")
def test_category_content_paginates_hot_locally(self, mock_request_html):
mock_request_html.return_value = HOT_HTML
result = self.spider.categoryContent("hot", "1", False, {})
self.assertEqual(mock_request_html.call_args.args[0], "https://www.hanju7.com/hot.html")
self.assertEqual(result["page"], 1)
self.assertEqual(result["total"], 3)
self.assertEqual(result["list"][0]["vod_id"], "h1")
def test_extract_redirect_location_from_native_search_response(self):
response = FakeResponse(status_code=302, headers={"Location": "/search.php?searchword=test"})
self.assertEqual(self.spider._extract_redirect_location(response), "/search.php?searchword=test")
@patch.object(Spider, "_request_html")
@patch.object(Spider, "_native_post_search")
def test_search_content_follows_redirect_and_parses_results(self, mock_native_search, mock_request_html):
mock_native_search.return_value = ("/search.php?page=1&searchword=%E6%96%B0%E4%B9%8C%E6%89%98%E9%82%A6", "k=v")
mock_request_html.return_value = SEARCH_HTML
result = self.spider.searchContent("新乌托邦", False, "1")
self.assertEqual(result["page"], 1)
self.assertEqual(mock_request_html.call_args.args[0], "https://www.hanju7.com/search.php?page=1&searchword=%E6%96%B0%E4%B9%8C%E6%89%98%E9%82%A6")
self.assertEqual(mock_request_html.call_args.kwargs["headers"]["Cookie"], "k=v")
self.assertEqual(
result["list"],
[
{
"vod_id": "search001",
"vod_name": "新乌托邦",
"vod_pic": MODULE.DEFAULT_PIC,
"vod_remarks": "朴正民 / 金智秀",
},
{
"vod_id": "search002",
"vod_name": "协商的技术",
"vod_pic": MODULE.DEFAULT_PIC,
"vod_remarks": "李帝勋",
},
],
)
@patch.object(Spider, "_request_html")
def test_detail_content_parses_metadata_and_playlists(self, mock_request_html):
mock_request_html.return_value = DETAIL_HTML
result = self.spider.detailContent(["abc123"])
vod = result["list"][0]
self.assertEqual(mock_request_html.call_args.args[0], "https://www.hanju7.com/detail/abc123.html")
self.assertEqual(vod["vod_name"], "比天堂还美丽")
self.assertEqual(vod["vod_pic"], "https://img.example.com/poster.jpg")
self.assertEqual(vod["vod_actor"], "金惠子 / 孙锡久")
self.assertEqual(vod["vod_remarks"], "更新至第3集")
self.assertEqual(vod["vod_year"], "2026")
self.assertEqual(vod["vod_content"], "一段剧情简介")
self.assertEqual(vod["vod_play_from"], "新韩剧线路")
self.assertEqual(vod["vod_play_url"], "第1集$p001#第2集$p002")
def test_decrypt_play_url_decodes_prefixed_iv_payload(self):
encrypted = encrypt_player_payload("https://cdn.example.com/final.m3u8")
self.assertEqual(self.spider._decrypt_play_url(encrypted), "https://cdn.example.com/final.m3u8")
@patch.object(Spider, "_request_text")
def test_player_content_returns_direct_media(self, mock_request_text):
mock_request_text.return_value = encrypt_player_payload("https://cdn.example.com/final.m3u8")
result = self.spider.playerContent("新韩剧线路", "p001", {})
self.assertEqual(mock_request_text.call_args.args[0], "https://www.hanju7.com/u/u1.php?ud=p001")
self.assertEqual(result["parse"], 0)
self.assertEqual(result["jx"], 0)
self.assertEqual(result["url"], "https://cdn.example.com/final.m3u8")
self.assertEqual(result["header"]["Referer"], "https://www.hanju7.com/")
@patch.object(Spider, "_request_text")
def test_player_content_falls_back_to_parser_for_embed_url(self, mock_request_text):
mock_request_text.return_value = encrypt_player_payload("https://player.example.com/embed?id=1")
result = self.spider.playerContent("新韩剧线路", "p002", {})
self.assertEqual(result["parse"], 1)
self.assertEqual(result["jx"], 1)
self.assertEqual(result["url"], "https://player.example.com/embed?id=1")
if __name__ == "__main__":
unittest.main()