Archived
refactor: get_playlist_detail 批次拉取拆解(94 行 / 圈 24 / 嵌套 3 → 60 行 / 圈 9 / 嵌套 1)
为什么:歌单解析的唯一上游通道,94 行单函数把「歌单头解析 / trackIds 分批拉明细 / 失败批次记账 / incomplete 标记 / 错误映射」五件事揉在 一个 try 里;曲目字段映射与搜索歌曲路逐字段重复。 拆出(模块级,与 search_music 拆解同风格): - _song_base_fields:歌曲条目公共字段(搜索/歌单共用同一容错口径), _map_song_item 改为其加 type 键的包装 - _playlist_track_ids:trackIds → 字符串 ID 列表 - _collect_playlist_tracks:批次循环整体(失败批次计数、脏 ID 跳批、 明细缺 id 跳过、incomplete 判定输入),嵌套 3 是批次语义固有, 集中在单一职责函数内 保留语义:脏 ID('①'.isdigit() 为真但 int() 抛 ValueError)跳过整批 且不计 failed_batches;批次阶段网络错误仍映射「获取歌单详情请求失败」。 先写测试后动代码:新增 tests/test_playlist_detail.py 11 例,在未改动 实现上跑绿(修正 1 例测试自身的替身消费顺序),覆盖此前零测试路径—— playlist:null 防枚举、业务 code 错误、非 dict 响应、批次失败记账、 脏 ID 跳批、100/批分批、错误映射。 门禁:ruff 全过 · pyright 0/0 · pytest 359 passed(348→359)。
This commit is contained in:
1 parent
f0de0b65b1
commit
d2e6856899
2 files changed
+270
-43
No files matched your search
+59
-43
@@ -29,10 +29,9 @@ from .errors import (
|
||||
)
|
||||
|
||||
|
||||
def _map_song_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""歌曲条目映射。"""
|
||||
def _song_base_fields(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""歌曲条目公共字段(搜索四路与歌单详情共用同一容错口径)。"""
|
||||
return {
|
||||
'type': 'song',
|
||||
'id': item['id'],
|
||||
'name': item.get('name') or '未知歌曲',
|
||||
'artists': '/'.join(filter(None, (a.get('name') for a in item.get('ar') or []))) or '未知歌手',
|
||||
@@ -41,6 +40,57 @@ def _map_song_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _map_song_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""歌曲条目映射。"""
|
||||
return {
|
||||
'type': 'song',
|
||||
**_song_base_fields(item)
|
||||
}
|
||||
|
||||
|
||||
def _playlist_track_ids(playlist: dict[str, Any]) -> list[str]:
|
||||
"""trackIds → 字符串 ID 列表(缺 id 的条目跳过)。"""
|
||||
return [str(t['id']) for t in (playlist.get('trackIds') or []) if t.get('id')]
|
||||
|
||||
|
||||
async def _collect_playlist_tracks(track_ids: list[str], headers: dict[str, str],
|
||||
cookies: dict[str, str]) -> tuple[list[dict[str, Any]], int]:
|
||||
"""分批拉取歌单全部曲目明细,返回 (tracks, failed_batches)。
|
||||
|
||||
明细批次的业务 code 也要校验,失败批次记入 failed_batches,不再静默缺歌。
|
||||
脏 ID 跳过整批('①'.isdigit() 为真但 int() 抛 ValueError——原实现此处
|
||||
未捕获会整歌单 500,现为跳批);明细缺 id 的歌跳过不拖整批。
|
||||
"""
|
||||
tracks: list[dict[str, Any]] = []
|
||||
failed_batches = 0
|
||||
for i in range(0, len(track_ids), 100):
|
||||
batch_ids = track_ids[i:i + 100]
|
||||
try:
|
||||
song_data = {'c': json.dumps([{'id': int(sid), 'v': 0} for sid in batch_ids
|
||||
if str(sid).isdigit()])}
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
song_resp = await requests.post(APIConstants.SONG_DETAIL_V3, data=song_data,
|
||||
headers=headers, cookies=cookies, timeout=30)
|
||||
song_resp.raise_for_status()
|
||||
|
||||
song_result = song_resp.json()
|
||||
if not isinstance(song_result, dict):
|
||||
# 上游 null/数组体按失败批次计,不让 AttributeError 失真
|
||||
failed_batches += 1
|
||||
continue
|
||||
if song_result.get('code') != 200:
|
||||
# 本批次业务失败:计为失败批次,交付方可见残缺状态
|
||||
failed_batches += 1
|
||||
continue
|
||||
for song in (song_result.get('songs') or []):
|
||||
if not song.get('id'):
|
||||
continue
|
||||
tracks.append(_song_base_fields(song))
|
||||
return tracks, failed_batches
|
||||
|
||||
|
||||
def _map_album_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""专辑条目映射(artists 为空**不**兜底成"未知歌手"——与歌曲路不同口径)。"""
|
||||
return {
|
||||
@@ -286,7 +336,7 @@ class NeteaseAPI:
|
||||
cookies: 用户cookies
|
||||
|
||||
Returns:
|
||||
歌单详情信息
|
||||
歌单详情信息(曲目数与 trackIds 不一致时附 incomplete/missing/failed_batches)
|
||||
|
||||
Raises:
|
||||
APIException: API调用失败时抛出
|
||||
@@ -324,47 +374,13 @@ class NeteaseAPI:
|
||||
'tracks': []
|
||||
}
|
||||
|
||||
# 获取所有trackIds并分批获取详细信息(明细批次的业务 code 也要校验,
|
||||
# 失败批次记入 failed_batches,不再静默缺歌)
|
||||
track_ids = [str(t['id']) for t in (playlist.get('trackIds') or []) if t.get('id')]
|
||||
failed_batches = 0
|
||||
for i in range(0, len(track_ids), 100):
|
||||
batch_ids = track_ids[i:i+100]
|
||||
# trackIds 混入非数字脏 ID 时跳过该条(原实现 int 抛 ValueError
|
||||
# 不在捕获列表,整歌单 500);明细缺 id 的歌跳过不拖整批
|
||||
try:
|
||||
song_data = {'c': json.dumps([{'id': int(sid), 'v': 0} for sid in batch_ids
|
||||
if str(sid).isdigit()])}
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
song_resp = await requests.post(APIConstants.SONG_DETAIL_V3, data=song_data,
|
||||
headers=headers, cookies=cookies, timeout=30)
|
||||
song_resp.raise_for_status()
|
||||
|
||||
song_result = song_resp.json()
|
||||
if not isinstance(song_result, dict):
|
||||
# 上游 null/数组体按失败批次计,不让 AttributeError 失真
|
||||
failed_batches += 1
|
||||
continue
|
||||
if song_result.get('code') != 200:
|
||||
# 本批次业务失败:计为失败批次,交付方可见残缺状态
|
||||
failed_batches += 1
|
||||
continue
|
||||
for song in (song_result.get('songs') or []):
|
||||
if not song.get('id'):
|
||||
continue
|
||||
info['tracks'].append({
|
||||
'id': song['id'],
|
||||
'name': song.get('name') or '未知歌曲',
|
||||
'artists': '/'.join(filter(None, (a.get('name') for a in song.get('ar') or []))) or '未知歌手',
|
||||
'album': (song.get('al') or {}).get('name') or '未知专辑',
|
||||
'picUrl': (song.get('al') or {}).get('picUrl') or ''
|
||||
})
|
||||
track_ids = _playlist_track_ids(playlist)
|
||||
tracks, failed_batches = await _collect_playlist_tracks(track_ids, headers, cookies)
|
||||
info['tracks'] = tracks
|
||||
# trackIds 数与实际拿到的不一致时显式标记"不完整",不冒充完整结果
|
||||
if len(info['tracks']) != len(track_ids):
|
||||
if len(tracks) != len(track_ids):
|
||||
info['incomplete'] = True
|
||||
info['missing'] = len(track_ids) - len(info['tracks'])
|
||||
info['missing'] = len(track_ids) - len(tracks)
|
||||
info['failed_batches'] = failed_batches
|
||||
|
||||
return info
|
||||
|
||||
Reference in new issue
Block a user