From 319acce28087dd9115b4eff3484055ece28edb9f Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Thu, 23 Apr 2026 18:24:46 +0800 Subject: [PATCH] feat: add butailing detail and player --- py/tests/test_不太灵.py | 63 ++++++++++++++++++++++++++++++++++ py/不太灵.py | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/py/tests/test_不太灵.py b/py/tests/test_不太灵.py index 2b2ac79..8e45eda 100644 --- a/py/tests/test_不太灵.py +++ b/py/tests/test_不太灵.py @@ -111,6 +111,69 @@ class TestBuTaiLingSpider(unittest.TestCase): self.assertEqual(result["total"], 2) self.assertEqual([item["vod_id"] for item in result["list"]], ["1", "2"]) + def test_detect_drive_type_by_url(self): + self.assertEqual(self.spider._detect_drive_type_by_url("https://pan.baidu.com/s/abc"), "baidu") + self.assertEqual(self.spider._detect_drive_type_by_url("https://pan.quark.cn/s/abc"), "quark") + self.assertEqual(self.spider._detect_drive_type_by_url("https://pan.xunlei.com/s/abc"), "xunlei") + self.assertEqual(self.spider._detect_drive_type_by_url("https://www.alipan.com/s/abc"), "aliyun") + self.assertEqual(self.spider._detect_drive_type_by_url("https://www.123pan.com/s/abc"), "a123") + self.assertEqual(self.spider._detect_drive_type_by_url("https://example.com/file"), "other") + + def test_extract_pan_sources_creates_independent_lines_and_ignores_magnet_groups(self): + detail = { + "title": "示例详情", + "movies_online_seed": { + "夸克": [ + {"seed_name": "夸克资源A", "link": "https://pan.quark.cn/s/q1"}, + {"seed_name": "夸克资源A", "link": "https://pan.quark.cn/s/q1"}, + ], + "百度网盘": [{"seed_name": "百度资源", "link": "https://pan.baidu.com/s/b1"}], + "未知": [{"seed_name": "未知资源", "link": "https://example.com/file"}], + }, + "ecca": {"WEB-4K": [{"zlink": "magnet:?xt=1"}]}, + } + play_from, play_url = self.spider._extract_pan_sources(detail) + self.assertEqual(play_from, "baidu#1$$$quark#1$$$other#1") + self.assertEqual( + play_url, + "baidu#1$https://pan.baidu.com/s/b1$$$quark#1$https://pan.quark.cn/s/q1$$$other#1$https://example.com/file", + ) + + @patch.object(Spider, "_request_api") + def test_detail_content_maps_metadata_and_pan_lines(self, mock_request_api): + mock_request_api.return_value = { + "doub_id": 88, + "title": "详情标题", + "image": "https://img.test/detail.jpg", + "ejs": "全 12 集", + "years": "2026", + "abstract": "这是一段剧情简介", + "performer": "演员甲/演员乙", + "director": "导演甲", + "production_area": "中国", + "movies_online_seed": { + "阿里": [{"seed_name": "阿里资源", "link": "https://www.alipan.com/s/ali1"}] + }, + } + result = self.spider.detailContent(["88"]) + vod = result["list"][0] + self.assertEqual(mock_request_api.call_args.args[0], "getVideoDetail") + self.assertEqual(mock_request_api.call_args.args[1], {"id": "88"}) + self.assertEqual(vod["vod_id"], "88") + self.assertEqual(vod["vod_name"], "详情标题") + self.assertEqual(vod["vod_play_from"], "aliyun#1") + self.assertEqual(vod["vod_play_url"], "aliyun#1$https://www.alipan.com/s/ali1") + + def test_player_content_passthroughs_share_links(self): + self.assertEqual( + self.spider.playerContent("quark#1", "https://pan.quark.cn/s/q1", {}), + {"parse": 0, "url": "https://pan.quark.cn/s/q1"}, + ) + self.assertEqual( + self.spider.playerContent("quark#1", "push://https://pan.quark.cn/s/q1", {}), + {"parse": 0, "url": "https://pan.quark.cn/s/q1"}, + ) + if __name__ == "__main__": unittest.main() diff --git a/py/不太灵.py b/py/不太灵.py index 2b8198e..f62cd9d 100644 --- a/py/不太灵.py +++ b/py/不太灵.py @@ -12,6 +12,8 @@ sys.path.append("..") class Spider(BaseSpider): + DRIVE_ORDER = {"baidu": 1, "quark": 2, "xunlei": 3, "aliyun": 4, "a123": 5, "other": 99} + def __init__(self): self.name = "不太灵" self.api_base = "https://web5.mukaku.com/prod/api/v1/" @@ -177,6 +179,62 @@ class Spider(BaseSpider): "iswp": self._to01(ext_obj.get("iswp"), 0), } + def _normalize_drive_key(self, value): + text = str(value or "").strip().lower() + if text in {"alipan", "aliyun", "aliyundrive", "ali", "阿里"}: + return "aliyun" + if "quark" in text or "夸克" in text: + return "quark" + if "baidu" in text or "百度" in text: + return "baidu" + if "xunlei" in text or "迅雷" in text: + return "xunlei" + if "123" in text: + return "a123" + return "other" + + def _detect_drive_type_by_url(self, url): + text = str(url or "").lower() + if "pan.quark.cn" in text: + return "quark" + if "pan.baidu.com" in text: + return "baidu" + if "pan.xunlei.com" in text: + return "xunlei" + if "alipan.com" in text or "aliyundrive.com" in text: + return "aliyun" + if "123865.com" in text or "123684.com" in text or "123pan.com" in text: + return "a123" + return "other" + + def _extract_pan_sources(self, detail): + groups = (detail or {}).get("movies_online_seed") + if not isinstance(groups, dict): + return "", "" + seen = set() + counters = {} + entries = [] + for group_name, items in groups.items(): + for item in items or []: + link = str((item or {}).get("link") or "").strip() + if not link or link in seen: + continue + seen.add(link) + kind = self._detect_drive_type_by_url(link) + if kind == "other": + kind = self._normalize_drive_key(group_name) + counters[kind] = counters.get(kind, 0) + 1 + line_name = f"{kind}#{counters[kind]}" + entries.append((line_name, link)) + entries.sort( + key=lambda pair: ( + self.DRIVE_ORDER.get(pair[0].split("#")[0], 999), + int(pair[0].split("#")[1]), + pair[0], + ) + ) + return "$$$".join(name for name, _ in entries), "$$$".join(f"{name}${url}" for name, url in entries) + def homeContent(self, filter): return {"class": self.classes, "filters": self._build_filters(self._request_api("getVideoTypeList", {}))} @@ -211,3 +269,20 @@ class Spider(BaseSpider): paged = self._paginate_items(self._dedupe_by_vod_id(matched), page, 20) paged["list"] = [self._normalize_video(item) for item in paged["list"]] return paged + + def detailContent(self, ids): + vod_id = str(ids[0] if isinstance(ids, list) and ids else ids or "").strip() + if not vod_id: + return {"list": []} + data = self._request_api("getVideoDetail", {"id": vod_id}) + if not isinstance(data, dict): + return {"list": []} + vod = self._normalize_video(data) + play_from, play_url = self._extract_pan_sources(data) + vod["vod_play_from"] = play_from + vod["vod_play_url"] = play_url + return {"list": [vod]} + + def playerContent(self, flag, id, vipFlags): + raw = str(id or "").strip() + return {"parse": 0, "url": raw[7:] if raw.startswith("push://") else raw}