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:
1 parent
800871ef6e
commit
dfc06bf4de
182 files changed
+26620
No files matched your search
@@ -0,0 +1,141 @@
|
||||
"""歌曲详情与音质档位换算(移植自 sdk/wy/musicDetail.ts)。
|
||||
|
||||
上游 weapi/v3/song/detail 返回 songs + privileges,音质档位由 privilege.maxbr/maxBrLevel 决定,
|
||||
档位声明顺序与 TS 版一致(flac24bit 先 push,switch 里依次追加,最后 reverse)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.common.request import http_fetch
|
||||
from app.sdk.common.utils import format_play_time, size_formate
|
||||
from app.sdk.platforms.wy.utils import UA_LINUX_CHROME, weapi_headers
|
||||
from app.sdk.platforms.wy.utils.crypto import weapi
|
||||
|
||||
|
||||
def _size_of(block: Any) -> str | None:
|
||||
"""{size: N} → '1.23 MB';缺块返回 None(与 TS 的 `item.x ? sizeFormate(item.x.size) : null` 一致)。"""
|
||||
if isinstance(block, dict) and block.get("size"):
|
||||
return size_formate(block["size"])
|
||||
return None
|
||||
|
||||
|
||||
def build_types(item: dict[str, Any], privilege: dict[str, Any], *, flac_size: bool = True) -> tuple[list[dict], dict]:
|
||||
"""按 privilege 生成 types/_types。`flac_size=False` 用于歌单详情(原实现那里 flac 的 size 恒为 null)。"""
|
||||
types: list[dict[str, Any]] = []
|
||||
_types: dict[str, dict[str, Any]] = {}
|
||||
|
||||
if privilege.get("maxBrLevel") == "hires":
|
||||
size = _size_of(item.get("hr"))
|
||||
types.append({"type": "flac24bit", "size": size})
|
||||
_types["flac24bit"] = {"size": size}
|
||||
|
||||
maxbr = privilege.get("maxbr")
|
||||
# 无 break 的 switch 直落是有意为之:高码率同时声明向下兼容的档位
|
||||
if maxbr in (999000, 320000, 192000, 128000):
|
||||
if maxbr == 999000:
|
||||
size = _size_of(item.get("sq")) if flac_size else None
|
||||
types.append({"type": "flac", "size": size})
|
||||
_types["flac"] = {"size": size}
|
||||
if maxbr in (999000, 320000):
|
||||
size = _size_of(item.get("h"))
|
||||
types.append({"type": "320k", "size": size})
|
||||
_types["320k"] = {"size": size}
|
||||
size = _size_of(item.get("l"))
|
||||
types.append({"type": "128k", "size": size})
|
||||
_types["128k"] = {"size": size}
|
||||
|
||||
types.reverse()
|
||||
return types, _types
|
||||
|
||||
|
||||
def _match_privilege(privileges: list[dict[str, Any]], item: dict[str, Any], index: int) -> dict[str, Any] | None:
|
||||
"""优先按下标取,id 不符时按 id 查;都找不到返回 None(原实现会跳过该曲目)。"""
|
||||
candidate = privileges[index] if index < len(privileges) else None
|
||||
if isinstance(candidate, dict) and candidate.get("id") == item.get("id"):
|
||||
return candidate
|
||||
for privilege in privileges:
|
||||
if isinstance(privilege, dict) and privilege.get("id") == item.get("id"):
|
||||
return privilege
|
||||
return None
|
||||
|
||||
|
||||
def get_singer(singers: Any) -> str:
|
||||
return "、".join(str(singer.get("name")) for singer in singers or [] if isinstance(singer, dict))
|
||||
|
||||
|
||||
def filter_list(songs: list[dict[str, Any]], privileges: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""song/detail 响应 → SongInfo 列表。"""
|
||||
rows: list[dict[str, Any]] = []
|
||||
for index, item in enumerate(songs):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
privilege = _match_privilege(privileges, item, index)
|
||||
if privilege is None:
|
||||
continue
|
||||
types, _types = build_types(item, privilege)
|
||||
album = item.get("al") or {}
|
||||
pc = item.get("pc")
|
||||
if pc:
|
||||
# 云盘/翻唱曲目走 pc 字段(歌手/歌名/专辑为纯文本)
|
||||
rows.append(
|
||||
{
|
||||
"singer": pc.get("ar") or "",
|
||||
"name": pc.get("sn") or "",
|
||||
"albumName": pc.get("alb") or "",
|
||||
"albumId": album.get("id"),
|
||||
"source": "wy",
|
||||
"interval": format_play_time((item.get("dt") or 0) / 1000),
|
||||
"songmid": item.get("id"),
|
||||
"img": album.get("picUrl") or "",
|
||||
"lrc": None,
|
||||
"otherSource": None,
|
||||
"types": types,
|
||||
"_types": _types,
|
||||
"typeUrl": {},
|
||||
}
|
||||
)
|
||||
else:
|
||||
rows.append(
|
||||
{
|
||||
"singer": get_singer(item.get("ar")),
|
||||
"name": item.get("name") or "",
|
||||
"albumName": album.get("name"),
|
||||
"albumId": album.get("id"),
|
||||
"source": "wy",
|
||||
"interval": format_play_time((item.get("dt") or 0) / 1000),
|
||||
"songmid": item.get("id"),
|
||||
"img": album.get("picUrl"),
|
||||
"lrc": None,
|
||||
"otherSource": None,
|
||||
"types": types,
|
||||
"_types": _types,
|
||||
"typeUrl": {},
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
async def get_list(ids: list[Any] | None = None, retry_num: int = 0) -> dict[str, Any]:
|
||||
"""按 id 批量取歌曲详情(weapi/v3/song/detail)。"""
|
||||
ids = ids or []
|
||||
if retry_num > 2:
|
||||
raise RuntimeError("try max num")
|
||||
|
||||
form = weapi(
|
||||
{
|
||||
"c": "[" + ",".join('{"id":' + str(id_) + "}" for id_ in ids) + "]",
|
||||
"ids": "[" + ",".join(str(id_) for id_ in ids) + "]",
|
||||
}
|
||||
)
|
||||
resp = await http_fetch(
|
||||
"https://music.163.com/weapi/v3/song/detail",
|
||||
method="post",
|
||||
headers={**weapi_headers(with_cookie=False), "User-Agent": UA_LINUX_CHROME},
|
||||
form=form,
|
||||
)
|
||||
body = resp.body if isinstance(resp.body, dict) else {}
|
||||
if resp.status_code != 200 or body.get("code") != 200:
|
||||
raise RuntimeError("获取歌曲详情失败")
|
||||
return {"source": "wy", "list": filter_list(body.get("songs") or [], body.get("privileges") or [])}
|
||||
Reference in new issue
Block a user