feat: add kuwo tingshu spider
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import unittest
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE = SourceFileLoader("kuwo_tingshu_spider", str(ROOT / "酷我听书.py")).load_module()
|
||||
Spider = MODULE.Spider
|
||||
|
||||
|
||||
class TestKuWoTingShuSpider(unittest.TestCase):
|
||||
def setUp(self):
|
||||
Spider._instance = None
|
||||
self.spider = Spider()
|
||||
self.spider.init()
|
||||
|
||||
def test_home_content_returns_fixed_classes_and_filters(self):
|
||||
content = self.spider.homeContent(False)
|
||||
self.assertEqual(
|
||||
[item["type_id"] for item in content["class"]],
|
||||
["2", "37", "5", "62"],
|
||||
)
|
||||
self.assertEqual([item["key"] for item in content["filters"]["2"]], ["class", "vip", "sort"])
|
||||
self.assertEqual(content["filters"]["2"][0]["value"][0], {"n": "都市传说", "v": "42"})
|
||||
self.assertEqual(content["filters"]["37"][1]["value"][2], {"n": "会员权限", "v": "1"})
|
||||
|
||||
def test_format_play_count_and_paid_track_helpers(self):
|
||||
self.assertEqual(self.spider._format_play_count(1234), "1234")
|
||||
self.assertEqual(self.spider._format_play_count(12000), "1.2万")
|
||||
self.assertEqual(self.spider._format_play_count(230000000), "2.3亿")
|
||||
self.assertTrue(self.spider._is_paid_track({"payInfo": {"feeType": {"bookvip": "1"}}}))
|
||||
self.assertFalse(self.spider._is_paid_track({"payInfo": {"feeType": {"bookvip": "0"}}}))
|
||||
|
||||
def test_parse_search_payload_accepts_single_quotes_json(self):
|
||||
payload = self.spider._parse_search_payload("{'albumlist':[{'name':'示例'}],'TOTAL':1}")
|
||||
self.assertEqual(payload["albumlist"][0]["name"], "示例")
|
||||
self.assertEqual(payload["TOTAL"], 1)
|
||||
|
||||
@patch.object(Spider, "fetch")
|
||||
def test_api_get_uses_fetch_and_parses_json(self, mock_fetch):
|
||||
mock_fetch.return_value = SimpleNamespace(status_code=200, text='{"data":{"data":[{"albumId":1}]}}')
|
||||
result = self.spider._api_get("/v2/api/search/filter/albums", {"categoryId": "2"})
|
||||
self.assertEqual(result["data"]["data"][0]["albumId"], 1)
|
||||
self.assertEqual(mock_fetch.call_args.args[0], "http://tingshu.kuwo.cn/v2/api/search/filter/albums")
|
||||
self.assertEqual(mock_fetch.call_args.kwargs["params"]["categoryId"], "2")
|
||||
self.assertEqual(mock_fetch.call_args.kwargs["headers"]["User-Agent"], "kwplayer_ar_9.1.8.1_tvivo.apk")
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_home_video_content_aggregates_default_categories(self, mock_api_get):
|
||||
mock_api_get.side_effect = [
|
||||
{"data": {"data": [{"albumId": 1, "albumName": "小说A", "coverImg": "a.jpg", "vip": 0, "playCnt": 3200}]}},
|
||||
{"data": {"data": [{"albumId": 2, "albumName": "音乐B", "coverImg": "b.jpg", "vip": 1, "playCnt": 54000}]}},
|
||||
{"data": {"data": []}},
|
||||
{"data": {"data": [{"albumId": 4, "albumName": "原声D", "coverImg": "d.jpg", "vip": 0, "playCnt": 780000000}]}},
|
||||
]
|
||||
result = self.spider.homeVideoContent()
|
||||
self.assertEqual([item["vod_id"] for item in result["list"]], ["1", "2", "4"])
|
||||
self.assertEqual(result["list"][0]["vod_remarks"], "免费 | 3200次播放 | 有声小说")
|
||||
self.assertEqual(result["list"][1]["vod_remarks"], "会员 | 5.4万次播放 | 音乐金曲")
|
||||
self.assertEqual(result["list"][2]["vod_remarks"], "免费 | 7.8亿次播放 | 影视原声")
|
||||
first_call_params = mock_api_get.call_args_list[0].args[1]
|
||||
self.assertEqual(first_call_params["classifyId"], "42")
|
||||
self.assertEqual(first_call_params["sortType"], "tsScore")
|
||||
self.assertEqual(first_call_params["rn"], 12)
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_category_content_skips_empty_pages_and_filters_vip(self, mock_api_get):
|
||||
mock_api_get.side_effect = [
|
||||
{"data": {"data": []}},
|
||||
{"data": {"data": [
|
||||
{"albumId": 10, "albumName": "免费书", "coverImg": "a.jpg", "vip": 0, "playCnt": 1},
|
||||
{"albumId": 11, "albumName": "会员书", "coverImg": "b.jpg", "vip": 1, "playCnt": 2},
|
||||
]}},
|
||||
]
|
||||
result = self.spider.categoryContent("2", "3", False, {"class": "42", "vip": "0", "sort": "pubDate"})
|
||||
self.assertEqual(result["page"], 3)
|
||||
self.assertEqual(result["limit"], 21)
|
||||
self.assertEqual([item["vod_id"] for item in result["list"]], ["10"])
|
||||
self.assertGreaterEqual(result["total"], 1)
|
||||
self.assertNotIn("pagecount", result)
|
||||
params = mock_api_get.call_args_list[1].args[1]
|
||||
self.assertEqual(params["categoryId"], "2")
|
||||
self.assertEqual(params["classifyId"], "42")
|
||||
self.assertEqual(params["sortType"], "pubDate")
|
||||
self.assertEqual(params["pn"], 4)
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_category_content_returns_empty_result_for_unknown_tid(self, mock_api_get):
|
||||
result = self.spider.categoryContent("999", "1", False, {})
|
||||
self.assertEqual(result, {"page": 1, "limit": 21, "total": 0, "list": []})
|
||||
mock_api_get.assert_not_called()
|
||||
|
||||
@patch.object(Spider, "_search_get")
|
||||
def test_search_content_maps_search_results(self, mock_search_get):
|
||||
mock_search_get.return_value = {
|
||||
"albumlist": [
|
||||
{"DC_TARGETID": "100", "name": "三体", "img": "a.jpg", "vip": "1", "artist": "播音甲"},
|
||||
{"DC_TARGETID": "101", "name": "凡人修仙传", "img": "b.jpg", "vip": "0", "artist": "播音乙"},
|
||||
],
|
||||
"TOTAL": 25,
|
||||
}
|
||||
result = self.spider.searchContent("修仙", False, "2")
|
||||
self.assertEqual(result["page"], 2)
|
||||
self.assertEqual(result["limit"], 21)
|
||||
self.assertEqual(result["total"], 25)
|
||||
self.assertEqual([item["vod_id"] for item in result["list"]], ["100", "101"])
|
||||
self.assertEqual(result["list"][0]["vod_remarks"], "会员 | 播音甲")
|
||||
|
||||
def test_search_content_returns_empty_result_for_blank_keyword(self):
|
||||
self.assertEqual(
|
||||
self.spider.searchContent("", False, "1"),
|
||||
{"page": 1, "limit": 0, "total": 0, "list": []},
|
||||
)
|
||||
|
||||
@patch.object(Spider, "_search_get")
|
||||
def test_detail_content_maps_album_and_tracks(self, mock_search_get):
|
||||
mock_search_get.return_value = {
|
||||
"name": "鬼吹灯",
|
||||
"img": "20260307/abc.jpg",
|
||||
"info": "摸金冒险故事",
|
||||
"artist": "艾宝良",
|
||||
"company": "酷我出品",
|
||||
"pub": "2026-03-07",
|
||||
"lang": "普通话",
|
||||
"finished": "1",
|
||||
"vip": "1",
|
||||
"songnum": "2",
|
||||
"musiclist": [
|
||||
{"name": "第一集", "musicrid": "MUSIC_1", "playcnt": "300", "payInfo": {"feeType": {"bookvip": "0"}}},
|
||||
{"name": "第二集", "musicrid": "MUSIC_2", "playcnt": "500", "payInfo": {"feeType": {"bookvip": "1"}}},
|
||||
],
|
||||
}
|
||||
result = self.spider.detailContent(["123"])
|
||||
vod = result["list"][0]
|
||||
self.assertEqual(vod["vod_id"], "123")
|
||||
self.assertEqual(vod["vod_name"], "鬼吹灯")
|
||||
self.assertEqual(vod["vod_pic"], "http://img3.sycdn.kuwo.cn/star/albumcover/240/20260307/abc.jpg")
|
||||
self.assertEqual(vod["vod_year"], "2026年")
|
||||
self.assertEqual(vod["vod_play_from"], "酷我听书")
|
||||
self.assertEqual(vod["vod_play_url"], "1.第一集$free|MUSIC_1#2.💎第二集$vip|MUSIC_2")
|
||||
self.assertEqual(vod["vod_remarks"], "会员 | 已完结 | 共2集 | 800播放")
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_player_content_resolves_free_track(self, mock_api_get):
|
||||
mock_api_get.return_value = {"data": {"url": "https://audio.example/free.mp3"}}
|
||||
result = self.spider.playerContent("酷我听书", "free|MUSIC_1", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "https://audio.example/free.mp3")
|
||||
self.assertEqual(result["header"]["Referer"], self.spider.api_host)
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_player_content_resolves_vip_track(self, mock_api_get):
|
||||
mock_api_get.return_value = {"url": "https://audio.example/vip.mp3"}
|
||||
result = self.spider.playerContent("酷我听书", "vip|MUSIC_2", {})
|
||||
self.assertEqual(result["parse"], 0)
|
||||
self.assertEqual(result["url"], "https://audio.example/vip.mp3")
|
||||
self.assertEqual(result["header"]["Referer"], "https://music-api.gdstudio.xyz")
|
||||
|
||||
@patch.object(Spider, "_api_get")
|
||||
def test_player_content_falls_back_to_generated_urls(self, mock_api_get):
|
||||
mock_api_get.return_value = {}
|
||||
free_result = self.spider.playerContent("酷我听书", "free|MUSIC_9", {})
|
||||
vip_result = self.spider.playerContent("酷我听书", "vip|MUSIC_8", {})
|
||||
self.assertIn("rid=MUSIC_9", free_result["url"])
|
||||
self.assertIn("source=kuwo", vip_result["url"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user