红果短剧
This commit is contained in:
+47
-21
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -17,7 +17,10 @@ class TestHongGuoDuanJuSpider(unittest.TestCase):
|
||||
|
||||
def test_home_content_exposes_categories(self):
|
||||
content = self.spider.homeContent(False)
|
||||
self.assertEqual(content["class"][0]["type_id"], "热播")
|
||||
ids = [item["type_id"] for item in content["class"]]
|
||||
self.assertEqual(content["class"][0]["type_id"], "逆袭")
|
||||
self.assertIn("现代言情", ids)
|
||||
self.assertNotIn("热播", ids)
|
||||
self.assertIn("分类", content["filters"])
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
@@ -37,14 +40,18 @@ class TestHongGuoDuanJuSpider(unittest.TestCase):
|
||||
def test_category_content_builds_query_and_maps_items(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"data": [
|
||||
{"id": "2", "title": "新剧回归", "cover": "http://img/2.jpg", "totalChapterNum": 36}
|
||||
{"book_id": "b2", "title": "新剧回归", "cover": "http://img/2.jpg", "sub_title": "36集"}
|
||||
]
|
||||
}
|
||||
result = self.spider.categoryContent("新剧", "3", False, {})
|
||||
result = self.spider.categoryContent("穿越", "3", False, {})
|
||||
self.assertEqual(result["page"], 3)
|
||||
self.assertEqual(result["list"][0]["vod_id"], "2")
|
||||
self.assertEqual(result["list"][0]["vod_id"], "b2")
|
||||
self.assertEqual(result["list"][0]["vod_remarks"], "36集")
|
||||
self.assertNotIn("pagecount", result)
|
||||
self.assertIn("name=%E6%96%B0%E5%89%A7&page=3", mock_json.call_args.args[0])
|
||||
self.assertIn("/api/duanju/api.php?", mock_json.call_args.args[0])
|
||||
self.assertIn("classname=%E7%A9%BF%E8%B6%8A", mock_json.call_args.args[0])
|
||||
self.assertIn("offset=40", mock_json.call_args.args[0])
|
||||
self.assertIn("showRawParams=false", mock_json.call_args.args[0])
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_search_content_uses_keyword_query(self, mock_json):
|
||||
@@ -63,6 +70,12 @@ class TestHongGuoDuanJuSpider(unittest.TestCase):
|
||||
self.assertEqual(result["list"], [])
|
||||
mock_json.assert_not_called()
|
||||
|
||||
@patch.object(Spider, "fetch")
|
||||
def test_category_content_returns_empty_for_invalid_json_response(self, mock_fetch):
|
||||
mock_fetch.return_value = MagicMock(status_code=200, text="<html>blocked</html>")
|
||||
result = self.spider.categoryContent("新剧", "1", False, {})
|
||||
self.assertEqual(result, {"page": 1, "limit": 0, "total": 0, "list": []})
|
||||
|
||||
def test_clean_title_removes_hot_and_new_tags(self):
|
||||
self.assertEqual(self.spider._clean_title("【热播】新剧名"), "新剧名")
|
||||
self.assertEqual(self.spider._clean_title("[新剧热播] 第二部"), "第二部")
|
||||
@@ -70,20 +83,36 @@ class TestHongGuoDuanJuSpider(unittest.TestCase):
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_detail_content_builds_single_playlist(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"book_id": "100",
|
||||
"book_name": "总裁有疾闪婚来袭",
|
||||
"author": "树下的椰子",
|
||||
"category_names": ["都市爱情", "闪婚", "总裁"],
|
||||
"desc": "一段剧情简介",
|
||||
"duration": "1小时43分钟",
|
||||
"book_pic": "http://img/d.jpg",
|
||||
"time": "2026-04-25 09:32:02",
|
||||
"total": "2",
|
||||
"data": [
|
||||
{"title": "【热播】逆袭人生 1", "cover": "http://img/d.jpg", "video_id": "v1"},
|
||||
{"title": "【热播】逆袭人生 2", "cover": "http://img/d.jpg", "video_id": "v2"},
|
||||
{"title": "第1集", "video_id": "v1", "volume_name": ""},
|
||||
{"title": "第2集", "video_id": "v2", "volume_name": ""},
|
||||
]
|
||||
}
|
||||
result = self.spider.detailContent(["100"])
|
||||
vod = result["list"][0]
|
||||
self.assertEqual(vod["vod_id"], "100")
|
||||
self.assertEqual(vod["vod_name"], "逆袭人生")
|
||||
self.assertEqual(vod["vod_name"], "总裁有疾闪婚来袭")
|
||||
self.assertEqual(vod["vod_pic"], "http://img/d.jpg")
|
||||
self.assertEqual(vod["vod_remarks"], "更新至2集")
|
||||
self.assertEqual(vod["vod_remarks"], "1小时43分钟")
|
||||
self.assertEqual(vod["vod_actor"], "树下的椰子")
|
||||
self.assertEqual(vod["vod_area"], "都市爱情,闪婚,总裁")
|
||||
self.assertEqual(vod["vod_content"], "一段剧情简介")
|
||||
self.assertEqual(vod["vod_play_from"], "红果短剧")
|
||||
self.assertIn("逆袭人生 1$v1", vod["vod_play_url"])
|
||||
self.assertIn("逆袭人生 2$v2", vod["vod_play_url"])
|
||||
self.assertIn("第1集$v1", vod["vod_play_url"])
|
||||
self.assertIn("第2集$v2", vod["vod_play_url"])
|
||||
self.assertEqual(
|
||||
mock_json.call_args.args[0],
|
||||
"https://api-v2.cenguigui.cn/api/duanju/api.php?book_id=100",
|
||||
)
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_detail_content_returns_empty_when_id_missing(self, mock_json):
|
||||
@@ -103,17 +132,14 @@ class TestHongGuoDuanJuSpider(unittest.TestCase):
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_player_content_returns_best_quality_direct_url(self, mock_json):
|
||||
mock_json.return_value = {
|
||||
"data": {
|
||||
"qualities": [
|
||||
{"quality": "sd", "title": "标清", "download_url": "http://video/sd.mp4"},
|
||||
{"quality": "1080p", "title": "超清", "download_url": "http://video/1080.mp4"},
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_json.return_value = {"url": "http://video/play.mp4"}
|
||||
result = self.spider.playerContent("红果短剧", "v1", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "http://video/1080.mp4")
|
||||
self.assertEqual(result["url"], "http://video/play.mp4")
|
||||
self.assertEqual(
|
||||
mock_json.call_args.args[0],
|
||||
"https://api-v2.cenguigui.cn/api/duanju/api.php?video_id=v1",
|
||||
)
|
||||
|
||||
@patch.object(Spider, "_get_json")
|
||||
def test_player_content_returns_empty_url_on_error(self, mock_json):
|
||||
|
||||
+81
-59
@@ -10,58 +10,61 @@ sys.path.append("..")
|
||||
|
||||
|
||||
CLASS_LIST = [
|
||||
{"type_id": "热播", "type_name": "🎬热播"},
|
||||
{"type_id": "新剧", "type_name": "🎬新剧"},
|
||||
{"type_id": "都市", "type_name": "🎬都市"},
|
||||
{"type_id": "穿越", "type_name": "🎬穿越"},
|
||||
{"type_id": "重生", "type_name": "🎬重生"},
|
||||
{"type_id": "赘婿", "type_name": "🎬赘婿"},
|
||||
{"type_id": "逆袭", "type_name": "🎬逆袭"},
|
||||
{"type_id": "霸总", "type_name": "🎬霸总"},
|
||||
{"type_id": "职场", "type_name": "🎬职场"},
|
||||
{"type_id": "异能", "type_name": "🎬异能"},
|
||||
{"type_id": "神医", "type_name": "🎬神医"},
|
||||
{"type_id": "系统", "type_name": "🎬系统"},
|
||||
{"type_id": "总裁", "type_name": "🎬总裁"},
|
||||
{"type_id": "豪门", "type_name": "🎬豪门"},
|
||||
{"type_id": "现代言情", "type_name": "🎬现代言情"},
|
||||
{"type_id": "打脸虐渣", "type_name": "🎬打脸虐渣"},
|
||||
{"type_id": "豪门恩怨", "type_name": "🎬豪门恩怨"},
|
||||
{"type_id": "神豪", "type_name": "🎬神豪"},
|
||||
{"type_id": "校园", "type_name": "🎬校园"},
|
||||
{"type_id": "青春", "type_name": "🎬青春"},
|
||||
{"type_id": "马甲", "type_name": "🎬马甲"},
|
||||
{"type_id": "年代", "type_name": "🎬年代"},
|
||||
{"type_id": "闪婚", "type_name": "🎬闪婚"},
|
||||
{"type_id": "战神", "type_name": "🎬战神"},
|
||||
{"type_id": "女主", "type_name": "🎬女主"},
|
||||
{"type_id": "修仙", "type_name": "🎬修仙"},
|
||||
{"type_id": "都市日常", "type_name": "🎬都市日常"},
|
||||
{"type_id": "战神归来", "type_name": "🎬战神归来"},
|
||||
{"type_id": "小人物", "type_name": "🎬小人物"},
|
||||
{"type_id": "女性成长", "type_name": "🎬女性成长"},
|
||||
{"type_id": "大女主", "type_name": "🎬大女主"},
|
||||
{"type_id": "穿越", "type_name": "🎬穿越"},
|
||||
{"type_id": "都市修仙", "type_name": "🎬都市修仙"},
|
||||
{"type_id": "强者回归", "type_name": "🎬强者回归"},
|
||||
{"type_id": "亲情", "type_name": "🎬亲情"},
|
||||
{"type_id": "古装", "type_name": "🎬古装"},
|
||||
{"type_id": "重生", "type_name": "🎬重生"},
|
||||
{"type_id": "闪婚", "type_name": "🎬闪婚"},
|
||||
{"type_id": "赘婿逆袭", "type_name": "🎬赘婿逆袭"},
|
||||
{"type_id": "虐恋", "type_name": "🎬虐恋"},
|
||||
{"type_id": "追妻", "type_name": "🎬追妻"},
|
||||
{"type_id": "天下无敌", "type_name": "🎬天下无敌"},
|
||||
{"type_id": "家庭伦理", "type_name": "🎬家庭伦理"},
|
||||
{"type_id": "萌宝", "type_name": "🎬萌宝"},
|
||||
{"type_id": "古风", "type_name": "🎬古风"},
|
||||
{"type_id": "传承", "type_name": "🎬传承"},
|
||||
{"type_id": "甜宠", "type_name": "🎬甜宠"},
|
||||
{"type_id": "奇幻", "type_name": "🎬奇幻"},
|
||||
{"type_id": "爱情", "type_name": "🎬爱情"},
|
||||
{"type_id": "古风权谋", "type_name": "🎬古风权谋"},
|
||||
{"type_id": "职场", "type_name": "🎬职场"},
|
||||
{"type_id": "奇幻脑洞", "type_name": "🎬奇幻脑洞"},
|
||||
{"type_id": "异能", "type_name": "🎬异能"},
|
||||
{"type_id": "无敌神医", "type_name": "🎬无敌神医"},
|
||||
{"type_id": "古风言情", "type_name": "🎬古风言情"},
|
||||
{"type_id": "传承觉醒", "type_name": "🎬传承觉醒"},
|
||||
{"type_id": "现言甜宠", "type_name": "🎬现言甜宠"},
|
||||
{"type_id": "奇幻爱情", "type_name": "🎬奇幻爱情"},
|
||||
{"type_id": "乡村", "type_name": "🎬乡村"},
|
||||
{"type_id": "历史", "type_name": "🎬历史"},
|
||||
{"type_id": "历史古代", "type_name": "🎬历史古代"},
|
||||
{"type_id": "王妃", "type_name": "🎬王妃"},
|
||||
{"type_id": "高手", "type_name": "🎬高手"},
|
||||
{"type_id": "娱乐", "type_name": "🎬娱乐"},
|
||||
{"type_id": "联合", "type_name": "🎬联合"},
|
||||
{"type_id": "破镜", "type_name": "🎬破镜"},
|
||||
{"type_id": "暗恋", "type_name": "🎬暗恋"},
|
||||
{"type_id": "高手下山", "type_name": "🎬高手下山"},
|
||||
{"type_id": "娱乐圈", "type_name": "🎬娱乐圈"},
|
||||
{"type_id": "强强联合", "type_name": "🎬强强联合"},
|
||||
{"type_id": "破镜重圆", "type_name": "🎬破镜重圆"},
|
||||
{"type_id": "暗恋成真", "type_name": "🎬暗恋成真"},
|
||||
{"type_id": "民国", "type_name": "🎬民国"},
|
||||
{"type_id": "冤家", "type_name": "🎬冤家"},
|
||||
{"type_id": "真假", "type_name": "🎬真假"},
|
||||
{"type_id": "欢喜冤家", "type_name": "🎬欢喜冤家"},
|
||||
{"type_id": "系统", "type_name": "🎬系统"},
|
||||
{"type_id": "真假千金", "type_name": "🎬真假千金"},
|
||||
{"type_id": "龙王", "type_name": "🎬龙王"},
|
||||
{"type_id": "校园", "type_name": "🎬校园"},
|
||||
{"type_id": "穿书", "type_name": "🎬穿书"},
|
||||
{"type_id": "女帝", "type_name": "🎬女帝"},
|
||||
{"type_id": "团宠", "type_name": "🎬团宠"},
|
||||
{"type_id": "玄幻", "type_name": "🎬玄幻"},
|
||||
{"type_id": "仙侠", "type_name": "🎬仙侠"},
|
||||
{"type_id": "青梅", "type_name": "🎬青梅"},
|
||||
{"type_id": "悬疑", "type_name": "🎬悬疑"},
|
||||
{"type_id": "推理", "type_name": "🎬推理"},
|
||||
{"type_id": "年代爱情", "type_name": "🎬年代爱情"},
|
||||
{"type_id": "玄幻仙侠", "type_name": "🎬玄幻仙侠"},
|
||||
{"type_id": "青梅竹马", "type_name": "🎬青梅竹马"},
|
||||
{"type_id": "悬疑推理", "type_name": "🎬悬疑推理"},
|
||||
{"type_id": "皇后", "type_name": "🎬皇后"},
|
||||
{"type_id": "替身", "type_name": "🎬替身"},
|
||||
{"type_id": "大叔", "type_name": "🎬大叔"},
|
||||
@@ -74,7 +77,7 @@ class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
self.name = "红果短剧"
|
||||
self.host = "https://api-v2.cenguigui.cn"
|
||||
self.api = self.host + "/api/duanju.html"
|
||||
self.api = self.host + "/api/duanju/api.php"
|
||||
self.headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
@@ -109,8 +112,8 @@ class Spider(BaseSpider):
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page = int(pg)
|
||||
items = self._list_items(tid, page)
|
||||
return {"page": page, "limit": len(items), "total": len(items), "list": items}
|
||||
items = self._list_category_items(tid, page)
|
||||
return {"page": page, "limit": len(items), "total": page * 30 + len(items), "list": items}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = int(pg)
|
||||
@@ -126,27 +129,30 @@ class Spider(BaseSpider):
|
||||
vod_id = str(raw_id or "").strip()
|
||||
if not vod_id:
|
||||
continue
|
||||
payload = self._get_json(f"{self.api}?id={quote(vod_id)}")
|
||||
payload = self._get_json(f"{self.api}?book_id={quote(vod_id)}")
|
||||
data = payload.get("data", []) if isinstance(payload, dict) else []
|
||||
if not data:
|
||||
continue
|
||||
first = data[0]
|
||||
base_name = self._clean_title(re.sub(r"\s+\d+\s*$", "", str(first.get("title", ""))))
|
||||
episodes = []
|
||||
for index, item in enumerate(data, start=1):
|
||||
play_id = str(item.get("video_id", "")).strip()
|
||||
if not play_id:
|
||||
continue
|
||||
episode_name = self._clean_title(str(item.get("title", f"第{index}集")))
|
||||
episode_name = self._clean_title(
|
||||
str(item.get("title") or item.get("volume_name") or f"第{index}集")
|
||||
)
|
||||
episodes.append(f"{episode_name}${play_id}")
|
||||
tags = payload.get("category_names") or []
|
||||
result["list"].append(
|
||||
{
|
||||
"vod_id": vod_id,
|
||||
"vod_name": base_name,
|
||||
"vod_pic": str(first.get("cover", "")),
|
||||
"vod_area": "中国",
|
||||
"vod_remarks": f"更新至{len(episodes)}集",
|
||||
"vod_content": f"{base_name} 短剧,共 {len(episodes)} 集",
|
||||
"vod_name": self._clean_title(payload.get("book_name", "")),
|
||||
"vod_pic": str(payload.get("book_pic", "")),
|
||||
"vod_area": ",".join(tags) if isinstance(tags, list) else str(tags or ""),
|
||||
"vod_remarks": str(payload.get("duration", "")).strip() or f"更新至{len(episodes)}集",
|
||||
"vod_year": f"更新时间:{payload.get('time', '')}" if payload.get("time") else "",
|
||||
"vod_actor": str(payload.get("author", "")),
|
||||
"vod_content": str(payload.get("desc", "")),
|
||||
"vod_play_from": "红果短剧",
|
||||
"vod_play_url": "#".join(episodes),
|
||||
}
|
||||
@@ -155,17 +161,13 @@ class Spider(BaseSpider):
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
payload = self._get_json(f"{self.api}?video_id={quote(str(id or '').strip())}")
|
||||
data = payload.get("data", {}) if isinstance(payload, dict) else {}
|
||||
qualities = self._sort_qualities(data.get("qualities", []))
|
||||
best = next(
|
||||
(item for item in qualities if str(item.get("download_url", "")).strip()),
|
||||
{},
|
||||
payload = self._get_json(
|
||||
f"{self.api}?video_id={quote(str(id or '').strip())}"
|
||||
)
|
||||
return {
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
"url": str(best.get("download_url", "")).strip(),
|
||||
"url": str((payload or {}).get("url", "")).strip(),
|
||||
"header": {},
|
||||
}
|
||||
except Exception:
|
||||
@@ -176,13 +178,30 @@ class Spider(BaseSpider):
|
||||
data = payload.get("data", []) if isinstance(payload, dict) else []
|
||||
return [self._map_vod(item) for item in data]
|
||||
|
||||
def _list_category_items(self, classname, page):
|
||||
offset = self._category_offset(page)
|
||||
url = (
|
||||
f"{self.api}?classname={quote(str(classname))}"
|
||||
f"&offset={offset}&showRawParams=false"
|
||||
)
|
||||
payload = self._get_json(url)
|
||||
data = payload.get("data", []) if isinstance(payload, dict) else []
|
||||
return [self._map_vod(item) for item in data]
|
||||
|
||||
def _category_offset(self, page):
|
||||
page_num = max(1, int(page))
|
||||
if page_num <= 1:
|
||||
return 1
|
||||
return (page_num - 1) * 20
|
||||
|
||||
def _map_vod(self, item):
|
||||
total = item.get("totalChapterNum")
|
||||
remarks = str(item.get("sub_title", "")).strip()
|
||||
return {
|
||||
"vod_id": str(item.get("id", "")),
|
||||
"vod_id": str(item.get("book_id") or item.get("id", "")),
|
||||
"vod_name": self._clean_title(item.get("title", "")),
|
||||
"vod_pic": str(item.get("cover", "")),
|
||||
"vod_remarks": f"更新至{total}集" if total else "",
|
||||
"vod_remarks": remarks or (f"更新至{total}集" if total else ""),
|
||||
}
|
||||
|
||||
def _clean_title(self, text):
|
||||
@@ -203,4 +222,7 @@ class Spider(BaseSpider):
|
||||
response = self.fetch(url, headers=dict(self.headers), timeout=10, verify=False)
|
||||
if response.status_code != 200:
|
||||
return {}
|
||||
return json.loads(response.text or "{}")
|
||||
try:
|
||||
return json.loads(response.text or "{}")
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user