diff --git a/py/tests/test_酷我听书.py b/py/tests/test_酷我听书.py index 8f6989a..950af84 100644 --- a/py/tests/test_酷我听书.py +++ b/py/tests/test_酷我听书.py @@ -142,6 +142,50 @@ class TestKuWoTingShuSpider(unittest.TestCase): self.assertEqual(vod["vod_play_url"], "1.第一集$free|MUSIC_1#2.💎第二集$vip|MUSIC_2") self.assertEqual(vod["vod_remarks"], "会员 | 已完结 | 共2集 | 800播放") + @patch.object(Spider, "_search_get") + def test_detail_content_paginates_tracks_beyond_2000(self, mock_search_get): + first_page_tracks = [ + {"name": f"第{i}集", "musicrid": f"MUSIC_{i}", "playcnt": "1", "payInfo": {"feeType": {"bookvip": "0"}}} + for i in range(1, 2001) + ] + second_page_tracks = [ + {"name": f"第{i}集", "musicrid": f"MUSIC_{i}", "playcnt": "1", "payInfo": {"feeType": {"bookvip": "0"}}} + for i in range(2001, 2006) + ] + mock_search_get.side_effect = [ + { + "name": "长篇评书", + "img": "cover.jpg", + "info": "超长连载", + "artist": "播音员", + "company": "酷我出品", + "pub": "2026-04-24", + "lang": "普通话", + "finished": "0", + "vip": "0", + "songnum": "2005", + "musiclist": first_page_tracks, + }, + { + "name": "长篇评书", + "songnum": "2005", + "musiclist": second_page_tracks, + }, + ] + + result = self.spider.detailContent(["999"]) + + vod = result["list"][0] + play_items = vod["vod_play_url"].split("#") + self.assertEqual(len(play_items), 2005) + self.assertEqual(play_items[0], "1.第1集$free|MUSIC_1") + self.assertEqual(play_items[-1], "2005.第2005集$free|MUSIC_2005") + self.assertEqual(vod["vod_remarks"], "免费 | 连载中 | 共2005集 | 2005播放") + self.assertEqual(mock_search_get.call_args_list[0].args[1]["pn"], 0) + self.assertEqual(mock_search_get.call_args_list[1].args[1]["pn"], 1) + self.assertEqual(mock_search_get.call_args_list[0].args[1]["rn"], 2000) + self.assertEqual(mock_search_get.call_args_list[1].args[1]["rn"], 2000) + @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"}} diff --git a/py/酷我听书.py b/py/酷我听书.py index 5966efb..743a96f 100644 --- a/py/酷我听书.py +++ b/py/酷我听书.py @@ -336,12 +336,8 @@ class Spider(BaseSpider): return f"http://img3.sycdn.kuwo.cn/star/albumcover/240/{image}" return image - def detailContent(self, ids): - album_id = str(ids[0] if isinstance(ids, list) and ids else ids).strip() - if not album_id: - return {"list": []} - - payload = self._search_get( + def _fetch_album_detail_page(self, album_id, page, page_size=2000): + return self._search_get( "/r.s", { "stype": "albuminfo", @@ -355,14 +351,71 @@ class Spider(BaseSpider): "bksource": "kwbook_ar_9.1.8.1_tvivo.apk", "corp": "kuwo", "albumid": album_id, - "pn": 0, - "rn": 2000, + "pn": page, + "rn": page_size, "show_copyright_off": 1, "vipver": "MUSIC_8.2.0.0_BCS17", "mobi": 1, "iskwbook": 1, }, ) or {} + + def _fetch_album_detail(self, album_id, page_size=2000, max_pages=20): + merged_payload = {} + tracks = [] + seen_track_ids = set() + total_expected = 0 + + for page in range(max_pages): + payload = self._fetch_album_detail_page(album_id, page, page_size) + page_tracks = payload.get("musiclist") or [] + if not payload and page == 0: + return {} + if not merged_payload and payload: + merged_payload = dict(payload) + elif payload: + for key, value in payload.items(): + if key == "musiclist": + continue + if merged_payload.get(key) in ("", None, [], {}) and value not in ("", None, [], {}): + merged_payload[key] = value + + songnum = payload.get("songnum") or merged_payload.get("songnum") + try: + total_expected = int(songnum or 0) + except (TypeError, ValueError): + total_expected = 0 + + added = 0 + for track in page_tracks: + track_id = str(track.get("musicrid") or "") + dedupe_key = track_id or f"{track.get('name', '')}|{len(tracks)}" + if dedupe_key in seen_track_ids: + continue + seen_track_ids.add(dedupe_key) + tracks.append(track) + added += 1 + + if not page_tracks or added == 0: + break + if total_expected and len(tracks) >= total_expected: + break + if len(page_tracks) < page_size: + break + + if not tracks: + return {} + merged_payload["musiclist"] = tracks + if total_expected: + merged_payload["songnum"] = str(total_expected) + return merged_payload + + def detailContent(self, ids): + album_id = str(ids[0] if isinstance(ids, list) and ids else ids).strip() + if not album_id: + return {"list": []} + + payload = self._fetch_album_detail(album_id) tracks = payload.get("musiclist") or [] if not tracks: return {"list": []}