Files
tvboxzt/py/tests/test_路漫漫.py
T

230 lines
11 KiB
Python

from base64 import b64encode
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("lumman_spider", str(ROOT / "路漫漫.py")).load_module()
Spider = MODULE.Spider
SAMPLE_LIST_HTML = """
<html><body>
<div class="video-img-box">
<a href="/vod/detail/1001.html">
<img class="lazyload" data-src="/upload/1001.jpg" />
<h6 class="title">海贼王</h6>
<div class="label">更新至1123集</div>
</a>
</div>
<div class="video-img-box">
<a href="/vod/detail/1002.html">
<img class="lazyload" src="https://img.example.com/1002.jpg" />
<h6 class="title">你的名字</h6>
<div class="label">全集</div>
</a>
</div>
</body></html>
"""
SAMPLE_DETAIL_HTML = """
<html><body>
<h1 class="page-title">进击的巨人</h1>
<div class="module-item-pic"><img class="lazyload" src="/upload/jjdr.jpg" /></div>
<div class="video-info-items">状态:已完结</div>
<div class="video-info-items">地区:日本</div>
<div class="video-info-content">人类与巨人的战斗。</div>
<a class="module-tab-item tab-item" href="#line1">在线播放</a>
<a class="module-tab-item tab-item" href="#line2">云播</a>
<div id="line1" class="module-player-list">
<a href="/vod/play/1001-1-1.html">第1集</a>
<a href="/vod/play/1001-1-2.html">第2集</a>
</div>
<div id="line2" class="module-player-list">
<a href="/vod/play/1001-2-1.html">HD</a>
</div>
</body></html>
"""
class TestLuManManSpider(unittest.TestCase):
def setUp(self):
Spider._instance = None
self.spider = Spider()
self.spider.init()
def test_home_content_returns_only_animation_classes(self):
result = self.spider.homeContent(False)
self.assertEqual(
result["class"],
[
{"type_id": "6", "type_name": "日本动漫"},
{"type_id": "7", "type_name": "国产动漫"},
{"type_id": "8", "type_name": "欧美动漫"},
{"type_id": "3", "type_name": "日本动画电影"},
{"type_id": "4", "type_name": "国产动画电影"},
{"type_id": "5", "type_name": "欧美动画电影"},
],
)
@patch.object(Spider, "_get_html")
def test_home_video_content_parses_cards(self, mock_html):
mock_html.return_value = SAMPLE_LIST_HTML
result = self.spider.homeVideoContent()
self.assertEqual(len(result["list"]), 2)
self.assertEqual(result["list"][0]["vod_id"], "vod/detail/1001.html")
self.assertEqual(result["list"][0]["vod_pic"], "https://www.lmm85.com/upload/1001.jpg")
self.assertEqual(result["list"][1]["vod_pic"], "https://img.example.com/1002.jpg")
@patch.object(Spider, "_get_html")
def test_category_content_builds_filtered_url(self, mock_html):
mock_html.return_value = SAMPLE_LIST_HTML
result = self.spider.categoryContent("6", "2", False, {"年代": "/year/2024", "排序": "/by/time"})
self.assertEqual(result["page"], 2)
self.assertEqual(len(result["list"]), 2)
mock_html.assert_called_with("https://www.lmm85.com/vod/show/id/6/year/2024/by/time/page/2.html")
@patch.object(Spider, "_get_html")
def test_search_content_uses_search_path(self, mock_html):
mock_html.return_value = SAMPLE_LIST_HTML
result = self.spider.searchContent("海贼", False, "3")
self.assertEqual(result["page"], 3)
self.assertEqual(result["list"][0]["vod_name"], "海贼王")
mock_html.assert_called_with("https://www.lmm85.com/vod/search/page/3/wd/%E6%B5%B7%E8%B4%BC.html")
@patch.object(Spider, "_get_html")
def test_detail_content_parses_meta_and_playlists(self, mock_html):
mock_html.return_value = SAMPLE_DETAIL_HTML
result = self.spider.detailContent(["vod/detail/1001.html"])
vod = result["list"][0]
self.assertEqual(vod["vod_name"], "进击的巨人")
self.assertEqual(vod["vod_pic"], "https://www.lmm85.com/upload/jjdr.jpg")
self.assertEqual(vod["vod_content"], "人类与巨人的战斗。")
self.assertEqual(vod["vod_remarks"], "状态:已完结 / 地区:日本")
self.assertEqual(vod["vod_play_from"], "在线播放$$$云播")
self.assertIn("第1集$vod/play/1001-1-1.html#第2集$vod/play/1001-1-2.html", vod["vod_play_url"])
self.assertIn("HD$vod/play/1001-2-1.html", vod["vod_play_url"])
@patch.object(Spider, "_get_html")
def test_detail_content_falls_back_to_direct_playlist_scan(self, mock_html):
mock_html.return_value = """
<html><body>
<h1 class="page-title">测试</h1>
<div class="module-player-list">
<a href="/vod/play/1-1-1.html">正片</a>
</div>
</body></html>
"""
result = self.spider.detailContent(["vod/detail/1.html"])
vod = result["list"][0]
self.assertEqual(vod["vod_play_from"], "播放列表")
self.assertEqual(vod["vod_play_url"], "正片$vod/play/1-1-1.html")
def test_decrypt_token_returns_plaintext(self):
key = b"ejjooopppqqqrwww"
iv = b"1348987635684651"
cipher = AES.new(key, AES.MODE_CBC, iv)
token = b64encode(cipher.encrypt(pad(b"plain-token", AES.block_size))).decode("utf-8")
self.assertEqual(self.spider._decrypt_token(token), "plain-token")
def test_decrypt_token_handles_bad_ciphertext(self):
self.assertEqual(self.spider._decrypt_token("not-valid"), "")
@patch.object(Spider, "_resolve_player_url")
def test_player_content_returns_direct_media_without_parse(self, mock_resolve):
result = self.spider.playerContent("在线播放", "https://cdn.example.com/video.m3u8", {})
self.assertEqual(result["parse"], 0)
self.assertEqual(result["url"], "https://cdn.example.com/video.m3u8")
mock_resolve.assert_not_called()
@patch.object(Spider, "_resolve_player_url")
@patch.object(Spider, "_get_html")
def test_player_content_decodes_encrypt_1_and_encrypt_2(self, mock_html, mock_resolve):
encoded = b64encode("https://cdn.example.com/e2.m3u8".encode("utf-8")).decode("utf-8")
mock_html.side_effect = [
'<script>var player_aaaa={"url":"https%3A%2F%2Fcdn.example.com%2Fe1.m3u8","encrypt":"1","from":"line"};</script>',
f'<script>var player_aaaa={{"url":"{encoded}","encrypt":"2","from":"line"}};</script>',
]
mock_resolve.side_effect = ["https://cdn.example.com/e1.m3u8", "https://cdn.example.com/e2.m3u8"]
first = self.spider.playerContent("在线播放", "vod/play/1001-1-1.html", {})
second = self.spider.playerContent("在线播放", "vod/play/1001-1-2.html", {})
self.assertEqual(first["url"], "https://cdn.example.com/e1.m3u8")
self.assertEqual(second["url"], "https://cdn.example.com/e2.m3u8")
@patch.object(Spider, "_resolve_player_url", return_value="")
@patch.object(Spider, "_get_html", return_value="<html><body>missing player</body></html>")
def test_player_content_falls_back_to_parse_when_player_missing(self, mock_html, mock_resolve):
result = self.spider.playerContent("在线播放", "vod/play/1001-1-1.html", {})
self.assertEqual(result["parse"], 1)
self.assertEqual(result["url"], "https://www.lmm85.com/vod/play/1001-1-1.html")
@patch.object(Spider, "_post_json")
@patch.object(Spider, "_get_html")
def test_resolve_player_url_requests_parser_api_with_decrypted_token(self, mock_html, mock_post_json):
player = {"url": "source-id", "encrypt": "0", "from": "lineA"}
mock_html.side_effect = [
'document.querySelector("iframe").src = MacPlayer.Parse + MacPlayer.PlayUrl + "&type=m3u8";',
'<script>var vid="vid-1";var t="123";var token="YxF6M7yw8U7OQcW9t6Qx4w==";var act="play";var play="1";post("/api.php",{});</script>',
]
mock_post_json.return_value = {"url": "https://cdn.example.com/final.m3u8"}
with patch.object(self.spider, "_decrypt_token", return_value="decoded-token") as mock_decrypt:
result = self.spider._resolve_player_url(player, "https://www.lmm85.com/vod/play/1001-1-1.html")
self.assertEqual(result, "https://cdn.example.com/final.m3u8")
mock_decrypt.assert_called_once_with("YxF6M7yw8U7OQcW9t6Qx4w==")
mock_post_json.assert_called_once_with(
"https://www.lmm85.com/api.php",
{"vid": "vid-1", "t": "123", "token": "decoded-token", "act": "play", "play": "1"},
referer="https://www.lmm85.com",
)
@patch.object(Spider, "_post_json", return_value={})
@patch.object(Spider, "_get_html", return_value='document.querySelector("iframe").src = "";')
def test_resolve_player_url_returns_empty_when_chain_cannot_be_built(self, mock_html, mock_post_json):
result = self.spider._resolve_player_url(
{"url": "vid", "encrypt": "0", "from": "lineA"},
"https://www.lmm85.com/vod/play/1.html",
)
self.assertEqual(result, "")
@patch.object(Spider, "_post_json")
@patch.object(Spider, "_get_html")
def test_resolve_player_url_supports_tudou_yunbox_flow(self, mock_html, mock_post_json):
player = {"url": "DU_token_value&t=DU", "encrypt": "0", "from": "tudou"}
mock_html.side_effect = [
"""
MacPlayer.Html = '<iframe id="playerCnt" width="100%" frameborder="0" allowfullscreen src="" ></iframe>';
document.getElementById('playerCnt').src = 'https://yun.92cj.com/acfun58.php?id=' + MacPlayer.Parse + MacPlayer.PlayUrl + '&referer='+ window.location.href;
""",
"""
<script>
var vid = "DU_token_value";
var t = "1777023867";
var token = "od4gtx7Jiv1lFUXEm2N2JfveIFB/NwbUd/fdO3/XqbLN6kDh1wBhXVZk+Kruh4qB";
var act = "0";
var play = "1";
$.post("ducloud.php",{"vid":vid,"t":t,"token":getc(token),"act":act,"play":play},function(data){}, "json");
</script>
""",
]
mock_post_json.return_value = {"url": "https://video.example.com/final.m3u8"}
result = self.spider._resolve_player_url(player, "https://www.lmm85.com/play/7541_2_1.html")
self.assertEqual(result, "https://video.example.com/final.m3u8")
mock_post_json.assert_called_once_with(
"https://yun.92cj.com/yunbox/ducloud.php",
{
"vid": "DU_token_value",
"t": "1777023867",
"token": "659d24991db8fb7e2750e30f7afb3107",
"act": "0",
"play": "1",
},
referer="https://yun.92cj.com/yunbox/?type=DU&vid=DU_token_value&referer=https://www.lmm85.com/play/7541_2_1.html",
)
if __name__ == "__main__":
unittest.main()