feat(python): 完成 Python 全量迁移——剩余平台、登录通道与公共层修复

- 新增 5sing/apple/xm/me/qt/lr 平台包与取流实现(含 xm-sign、MissEvan 签名、蜻蜓 HMAC、Apple developer token)
- 新增 wy/kg/tx/lr 登录:扫码、手机号、账密(kg 20028 风控指纹)、cookie 绑定、kg/tx 续期
- request 层:保留多值 Set-Cookie;JSON body 紧凑化(修复 kg 网关验签);直连不经系统代理(对齐 Node)
- 登录路由统一 Body(embed=True),修复扫码轮询 422;lifespan 接入登录续期调度
- ruff/format/pyright 全绿;14 平台真实联网与 HTTP 路由验证通过
This commit is contained in:
snowy-yang committed 2026-10-23 21:55:45 +08:00
1 parent 800871ef6e
commit dfc06bf4de
182 files changed
+26620

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
"""网易云榜单(移植自 sdk/wy/leaderboard.ts)。
榜单列表是硬编码的 top_list(原实现的 get_boards 直接返回它,不再请求 weapi/toplist);
详情走 weapi/v3/playlist/detail 拿 trackIds,再用 song/detail 批量补全曲目。
id 必须是 `wy__<bangid>` 形态:路由 `_board_handler` 会剥掉 `wy__` 前缀得到裸 bangid,
前端拿到的 id 也只有带前缀时才和详情接口闭环。
"""
from __future__ import annotations
import asyncio
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.platforms.wy import music_detail
from app.sdk.platforms.wy.utils import weapi_headers
from app.sdk.platforms.wy.utils.crypto import weapi
LIMIT = 100000
TOP_LIST: list[dict[str, str]] = [
{"id": "wy__19723756", "name": "飙升榜", "bangid": "19723756"},
{"id": "wy__3779629", "name": "新歌榜", "bangid": "3779629"},
{"id": "wy__2884035", "name": "原创榜", "bangid": "2884035"},
{"id": "wy__3778678", "name": "热歌榜", "bangid": "3778678"},
{"id": "wy__991319590", "name": "说唱榜", "bangid": "991319590"},
{"id": "wy__71384707", "name": "古典榜", "bangid": "71384707"},
{"id": "wy__1978921795", "name": "电音榜", "bangid": "1978921795"},
{"id": "wy__5453912201", "name": "黑胶VIP爱听榜", "bangid": "5453912201"},
{"id": "wy__71385702", "name": "ACG榜", "bangid": "71385702"},
{"id": "wy__745956260", "name": "韩语榜", "bangid": "745956260"},
{"id": "wy__10520166", "name": "国电榜", "bangid": "10520166"},
{"id": "wy__180106", "name": "UK排行榜周榜", "bangid": "180106"},
{"id": "wy__60198", "name": "美国Billboard榜", "bangid": "60198"},
{"id": "wy__3812895", "name": "Beatport全球电子舞曲榜", "bangid": "3812895"},
{"id": "wy__21845217", "name": "KTV唛榜", "bangid": "21845217"},
{"id": "wy__60131", "name": "日本Oricon榜", "bangid": "60131"},
{"id": "wy__2809513713", "name": "欧美热歌榜", "bangid": "2809513713"},
{"id": "wy__2809577409", "name": "欧美新歌榜", "bangid": "2809577409"},
{"id": "wy__27135204", "name": "法国 NRJ Vos Hits 周榜", "bangid": "27135204"},
{"id": "wy__3001835560", "name": "ACG动画榜", "bangid": "3001835560"},
{"id": "wy__3001795926", "name": "ACG游戏榜", "bangid": "3001795926"},
{"id": "wy__3001890046", "name": "ACG VOCALOID榜", "bangid": "3001890046"},
{"id": "wy__3112516681", "name": "中国新乡村音乐排行榜", "bangid": "3112516681"},
{"id": "wy__5059644681", "name": "日语榜", "bangid": "5059644681"},
{"id": "wy__5059633707", "name": "摇滚榜", "bangid": "5059633707"},
{"id": "wy__5059642708", "name": "国风榜", "bangid": "5059642708"},
{"id": "wy__5338990334", "name": "潜力爆款榜", "bangid": "5338990334"},
{"id": "wy__5059661515", "name": "民谣榜", "bangid": "5059661515"},
{"id": "wy__6688069460", "name": "听歌识曲榜", "bangid": "6688069460"},
{"id": "wy__6723173524", "name": "网络热歌榜", "bangid": "6723173524"},
{"id": "wy__6732051320", "name": "俄语榜", "bangid": "6732051320"},
{"id": "wy__6732014811", "name": "越南语榜", "bangid": "6732014811"},
{"id": "wy__6886768100", "name": "中文DJ榜", "bangid": "6886768100"},
{"id": "wy__6939992364", "name": "俄罗斯top hit流行音乐榜", "bangid": "6939992364"},
{"id": "wy__7095271308", "name": "泰语榜", "bangid": "7095271308"},
{"id": "wy__7356827205", "name": "BEAT排行榜", "bangid": "7356827205"},
{"id": "wy__7325478166", "name": "编辑推荐榜VOL.44 天才女子摇滚乐队boygenius剖白卑微心迹", "bangid": "7325478166"},
{"id": "wy__7603212484", "name": "LOOK直播歌曲榜", "bangid": "7603212484"},
{"id": "wy__7775163417", "name": "赏音榜", "bangid": "7775163417"},
{"id": "wy__7785123708", "name": "黑胶VIP新歌榜", "bangid": "7785123708"},
{"id": "wy__7785066739", "name": "黑胶VIP热歌榜", "bangid": "7785066739"},
{"id": "wy__7785091694", "name": "黑胶VIP爱搜榜", "bangid": "7785091694"},
]
def filter_boards_data(raw_list: Any) -> list[dict[str, str]]:
"""weapi/toplist 响应 → 榜单列表(原实现的 filterBoardsData;当前 get_boards 不走它)。"""
return [
{"id": f"wy__{board['id']}", "name": str(board.get("name")), "bangid": str(board["id"])}
for board in raw_list or []
if isinstance(board, dict) and board.get("id") is not None
]
async def get_boards() -> dict[str, Any]:
"""榜单列表。原实现直接返回硬编码 top_list,不请求上游。"""
return {"list": TOP_LIST, "source": "wy"}
async def get_list(bangid: str, page: int = 1, retry_num: int = 0) -> dict[str, Any]:
"""榜单详情。失败递归重试最多 6 次,超出抛 try max num(文案与原文一致)。"""
retry_num += 1
if retry_num > 6:
raise RuntimeError("try max num")
resp = await http_fetch(
"https://music.163.com/weapi/v3/playlist/detail",
method="post",
headers=weapi_headers(with_cookie=False),
form=weapi({"id": bangid, "n": LIMIT, "p": 1}),
)
body = resp.body if isinstance(resp.body, dict) else {}
if resp.status_code != 200 or body.get("code") != 200:
# 让出事件循环:上游持续 5xx 时递归重试不应霸占单线程事件循环
await asyncio.sleep(0)
return await get_list(bangid, page, retry_num)
playlist = body.get("playlist") or {}
ids = [track.get("id") for track in playlist.get("trackIds") or [] if isinstance(track, dict)]
try:
detail = await music_detail.get_list(ids)
except Exception as err:
print(f"[wy] 榜单歌曲详情获取失败将重试({retry_num + 1}): {err}")
if str(err) == "try max num":
raise
await asyncio.sleep(0)
return await get_list(bangid, page, retry_num)
rows = detail["list"]
return {"total": len(rows), "list": rows, "limit": LIMIT, "page": page, "source": "wy"}
def get_detail_page_url(bangid: str) -> str:
bare = bangid.replace("wy__", "") if isinstance(bangid, str) else bangid
return f"https://music.163.com/#/discover/toplist?id={bare}"