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,63 @@
|
||||
"""喜马拉雅 URL 获取(对应 sdk/apis/xm.ts,PC 桌面端 baseInfo 通道)。
|
||||
|
||||
链路:make_xm_sign(hdaa 上报) → baseInfo device=win(带 PC 设备 Cookie) → playUrlList
|
||||
→ decode_win_url(AES-ECB,客户端 4.0.14 asar 密钥) → a.xmcdn.com 带签名直链。
|
||||
|
||||
实测(2026-10-02)匿名免费曲目 HTTP 200 audio/mp4 可播;付费/VIP 曲 ret=1001(需登录)。
|
||||
档位命名如 M4A_64/MP3_64/M4A_24/MP3_32/AAC_24(音乐内容另有 MP3_128/M4A_128);
|
||||
喜马拉雅以有声内容为主,码率低于音乐平台,音质映射到 '64k'/'128k' 桶。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.platforms.xm.decoder import decode_win_url
|
||||
from app.sdk.platforms.xm.music_info import get_track_info, play_url_list
|
||||
|
||||
#: 优先 M4A(同码率体积更小),128 档优先 MP3_128
|
||||
QUALITY_PREFERENCE: dict[str, list[str]] = {
|
||||
"128k": ["MP3_128", "M4A_128", "M4A_64", "MP3_64", "M4A_24", "MP3_32", "AAC_24"],
|
||||
"64k": ["M4A_64", "MP3_64", "M4A_24", "MP3_32", "AAC_24"],
|
||||
}
|
||||
SUPPORT_QUALITYS: list[str] = ["64k", "128k"]
|
||||
|
||||
|
||||
def _bitrate_of(type_name: str) -> int:
|
||||
match = re.search(r"(\d+)", str(type_name or ""))
|
||||
return int(match.group(1)) if match else 0
|
||||
|
||||
|
||||
def _bucket_of(bitrate: int) -> str:
|
||||
return "128k" if bitrate >= 128 else "64k"
|
||||
|
||||
|
||||
def _resolve_url(raw: str) -> str:
|
||||
return raw if raw.startswith("http") else decode_win_url(raw)
|
||||
|
||||
|
||||
async def get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
|
||||
"""取流:按档位偏好找 playUrlList,未知命名时取码率最高的一条。"""
|
||||
del retry_num # 契约参数,无重试语义
|
||||
track_id = str(song_info.get("songmid") or "")
|
||||
track_info = await get_track_info(track_id)
|
||||
play_list = play_url_list(track_info)
|
||||
if not play_list:
|
||||
raise RuntimeError("无可用播放资源(付费/版权内容需登录)")
|
||||
|
||||
preference = QUALITY_PREFERENCE.get(quality, QUALITY_PREFERENCE["128k"])
|
||||
picked: dict[str, str] | None = None
|
||||
for want in preference:
|
||||
hit = next((item for item in play_list if item.get("type") == want and item.get("url")), None)
|
||||
if hit:
|
||||
picked = {"type": str(hit["type"]), "url": _resolve_url(str(hit["url"]))}
|
||||
break
|
||||
if picked is None:
|
||||
# 兜底:未知档位命名时取码率最高的一条
|
||||
candidates = [item for item in play_list if item.get("url")]
|
||||
if not candidates:
|
||||
raise RuntimeError("无可用播放资源")
|
||||
best = sorted(candidates, key=lambda item: _bitrate_of(str(item.get("type") or "")), reverse=True)[0]
|
||||
picked = {"type": str(best["type"]), "url": _resolve_url(str(best["url"]))}
|
||||
return {"type": _bucket_of(_bitrate_of(picked["type"])), "url": picked["url"]}
|
||||
Reference in new issue
Block a user