feat: add lumman detail parsing

This commit is contained in:
Harold
2026-04-24 17:33:15 +08:00
parent b2bba86c90
commit 535fcaea72
2 changed files with 119 additions and 0 deletions
+47
View File
@@ -26,6 +26,25 @@ SAMPLE_LIST_HTML = """
</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):
@@ -72,6 +91,34 @@ class TestLuManManSpider(unittest.TestCase):
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")
if __name__ == "__main__":
unittest.main()
+72
View File
@@ -52,6 +52,15 @@ class Spider(BaseSpider):
matched = re.search(r"(/vod/detail/[^?#]+\.html)", raw)
return matched.group(1).lstrip("/") if matched else raw.lstrip("/")
def _decode_vod_id(self, vod_id):
raw = str(vod_id or "").strip().lstrip("/")
return self._abs_url(raw)
def _encode_play_id(self, href):
raw = str(href or "").strip()
matched = re.search(r"(/vod/play/[^?#]+\.html)", raw)
return matched.group(1).lstrip("/") if matched else raw.lstrip("/")
def _clean_text(self, text):
return re.sub(r"\s+", " ", str(text or "")).strip()
@@ -112,3 +121,66 @@ class Spider(BaseSpider):
url = f"{self.host}/vod/search/page/{page}/wd/{quote(keyword)}.html"
items = self._parse_cards(self._get_html(url))
return {"page": page, "total": len(items), "list": items[:10] if quick else items}
def _parse_play_groups(self, root):
groups = []
tabs = root.xpath("//*[contains(@class,'module-tab-item') and contains(@class,'tab-item')]")
for index, tab in enumerate(tabs):
name = self._clean_text("".join(tab.xpath(".//text()")))
target = self._clean_text((tab.xpath("./@href") or [""])[0])
if not name:
continue
playlist = []
if target.startswith("#"):
playlist = root.xpath(f"//*[@id='{target[1:]}']")
if not playlist:
playlist = root.xpath(f"(//*[contains(@class,'module-player-list')])[{index + 1}]")
if not playlist:
continue
episodes = []
for anchor in playlist[0].xpath(".//a[@href]"):
ep_name = self._clean_text("".join(anchor.xpath(".//text()")))
ep_id = self._encode_play_id((anchor.xpath("./@href") or [""])[0])
if ep_name and ep_id:
episodes.append(f"{ep_name}${ep_id}")
if episodes:
groups.append((name, "#".join(episodes)))
if groups:
return groups
fallback = []
for anchor in root.xpath("//*[contains(@class,'module-player-list')]//a[@href]"):
ep_name = self._clean_text("".join(anchor.xpath(".//text()")))
ep_id = self._encode_play_id((anchor.xpath("./@href") or [""])[0])
if ep_name and ep_id:
fallback.append(f"{ep_name}${ep_id}")
if fallback:
return [("播放列表", "#".join(fallback))]
return []
def detailContent(self, ids):
raw_id = ids[0] if isinstance(ids, list) else ids
html = self._get_html(self._decode_vod_id(raw_id))
root = self.html(html)
if root is None:
return {"list": []}
groups = self._parse_play_groups(root)
remarks = [
self._clean_text("".join(node.xpath(".//text()")))
for node in root.xpath("//*[contains(@class,'video-info-items')]")
]
pic = (
root.xpath("//*[contains(@class,'module-item-pic')]//img[1]/@data-src")
or root.xpath("//*[contains(@class,'module-item-pic')]//img[1]/@src")
or [""]
)[0]
vod = {
"vod_id": str(raw_id),
"vod_name": self._clean_text("".join(root.xpath("//*[contains(@class,'page-title')][1]//text()"))),
"vod_pic": self._abs_url(pic),
"vod_content": self._clean_text("".join(root.xpath("//*[contains(@class,'video-info-content')][1]//text()"))),
"vod_remarks": " / ".join([value for value in remarks if value]),
"vod_play_from": "$$$".join(name for name, _ in groups),
"vod_play_url": "$$$".join(urls for _, urls in groups),
}
return {"list": [vod]}