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,84 @@
|
||||
"""千千音乐 取流(对应 sdk/apis/bd.ts)。
|
||||
|
||||
通道:`/v1/song/tracklink`(签名 GET)。档位:3000=flac / 320 / 128 / 64(aac)。
|
||||
实测(2026-10 匿名):
|
||||
|
||||
* 免费曲目(搜索 isVip=0)各档位均返回明文直链 `data.path`(audio0x.dmhmusic.com,CDN 签名 URL);
|
||||
* VIP/版权曲目返回 200 / errno 22000 但 `data` 无 path、无 trail_audio_info,
|
||||
需登录态会员 cookie 才可能有直链 —— 这类曲目沿降级链走到底后如实报错,不造假数据。
|
||||
|
||||
**签名串与实际 query 必须同源**:`sign_params` 一次产出签名与参数,query 由
|
||||
`URLSearchParams` 等价编码拼好后整串交给 `http_fetch`,不会再被二次序列化。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.platforms.bd.util import api_ok, error_text, first_non_empty, num_or_zero, track_link
|
||||
|
||||
# 请求档位 → tracklink rate 尝试链(优先请求档,逐级降级;与上游 3000→320→128→64 的探测顺序一致)
|
||||
QUALITY_RATES: dict[str, list[str]] = {
|
||||
"64k": ["64"],
|
||||
"128k": ["128", "64"],
|
||||
"320k": ["320", "128", "64"],
|
||||
"flac": ["3000", "320", "128", "64"],
|
||||
"flac24bit": ["3000", "320", "128", "64"],
|
||||
}
|
||||
DEFAULT_RATES = ["3000", "320", "128", "64"]
|
||||
|
||||
# 千千没有 flac24bit 对外档位(上游只到 3000 = flac)
|
||||
SUPPORT_QUALITYS: list[str] = ["64k", "128k", "320k", "flac"]
|
||||
|
||||
|
||||
def type_of_hit(request_rate: str, data: Any) -> str:
|
||||
"""命中档位判定:以响应 format/rate/bits 为准(可能低于请求档位)。"""
|
||||
source = data if isinstance(data, dict) else {}
|
||||
fmt = str(source.get("format") or "").lower()
|
||||
rate = num_or_zero(source.get("rate"))
|
||||
bits = num_or_zero(source.get("bits"))
|
||||
if fmt == "flac" or rate >= 1000:
|
||||
return "flac24bit" if bits == 24 else "flac"
|
||||
if rate >= 320 or request_rate == "320":
|
||||
return "320k"
|
||||
if rate >= 128 or request_rate == "128":
|
||||
return "128k"
|
||||
return "64k"
|
||||
|
||||
|
||||
async def get_download_url(tsid: str, type: str | None = None) -> dict[str, Any]:
|
||||
"""逐档请求 tracklink,取首个带 path 的档位。"""
|
||||
id = str(tsid if tsid is not None else "").strip()
|
||||
if not id:
|
||||
raise RuntimeError("千千歌曲 id(TSID)为空")
|
||||
rates = QUALITY_RATES.get(str(type if type is not None else ""), DEFAULT_RATES)
|
||||
|
||||
last_err: Exception | None = None
|
||||
for rate in rates:
|
||||
try:
|
||||
body = await track_link(id, rate)
|
||||
if not api_ok(body):
|
||||
last_err = RuntimeError(error_text(body, "千千取流失败"))
|
||||
continue
|
||||
data = (body.get("data") if isinstance(body, dict) else None) or {}
|
||||
data = data if isinstance(data, dict) else {}
|
||||
trail = data.get("trail_audio_info")
|
||||
url = first_non_empty(data.get("path"), trail.get("path") if isinstance(trail, dict) else None)
|
||||
if url:
|
||||
return {"type": type_of_hit(rate, data), "url": url}
|
||||
last_err = RuntimeError("千千该歌曲无可用直链(VIP/版权曲目需登录态,免费曲目可直接取流)")
|
||||
except Exception as err: # noqa: BLE001 - 单档网络/上游失败沿降级链继续,与原实现一致
|
||||
last_err = err
|
||||
if last_err is not None:
|
||||
raise last_err
|
||||
raise RuntimeError("千千未找到可用音频直链")
|
||||
|
||||
|
||||
async def get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
|
||||
# 上游 GetDownloadURL:Song.URL 已有直链时直接复用(Parse 场景)
|
||||
if song_info.get("url"):
|
||||
return {"type": quality or "128k", "url": str(song_info["url"])}
|
||||
tsid = first_non_empty(song_info.get("songmid"), song_info.get("TSID"))
|
||||
if not tsid:
|
||||
raise RuntimeError("千千歌曲 id(TSID)为空")
|
||||
return await get_download_url(tsid, quality)
|
||||
Reference in new issue
Block a user