- 新增 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 路由验证通过
98 lines
3.5 KiB
Python
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",
|
|
}
|