AAZ音乐
This commit is contained in:
+1
-1
@@ -197,7 +197,7 @@ class Spider(BaseSpider):
|
||||
|
||||
def _post_play_api(self, song_id):
|
||||
body = "id=%s&type=music" % quote(str(song_id or ""))
|
||||
response = self.fetch(
|
||||
response = self.post(
|
||||
self._build_url("/js/play.php"),
|
||||
headers={
|
||||
"User-Agent": self.headers["User-Agent"],
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# 嘟嘟短剧 Python 爬虫设计
|
||||
|
||||
## 目标
|
||||
|
||||
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的 `嘟嘟短剧` 爬虫,对接 `https://api-v2.cenguigui.cn/api/duanju.html`,支持首页、分类、搜索、详情和播放。
|
||||
|
||||
## 范围
|
||||
|
||||
本次实现包含:
|
||||
|
||||
- 新增 `py/嘟嘟短剧.py`
|
||||
- 新增 `py/tests/test_嘟嘟短剧.py`
|
||||
- 固定分类列表
|
||||
- 分类、搜索、详情与播放 API 对接
|
||||
- 标题清洗与多清晰度播放地址选择
|
||||
|
||||
本次实现不包含:
|
||||
|
||||
- 修改 `py/base/` 公共层
|
||||
- 改造 `py/短剧优选.py`
|
||||
- 新增真实联网测试
|
||||
- 引入缓存、代理或登录逻辑
|
||||
|
||||
## 方案
|
||||
|
||||
采用“单 Spider 文件 + API helper + 离线 JSON 测试”的方案。
|
||||
|
||||
原因:
|
||||
|
||||
- 用户目标是新增独立爬虫,不是替换聚合源
|
||||
- 目标站点是 JSON API,核心工作是请求构造和字段映射,不需要 HTML 解析
|
||||
- 仓库现有测试体系适合用 `unittest + mock` 固定接口响应
|
||||
|
||||
## 接口映射
|
||||
|
||||
- `homeContent`
|
||||
- 返回固定分类列表
|
||||
- `homeVideoContent`
|
||||
- 请求 `?name=热播&page=1`
|
||||
- 将返回列表映射为首页推荐
|
||||
- `categoryContent`
|
||||
- 请求 `?name=<分类名>&page=<页码>`
|
||||
- 返回列表、`page`、`limit`、`total`
|
||||
- 不额外引入复杂分页推断,仅在当前页有结果时维持最小可翻页行为
|
||||
- `searchContent`
|
||||
- 请求 `?name=<关键词>&page=<页码>`
|
||||
- 与分类复用同一列表映射
|
||||
- `detailContent`
|
||||
- 请求 `?id=<剧集ID>`
|
||||
- 将分集数组整理为单线路 `嘟嘟短剧`
|
||||
- 使用 `video_id` 作为播放条目 ID
|
||||
- `playerContent`
|
||||
- 请求 `?video_id=<分集video_id>`
|
||||
- 对 `qualities` 按画质优先级排序并返回直链
|
||||
|
||||
## 数据约定
|
||||
|
||||
- `vod_id` 使用 API 返回的剧目 `id`
|
||||
- `vod_name` 对标题做轻量清洗,移除如 `【热播】`、`【新剧】` 等标签
|
||||
- `vod_pic` 使用 `cover`
|
||||
- `vod_remarks` 优先使用总集数,格式为 `更新至N集`
|
||||
- `vod_play_from` 固定为 `嘟嘟短剧`
|
||||
- `vod_play_url` 使用 `集名$video_id` 形式拼接
|
||||
- `playerContent` 优先返回更高清晰度地址,优先级为 `1080p > sc > sd`
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 请求非 2xx 或 JSON 解析失败时,列表接口返回空列表
|
||||
- 详情失败时返回空 `list`
|
||||
- 播放失败时返回 `parse=0` 且 `url` 为空字符串
|
||||
- 尽量把错误限制在当前接口,不影响其他入口
|
||||
|
||||
## 测试策略
|
||||
|
||||
使用嵌入式 JSON 夹具和 mock 覆盖:
|
||||
|
||||
- 首页分类与首页推荐
|
||||
- 分类请求参数与列表映射
|
||||
- 搜索空关键字与正常搜索
|
||||
- 标题清洗逻辑
|
||||
- 详情页分集整理与基础元数据
|
||||
- 播放画质排序与直链输出
|
||||
- 请求失败时的空结果回退
|
||||
+12
-6
@@ -194,17 +194,23 @@ class TestAAZMusicSpider(unittest.TestCase):
|
||||
def test_detail_content_returns_empty_list_for_invalid_vod_id(self):
|
||||
self.assertEqual(self.spider.detailContent(["bad"])["list"], [])
|
||||
|
||||
@patch.object(Spider, "fetch")
|
||||
def test_player_content_resolves_song_id_to_direct_url(self, mock_fetch):
|
||||
mock_fetch.return_value = SimpleNamespace(status_code=200, text=PLAY_JSON)
|
||||
@patch.object(Spider, "post")
|
||||
def test_player_content_resolves_song_id_to_direct_url(self, mock_post):
|
||||
mock_post.return_value = SimpleNamespace(status_code=200, text=PLAY_JSON)
|
||||
result = self.spider.playerContent("AAZ音乐", "song:2001", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "https://cdn.example.com/aaz/song2001.mp3")
|
||||
self.assertEqual(result["header"]["Referer"], "https://www.aaz.cx/")
|
||||
self.assertEqual(mock_post.call_args.args[0], "https://www.aaz.cx/js/play.php")
|
||||
self.assertEqual(mock_post.call_args.kwargs["data"], "id=2001&type=music")
|
||||
self.assertEqual(
|
||||
mock_post.call_args.kwargs["headers"]["Content-Type"],
|
||||
"application/x-www-form-urlencoded; charset=UTF-8",
|
||||
)
|
||||
|
||||
@patch.object(Spider, "fetch")
|
||||
def test_player_content_returns_empty_url_for_invalid_or_empty_payload(self, mock_fetch):
|
||||
mock_fetch.return_value = SimpleNamespace(status_code=200, text=EMPTY_PLAY_JSON)
|
||||
@patch.object(Spider, "post")
|
||||
def test_player_content_returns_empty_url_for_invalid_or_empty_payload(self, mock_post):
|
||||
mock_post.return_value = SimpleNamespace(status_code=200, text=EMPTY_PLAY_JSON)
|
||||
self.assertEqual(self.spider.playerContent("AAZ音乐", "song:2001", {})["url"], "")
|
||||
self.assertEqual(self.spider.playerContent("AAZ音乐", "bad", {})["url"], "")
|
||||
|
||||
|
||||
+19
-19
@@ -83,11 +83,11 @@ class Spider(BaseSpider):
|
||||
"search": "/v3/search",
|
||||
"login": "https://u.shytkjgs.com/user/v1/account/login",
|
||||
},
|
||||
"甜圈": {
|
||||
"host": "https://mov.cenguigui.cn",
|
||||
"url1": "/duanju/api.php?classname",
|
||||
"url2": "/duanju/api.php?book_id",
|
||||
"search": "/duanju/api.php?name",
|
||||
"红果": {
|
||||
"host": "https://api-v2.cenguigui.cn",
|
||||
"url1": "/api/duanju/api.php?classname",
|
||||
"url2": "/api/duanju/api.php?book_id",
|
||||
"search": "/api/duanju/api.php?name",
|
||||
},
|
||||
"短剧网": {
|
||||
"host": "https://sm3.cc",
|
||||
@@ -100,7 +100,7 @@ class Spider(BaseSpider):
|
||||
# {"type_id": "围观", "type_name": "围观短剧"},
|
||||
# {"type_id": "剧王", "type_name": "剧王短剧"},
|
||||
{"type_id": "七星", "type_name": "七星短剧"},
|
||||
# {"type_id": "甜圈", "type_name": "甜圈短剧"},
|
||||
{"type_id": "红果", "type_name": "红果短剧"},
|
||||
{"type_id": "短剧网", "type_name": "短剧网"},
|
||||
]
|
||||
self.filters = {
|
||||
@@ -164,7 +164,7 @@ class Spider(BaseSpider):
|
||||
("七星剧场", "1"), ("七星新剧", "3"), ("七星热播", "2"),
|
||||
("七星星选", "7"), ("七星阳光", "5"),
|
||||
])],
|
||||
"甜圈": [self._filter_area("分类", [
|
||||
"红果": [self._filter_area("分类", [
|
||||
("逆袭", "逆袭"), ("霸总", "霸总"), ("现代言情", "现代言情"),
|
||||
("打脸虐渣", "打脸虐渣"), ("豪门恩怨", "豪门恩怨"),
|
||||
("神豪", "神豪"), ("马甲", "马甲"), ("都市日常", "都市日常"),
|
||||
@@ -198,7 +198,7 @@ class Spider(BaseSpider):
|
||||
self.filter_defaults = {
|
||||
"七猫": {"area": "0"}, "星芽": {"area": "1"}, "西饭": {"area": ""},
|
||||
"围观": {"area": "都市"}, "剧王": {"area": "/tag/逆袭/"},
|
||||
"七星": {"area": "1"}, "甜圈": {"area": "逆袭"},
|
||||
"七星": {"area": "1"}, "红果": {"area": "逆袭"},
|
||||
"短剧网": {"area": "1"},
|
||||
}
|
||||
|
||||
@@ -228,8 +228,7 @@ class Spider(BaseSpider):
|
||||
return {
|
||||
"list": items,
|
||||
"page": page,
|
||||
"pagecount": pagecount,
|
||||
"total": pagecount * len(items) if items else 0,
|
||||
"total": pagecount * 30 + len(items) if items else 0,
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
@@ -272,8 +271,9 @@ class Spider(BaseSpider):
|
||||
}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if "甜圈" in flag:
|
||||
return {"parse": 0, "jx": 0, "url": f"https://mov.cenguigui.cn/duanju/api.php?video_id={id}&type=mp4"}
|
||||
if "红果" in flag:
|
||||
res = self._get_json("https://api-v2.cenguigui.cn/api/duanju/api.php?video_id=" + id)
|
||||
return {"parse": 0, "jx": 0, "url": res.get("url")}
|
||||
if "星芽" in flag or "七猫" in flag or "西饭" in flag:
|
||||
return {"parse": 0, "jx": 0, "url": id}
|
||||
if "七星" in flag:
|
||||
@@ -410,7 +410,7 @@ class Spider(BaseSpider):
|
||||
"围观": self._cat_weiguan,
|
||||
"剧王": self._cat_juwang,
|
||||
"七星": self._cat_qixing,
|
||||
"甜圈": self._cat_tianquan,
|
||||
"红果": self._cat_tianquan,
|
||||
"短剧网": self._cat_duanjuwang,
|
||||
}.get(tid)
|
||||
if handler:
|
||||
@@ -535,7 +535,7 @@ class Spider(BaseSpider):
|
||||
items = []
|
||||
for it in res.get("data") or []:
|
||||
items.append({
|
||||
"vod_id": f"甜圈@{it.get('book_id', '')}",
|
||||
"vod_id": f"红果@{it.get('book_id', '')}",
|
||||
"vod_name": self._s(it.get("title")),
|
||||
"vod_pic": self._s(it.get("cover")),
|
||||
"vod_remarks": self._s(it.get("sub_title")),
|
||||
@@ -596,7 +596,7 @@ class Spider(BaseSpider):
|
||||
"围观": self._detail_weiguan,
|
||||
"剧王": self._detail_juwang,
|
||||
"七星": self._detail_qixing,
|
||||
"甜圈": self._detail_tianquan,
|
||||
"红果": self._detail_tianquan,
|
||||
"短剧网": self._detail_duanjuwang,
|
||||
}.get(platform)
|
||||
if handler:
|
||||
@@ -753,7 +753,7 @@ class Spider(BaseSpider):
|
||||
"vod_remarks": self._s(res.get("duration")),
|
||||
"vod_year": f"更新时间:{res.get('time', '')}",
|
||||
"vod_actor": self._s(res.get("author")),
|
||||
"vod_play_from": "甜圈短剧",
|
||||
"vod_play_from": "红果短剧",
|
||||
"vod_play_url": play_urls,
|
||||
}
|
||||
|
||||
@@ -866,7 +866,7 @@ class Spider(BaseSpider):
|
||||
"围观": self._search_weiguan,
|
||||
"剧王": self._search_juwang,
|
||||
"七星": self._search_qixing,
|
||||
"甜圈": self._search_tianquan,
|
||||
"红果": self._search_tianquan,
|
||||
"短剧网": self._search_duanjuwang,
|
||||
}.get(platform_id)
|
||||
if handler:
|
||||
@@ -982,10 +982,10 @@ class Spider(BaseSpider):
|
||||
items = []
|
||||
for it in res.get("data") or []:
|
||||
items.append({
|
||||
"vod_id": f"甜圈@{it.get('book_id', '')}",
|
||||
"vod_id": f"红果@{it.get('book_id', '')}",
|
||||
"vod_name": self._s(it.get("title")),
|
||||
"vod_pic": self._s(it.get("cover")),
|
||||
"vod_remarks": f"甜圈短剧 | {self._s(it.get('sub_title'))}",
|
||||
"vod_remarks": f"红果短剧 | {self._s(it.get('sub_title'))}",
|
||||
})
|
||||
return items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user