From 5b5bc3951003fa96aa5a8cf9de6f0b25df4a853b Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Fri, 24 Apr 2026 18:40:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=E4=B8=A4=E4=B8=AABT=20detail=20p?= =?UTF-8?q?arsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- py/tests/test_两个BT.py | 35 ++++++++++++++++ py/两个BT.py | 89 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/py/tests/test_两个BT.py b/py/tests/test_两个BT.py index 24c6fae..3980333 100644 --- a/py/tests/test_两个BT.py +++ b/py/tests/test_两个BT.py @@ -134,6 +134,41 @@ class TestLiangGeBTSpider(unittest.TestCase): ], ) + def test_encode_and_decode_play_id_round_trip(self): + payload = self.spider._decode_play_id(self.spider._encode_play_id("play-1", "900", "第1集")) + self.assertEqual(payload, {"pid": "play-1", "sid": "900", "name": "第1集"}) + + @patch.object(Spider, "_request_html") + def test_detail_content_extracts_meta_and_playlist(self, mock_request_html): + mock_request_html.return_value = """ + + 两个BT详情页 + +

示例详情

+
+
这里是剧情简介
+
  • 主演:演员甲 / 演员乙
  • +
  • 导演:导演甲
  • + 第1集 + 第2集 + + + """ + result = self.spider.detailContent(["900"]) + 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/movie/900.html") + self.assertEqual(vod["vod_id"], "900") + self.assertEqual(vod["vod_name"], "示例详情") + self.assertEqual(vod["vod_pic"], "https://www.bttwoo.com/detail.jpg") + self.assertEqual(vod["vod_content"], "这里是剧情简介") + self.assertEqual(vod["vod_actor"], "演员甲 / 演员乙") + self.assertEqual(vod["vod_director"], "导演甲") + self.assertEqual(vod["vod_play_from"], "两个BT") + self.assertEqual(first_name, "第1集") + self.assertEqual(self.spider._decode_play_id(first_id)["pid"], "play-1") + self.assertEqual(self.spider._decode_play_id(first_id)["sid"], "900") + if __name__ == "__main__": unittest.main() diff --git a/py/两个BT.py b/py/两个BT.py index 4c2adce..af297ad 100644 --- a/py/两个BT.py +++ b/py/两个BT.py @@ -1,4 +1,6 @@ # coding=utf-8 +import base64 +import json import re import sys from urllib.parse import quote, urljoin @@ -69,7 +71,12 @@ class Spider(BaseSpider): return {"page": page, "limit": len(items), "total": len(items), "list": items} def detailContent(self, ids): - return {"list": []} + vod_id = str(ids[0] if isinstance(ids, list) and ids else ids or "").strip() + if not vod_id: + return {"list": []} + html = self._request_html(self.host + f"/movie/{vod_id}.html") + detail = self._parse_detail(html, vod_id) + return {"list": [detail]} if detail else {"list": []} def playerContent(self, flag, id, vipFlags): return {"parse": 1, "jx": 1, "playUrl": "", "url": "", "header": {}} @@ -151,6 +158,86 @@ class Spider(BaseSpider): matched = re.search(r"/movie/(\d+)\.html", str(href or "").strip()) return matched.group(1) if matched else "" + def _extract_play_pid(self, href): + matched = re.search(r"/v_play/([^.]+)\.html", str(href or "").strip()) + return matched.group(1) if matched else "" + + def _parse_detail(self, html, vod_id): + root = self._parse_html(html) + if root is None: + return None + + vod_name = ( + self._first_text(root, "//h1[1]") + or self._first_text(root, "//h2[1]") + or self._extract_title_text(html) + ) + vod_pic = ( + self._first_attr(root, "//img[contains(@class,'poster')][1]", "src") + or self._first_attr(root, "//*[contains(@class,'poster')]//img[1]", "src") + or self._first_attr(root, "//img[1]", "src") + ) + vod_content = ( + self._first_text(root, "//*[contains(@class,'intro')][1]") + or self._first_text(root, "//*[contains(@class,'description')][1]") + or self._first_text(root, "//*[contains(@class,'desc')][1]") + ) + vod_actor = self._extract_meta_text(root, "主演") + vod_director = self._extract_meta_text(root, "导演") + + episodes = [] + seen = set() + for index, node in enumerate(root.xpath("//a[contains(@href,'/v_play/')]")): + href = str(node.get("href") or "").strip() + pid = self._extract_play_pid(href) + name = self._clean_text(node.text_content()) or f"第{index + 1}集" + if not pid or pid in seen: + continue + seen.add(pid) + episodes.append(f"{name}${self._encode_play_id(pid, vod_id, name)}") + + if not episodes: + return None + + return { + "vod_id": vod_id, + "vod_name": vod_name or "未知标题", + "vod_pic": self._abs_url(vod_pic), + "vod_content": vod_content, + "vod_actor": vod_actor, + "vod_director": vod_director, + "vod_play_from": "两个BT", + "vod_play_url": "#".join(episodes), + } + + def _encode_play_id(self, pid, sid, name): + raw = json.dumps( + {"pid": str(pid or ""), "sid": str(sid or ""), "name": str(name or "")}, + ensure_ascii=False, + separators=(",", ":"), + ) + return base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + def _decode_play_id(self, value): + try: + raw = base64.b64decode(str(value or "").encode("utf-8")).decode("utf-8") + data = json.loads(raw) + except Exception: + return {"pid": "", "sid": "", "name": ""} + return { + "pid": str(data.get("pid") or ""), + "sid": str(data.get("sid") or ""), + "name": str(data.get("name") or ""), + } + + def _extract_title_text(self, html): + matched = re.search(r"(.*?)", str(html or ""), re.I | re.S) + return self._clean_text(matched.group(1)) if matched else "" + + def _extract_meta_text(self, root, label): + text = self._first_text(root, f"//*[contains(text(),'{label}')][1]") + return re.sub(rf"^{label}[::]?", "", text).strip() + def _abs_url(self, value): raw = str(value or "").strip() if not raw: