Files
MusicServer/app/sdk/platforms/tx/music_search.py
T
snowy-yang dfc06bf4de 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 路由验证通过
2026-10-23 21:55:45 +08:00

140 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
}