Files
MusicServer/app/sdk/platforms/kg/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

98 lines
3.5 KiB
Python

"""酷狗搜索(对应 sdk/kg/musicSearch.ts)。"""
from __future__ import annotations
import math
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.common.utils import decode_name, format_play_time, format_singer_name, size_formate
from app.sdk.platforms.kg.util import encode_uri_component
DEFAULT_LIMIT = 30
_SEARCH_API = "https://songsearch.kugou.com/song_search_v2"
def _filter_data(raw: dict[str, Any]) -> dict[str, Any]:
types: list[dict[str, Any]] = []
_types: dict[str, dict[str, Any]] = {}
for quality, size_key, hash_key in (
("128k", "FileSize", "FileHash"),
("320k", "HQFileSize", "HQFileHash"),
("flac", "SQFileSize", "SQFileHash"),
("flac24bit", "ResFileSize", "ResFileHash"),
):
if raw.get(size_key, 0) == 0:
continue
size = size_formate(raw.get(size_key))
types.append({"type": quality, "size": size, "hash": raw.get(hash_key)})
_types[quality] = {"size": size, "hash": raw.get(hash_key)}
return {
"singer": decode_name(format_singer_name(raw.get("Singers"), "name")),
"name": decode_name(raw.get("SongName")),
"albumName": decode_name(raw.get("AlbumName")),
"albumId": raw.get("AlbumID"),
"songmid": raw.get("Audioid"),
"source": "kg",
"interval": format_play_time(raw.get("Duration")),
"_interval": raw.get("Duration"),
"img": None,
"lrc": None,
"otherSource": None,
"hash": raw.get("FileHash"),
"types": types,
"_types": _types,
"typeUrl": {},
}
def _handle_result(raw_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""同名同 hash 去重;`Grp` 是同一首歌的其它版本,也一并展开。"""
ids: set[str] = set()
result: list[dict[str, Any]] = []
for item in raw_list:
key = f"{item.get('Audioid')}{item.get('FileHash')}"
if key in ids:
continue
ids.add(key)
result.append(_filter_data(item))
for child in item.get("Grp") or []:
child_key = f"{child.get('Audioid')}{child.get('FileHash')}"
if child_key in ids:
continue
ids.add(child_key)
result.append(_filter_data(child))
return result
async def search(keyword: str, page: int = 1, limit: int = DEFAULT_LIMIT, retry_num: int = 0) -> dict[str, Any]:
if retry_num > 3:
raise RuntimeError("try max num")
page_size = DEFAULT_LIMIT if limit is None else limit
url = (
f"{_SEARCH_API}?keyword={encode_uri_component(keyword)}&page={page}&pagesize={page_size}"
"&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1"
)
try:
resp = await http_fetch(url)
except Exception: # noqa: BLE001 - 可降级失败:原实现同样重试同类请求
return await search(keyword, page, limit, retry_num + 1)
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("error_code") != 0:
return await search(keyword, page, limit, retry_num + 1)
data = body.get("data") or {}
raw_list = data.get("lists")
if raw_list is None:
return await search(keyword, page, limit, retry_num + 1)
songs = _handle_result(raw_list)
total = data.get("total") or 0
return {
"list": songs,
"allPage": math.ceil(total / page_size) if page_size else 1,
"limit": page_size,
"total": total,
"source": "kg",
}