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,139 @@
|
||||
"""QQ音乐搜索(移植自 sdk/tx/musicSearch.ts)。
|
||||
|
||||
走 musics.fcg 签名通道(zzc 签名 + SearchCgiService/DoSearchForQQMusicMobile)。
|
||||
item_song → SongInfo;无 media_mid 的条目直接丢弃(原文同样 skip)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.common.utils import format_play_time, format_singer_name
|
||||
from app.sdk.platforms.tx.filter import build_types, song_img
|
||||
from app.sdk.platforms.tx.utils import sign_request
|
||||
|
||||
DEFAULT_LIMIT = 50
|
||||
SUCCESS_CODE = 0
|
||||
|
||||
# 客户端指纹参数,原样照抄(少一项上游会拒)
|
||||
_SEARCH_COMM: dict[str, str] = {
|
||||
"ct": "11",
|
||||
"cv": "14090508",
|
||||
"v": "14090508",
|
||||
"tmeAppID": "qqmusic",
|
||||
"phonetype": "EBG-AN10",
|
||||
"deviceScore": "553.47",
|
||||
"devicelevel": "50",
|
||||
"newdevicelevel": "20",
|
||||
"rom": "HuaWei/EMOTION/EmotionUI_14.2.0",
|
||||
"os_ver": "12",
|
||||
"OpenUDID": "0",
|
||||
"OpenUDID2": "0",
|
||||
"QIMEI36": "0",
|
||||
"udid": "0",
|
||||
"chid": "0",
|
||||
"aid": "0",
|
||||
"oaid": "0",
|
||||
"taid": "0",
|
||||
"tid": "0",
|
||||
"wid": "0",
|
||||
"uid": "0",
|
||||
"sid": "0",
|
||||
"modeSwitch": "6",
|
||||
"teenMode": "0",
|
||||
"ui_mode": "2",
|
||||
"nettype": "1020",
|
||||
"v4ip": "",
|
||||
}
|
||||
|
||||
|
||||
def _search_id() -> str:
|
||||
"""对应原实现的 `Math.random().toString().slice(2)`:去掉 `0.` 前缀的随机数字串。"""
|
||||
return str(random.random()).split(".", 1)[-1]
|
||||
|
||||
|
||||
def handle_result(raw_list: Any) -> list[dict[str, Any]]:
|
||||
if not raw_list or not isinstance(raw_list, list):
|
||||
return []
|
||||
songs: list[dict[str, Any]] = []
|
||||
for item in raw_list:
|
||||
file = (item or {}).get("file") or {}
|
||||
if not file.get("media_mid"):
|
||||
continue
|
||||
types, _types = build_types(file)
|
||||
album = item.get("album") or {}
|
||||
album_mid = album.get("mid") or ""
|
||||
singers = item.get("singer") or []
|
||||
songs.append(
|
||||
{
|
||||
"singer": format_singer_name(singers, "name"),
|
||||
"name": item.get("title"),
|
||||
"albumName": album.get("name") or "",
|
||||
"albumId": album_mid,
|
||||
"source": "tx",
|
||||
"interval": format_play_time(item.get("interval")),
|
||||
"songId": item.get("id"),
|
||||
"albumMid": album_mid,
|
||||
"strMediaMid": file.get("media_mid"),
|
||||
"songmid": item.get("mid"),
|
||||
"img": song_img(album_mid, singers),
|
||||
"types": types,
|
||||
"_types": _types,
|
||||
"typeUrl": {},
|
||||
}
|
||||
)
|
||||
return songs
|
||||
|
||||
|
||||
async def _music_search(keyword: str, page: int, limit: int, retry_num: int) -> dict[str, Any]:
|
||||
"""签名通道搜索。重试上限 5 次(文案与原文一致:搜索失败)。"""
|
||||
if retry_num > 5:
|
||||
raise RuntimeError("搜索失败")
|
||||
resp = await sign_request(
|
||||
{
|
||||
"comm": _SEARCH_COMM,
|
||||
"req": {
|
||||
"module": "music.search.SearchCgiService",
|
||||
"method": "DoSearchForQQMusicMobile",
|
||||
"param": {
|
||||
"search_type": 0,
|
||||
"searchid": _search_id(),
|
||||
"query": keyword,
|
||||
"page_num": page,
|
||||
"num_per_page": limit,
|
||||
"highlight": 0,
|
||||
"nqc_flag": 0,
|
||||
"multi_zhida": 0,
|
||||
"cat": 2,
|
||||
"grp": 1,
|
||||
"sin": 0,
|
||||
"sem": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
body = resp.body if isinstance(resp.body, dict) else {}
|
||||
req = body.get("req") or {}
|
||||
if not req or body.get("code") != SUCCESS_CODE or req.get("code") != SUCCESS_CODE:
|
||||
return await _music_search(keyword, page, limit, retry_num + 1)
|
||||
return req.get("data") or {}
|
||||
|
||||
|
||||
async def search(keyword: str, page: int = 1, limit: int = 20, retry_num: int = 0) -> dict[str, Any]:
|
||||
if not limit:
|
||||
limit = DEFAULT_LIMIT
|
||||
data = await _music_search(keyword, page, limit, retry_num)
|
||||
# 响应结构:req.data.{body, meta}。歌曲数组在 data.body.item_song,总数在 data.meta.estimate_sum
|
||||
# (TS 侧写的是 body.item_song / meta.estimate_sum,其中 body 即此处的 data)。
|
||||
inner = data.get("body")
|
||||
meta = data.get("meta")
|
||||
songs = handle_result(inner.get("item_song") if isinstance(inner, dict) else None)
|
||||
total = (meta.get("estimate_sum") or 0) if isinstance(meta, dict) else 0
|
||||
return {
|
||||
"list": songs,
|
||||
"allPage": -(-int(total) // limit) if total else 0, # ceil 除法
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"source": "tx",
|
||||
}
|
||||
Reference in new issue
Block a user