From 535fcaea7223edf62496acd59d5ad34d3a70a4ad Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Fri, 24 Apr 2026 17:33:15 +0800 Subject: [PATCH] feat: add lumman detail parsing --- py/tests/test_路漫漫.py | 47 +++++++++++++++++++++++++++ py/路漫漫.py | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/py/tests/test_路漫漫.py b/py/tests/test_路漫漫.py index bd8f09a..e139697 100644 --- a/py/tests/test_路漫漫.py +++ b/py/tests/test_路漫漫.py @@ -26,6 +26,25 @@ SAMPLE_LIST_HTML = """ """ +SAMPLE_DETAIL_HTML = """ + +

进击的巨人

+
+
状态:已完结
+
地区:日本
+
人类与巨人的战斗。
+在线播放 +云播 +
+ 第1集 + 第2集 +
+
+ HD +
+ +""" + 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 = """ + +

测试

+
+ 正片 +
+ + """ + 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() diff --git a/py/路漫漫.py b/py/路漫漫.py index cd49d9b..0a1c69e 100644 --- a/py/路漫漫.py +++ b/py/路漫漫.py @@ -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]}