AAZ音乐

This commit is contained in:
Harold
2026-04-25 09:17:23 +08:00
parent 7f42cdffcd
commit 33fd50339c
4 changed files with 32 additions and 109 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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