- 新增 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 路由验证通过
64 lines
2.8 KiB
Python
64 lines
2.8 KiB
Python
"""喜马拉雅 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"]}
|