两个BT
新韩剧网
This commit is contained in:
+142
-19
@@ -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 = """
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user