diff --git a/py/tests/test_茶杯狐.py b/py/tests/test_茶杯狐.py index 92ff381..3b5d483 100644 --- a/py/tests/test_茶杯狐.py +++ b/py/tests/test_茶杯狐.py @@ -33,6 +33,14 @@ class TestCupfoxSpider(unittest.TestCase): self.spider._decode_play_id("play/abc123"), "https://www.cupfox.ai/play/abc123.html", ) + self.assertEqual( + self.spider._encode_detail_id("/video/119983.html"), + "detail/video/119983", + ) + self.assertEqual( + self.spider._decode_detail_id("detail/video/119983"), + "https://www.cupfox.ai/video/119983.html", + ) def test_merge_set_cookie_and_cookie_header(self): jar = {} @@ -83,6 +91,32 @@ class TestCupfoxSpider(unittest.TestCase): self.assertEqual(len(calls), 3) self.assertEqual(calls[2]["headers"]["Cookie"], "session=abc; shield=passed") + def test_request_with_firewall_accepts_capitalized_set_cookie_header(self): + calls = [] + + def fake_request(url, method="GET", body=None, headers=None): + calls.append({"url": url, "method": method, "body": body, "headers": headers or {}}) + if len(calls) == 1: + return { + "status_code": 200, + "text": '
', + "headers": {"Set-Cookie": "PHPSESSID=abc123; path=/"}, + } + if "robot.php" in url: + self.assertEqual(headers["Cookie"], "PHPSESSID=abc123") + return {"status_code": 200, "text": '{"msg":"ok"}', "headers": {}} + return { + "status_code": 200, + "text": "ok", + "headers": {}, + } + + self.spider._request_text = fake_request + html = self.spider._request_with_firewall("https://www.cupfox.ai/type/1-2.html") + + self.assertEqual(html, "ok") + self.assertEqual(calls[2]["headers"]["Cookie"], "PHPSESSID=abc123") + def test_extract_player_data_reads_embedded_json(self): html = '' data = self.spider._extract_player_data(html) @@ -159,6 +193,45 @@ class TestCupfoxSpider(unittest.TestCase): self.assertEqual(search["list"][0]["vod_remarks"], "搜索备注") self.assertNotIn("pagecount", search) + def test_category_content_uses_plain_type_path_for_first_page(self): + seen = [] + + def fake_request(url): + seen.append(url) + return """ +
+ + +
+ """ + + self.spider._request_with_firewall = fake_request + self.spider.categoryContent("1", "1", False, {}) + + self.assertEqual(seen, ["https://www.cupfox.ai/type/1.html"]) + + def test_parse_cards_supports_video_detail_links(self): + html = """ +
+ +
+ 正片 +
+ """ + items = self.spider._parse_cards(html) + + self.assertEqual( + items, + [ + { + "vod_id": "detail/video/119983", + "vod_name": "挽救计划", + "vod_pic": "https://www.cupfox.ai/poster.jpg", + "vod_remarks": "正片", + } + ], + ) + def test_detail_content_builds_play_sources(self): html = """

详情标题

diff --git a/py/茶杯狐.py b/py/茶杯狐.py index 4545a23..af8d5b4 100644 --- a/py/茶杯狐.py +++ b/py/茶杯狐.py @@ -39,12 +39,21 @@ class Spider(BaseSpider): return urljoin(self.host + "/", str(path or "").strip()) def _encode_detail_id(self, href): - matched = re.search(r"/movie/([^/?#]+)\.html", self._build_url(href)) - return f"detail/{matched.group(1)}" if matched else "" + movie_match = re.search(r"/movie/([^/?#]+)\.html", self._build_url(href)) + if movie_match: + return f"detail/{movie_match.group(1)}" + video_match = re.search(r"/video/([^/?#]+)\.html", self._build_url(href)) + if video_match: + return f"detail/video/{video_match.group(1)}" + return "" def _decode_detail_id(self, vod_id): - matched = re.search(r"^detail/([^/?#]+)$", str(vod_id or "").strip()) - return self._build_url(f"/movie/{matched.group(1)}.html") if matched else "" + raw = str(vod_id or "").strip() + video_match = re.search(r"^detail/video/([^/?#]+)$", raw) + if video_match: + return self._build_url(f"/video/{video_match.group(1)}.html") + movie_match = re.search(r"^detail/(?:movie/)?([^/?#]+)$", raw) + return self._build_url(f"/movie/{movie_match.group(1)}.html") if movie_match else "" def _encode_play_id(self, href): matched = re.search(r"/play/([^/?#]+)\.html", self._build_url(href)) @@ -67,6 +76,15 @@ class Spider(BaseSpider): def _cookie_header(self, cookie_jar): return "; ".join([f"{key}={value}" for key, value in cookie_jar.items()]) + def _header_value(self, headers, name, default=None): + if not isinstance(headers, dict): + return default + lowered = str(name or "").lower() + for key, value in headers.items(): + if str(key).lower() == lowered: + return value + return default + def _extract_firewall_token(self, html_text): matched = re.search(r'var\s+token\s*=\s*encrypt\("([^"]+)"\)', str(html_text or "")) return matched.group(1) if matched else "" @@ -94,13 +112,13 @@ class Spider(BaseSpider): return { "status_code": response.status_code, "text": response.text or "", - "headers": dict(response.headers or {}), + "headers": {str(key).lower(): value for key, value in dict(response.headers or {}).items()}, } def _request_with_firewall(self, url): cookie_jar = {} first = self._request_text(url) - self._merge_set_cookie(cookie_jar, first["headers"].get("set-cookie", [])) + self._merge_set_cookie(cookie_jar, self._header_value(first["headers"], "set-cookie", [])) if not re.search(r"人机验证|verifyBox", first["text"] or ""): if int(first["status_code"] or 0) != 200: raise ValueError(f"HTTP {first['status_code']} @ {url}") @@ -130,7 +148,7 @@ class Spider(BaseSpider): body=verify_body, headers=verify_headers, ) - self._merge_set_cookie(cookie_jar, verify["headers"].get("set-cookie", [])) + self._merge_set_cookie(cookie_jar, self._header_value(verify["headers"], "set-cookie", [])) second_headers = {} solved_cookie = self._cookie_header(cookie_jar) @@ -256,7 +274,8 @@ class Spider(BaseSpider): def categoryContent(self, tid, pg, filter, extend): page = int(pg) - html = self._request_with_firewall(self._build_url(f"/type/{tid}-{page}.html")) + path = f"/type/{tid}.html" if page == 1 else f"/type/{tid}-{page}.html" + html = self._request_with_firewall(self._build_url(path)) items = self._parse_cards(html) return {"page": page, "limit": self.page_limit, "total": page * len(items), "list": items}