Files
MusicServer/app/sdk/platforms/kw/leaderboard.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

156 lines
6.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.
"""酷我榜单(对应 sdk/kw/leaderboard.ts)。
`get_boards` 直接返回硬编码榜单表(不发请求),id 形如 `kw__93`(路由层会剥掉前缀
再把裸 bangid 传回 `get_list`)。详情走 `wbd.kuwo.cn/api/bd/bang/bang_info`:
请求体整体 AES-128-ECB 加密 + 签名,响应同样是加密体,`util.wbd_*` 负责双向转换。
"""
from __future__ import annotations
import re
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.common.utils import decode_name, format_play_time
from app.sdk.platforms.kw.util import format_singer, wbd_build_param, wbd_decode_data
LIST_DETAIL_LIMIT = 100
BOARD_LIST: list[dict[str, str]] = [
{"id": "kw__93", "name": "飙升榜", "bangid": "93"},
{"id": "kw__17", "name": "新歌榜", "bangid": "17"},
{"id": "kw__16", "name": "热歌榜", "bangid": "16"},
{"id": "kw__158", "name": "抖音热歌榜", "bangid": "158"},
{"id": "kw__292", "name": "铃声榜", "bangid": "292"},
{"id": "kw__284", "name": "热评榜", "bangid": "284"},
{"id": "kw__290", "name": "ACG新歌榜", "bangid": "290"},
{"id": "kw__286", "name": "台湾KKBOX榜", "bangid": "286"},
{"id": "kw__279", "name": "冬日暖心榜", "bangid": "279"},
{"id": "kw__281", "name": "巴士随身听榜", "bangid": "281"},
{"id": "kw__255", "name": "KTV点唱榜", "bangid": "255"},
{"id": "kw__280", "name": "家务进行曲榜", "bangid": "280"},
{"id": "kw__282", "name": "熬夜修仙榜", "bangid": "282"},
{"id": "kw__283", "name": "枕边轻音乐榜", "bangid": "283"},
{"id": "kw__278", "name": "古风音乐榜", "bangid": "278"},
{"id": "kw__264", "name": "Vlog音乐榜", "bangid": "264"},
{"id": "kw__242", "name": "电音榜", "bangid": "242"},
{"id": "kw__187", "name": "流行趋势榜", "bangid": "187"},
{"id": "kw__204", "name": "现场音乐榜", "bangid": "204"},
{"id": "kw__186", "name": "ACG神曲榜", "bangid": "186"},
{"id": "kw__185", "name": "最强翻唱榜", "bangid": "185"},
{"id": "kw__26", "name": "经典怀旧榜", "bangid": "26"},
{"id": "kw__104", "name": "华语榜", "bangid": "104"},
{"id": "kw__182", "name": "粤语榜", "bangid": "182"},
{"id": "kw__22", "name": "欧美榜", "bangid": "22"},
{"id": "kw__184", "name": "韩语榜", "bangid": "184"},
{"id": "kw__183", "name": "日语榜", "bangid": "183"},
{"id": "kw__145", "name": "会员畅听榜", "bangid": "145"},
{"id": "kw__153", "name": "网红新歌榜", "bangid": "153"},
{"id": "kw__64", "name": "影视金曲榜", "bangid": "64"},
{"id": "kw__176", "name": "DJ嗨歌榜", "bangid": "176"},
{"id": "kw__106", "name": "真声音", "bangid": "106"},
{"id": "kw__12", "name": "Billboard榜", "bangid": "12"},
{"id": "kw__49", "name": "iTunes音乐榜", "bangid": "49"},
{"id": "kw__180", "name": "beatport电音榜", "bangid": "180"},
{"id": "kw__13", "name": "英国UK榜", "bangid": "13"},
{"id": "kw__164", "name": "百大DJ榜", "bangid": "164"},
{"id": "kw__246", "name": "YouTube音乐排行榜", "bangid": "246"},
{"id": "kw__265", "name": "韩国Genie榜", "bangid": "265"},
{"id": "kw__14", "name": "韩国M-net榜", "bangid": "14"},
{"id": "kw__8", "name": "香港电台榜", "bangid": "8"},
{"id": "kw__15", "name": "日本公信榜", "bangid": "15"},
{"id": "kw__151", "name": "腾讯音乐人原创榜", "bangid": "151"},
]
BANG_API = "https://wbd.kuwo.cn/api/bd/bang/bang_info"
_BITRATE_QUALITY = {"4000": "flac24bit", "2000": "flac", "320": "320k", "128": "128k"}
# 榜单列表按音质等级升序排列(128k → 320k → flac → flac24bit)
_QUALITY_RANK = {"flac24bit": 4, "flac": 3, "320k": 2, "128k": 1}
_MININFO_RE = re.compile(r"level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)")
def _filter_data(raw_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
for item in raw_list:
types: list[dict[str, str]] = []
_types: dict[str, dict[str, str]] = {}
seen: set[str] = set()
for entry in str(item.get("n_minfo") or "").split(";"):
match = _MININFO_RE.search(entry)
if match is None:
continue
bitrate = match.group(2)
if bitrate in seen:
continue
seen.add(bitrate)
quality = _BITRATE_QUALITY.get(bitrate)
if quality is None:
continue
# 原实现 types 留原样、_types 转大写,两者不完全一致
types.append({"type": quality, "size": match.group(4)})
_types[quality] = {"size": match.group(4).upper()}
types.sort(key=lambda entry: _QUALITY_RANK[entry["type"]])
result.append(
{
"singer": format_singer(decode_name(item.get("artist"))),
"name": decode_name(item.get("name")),
"albumName": decode_name(item.get("album")),
"albumId": item.get("albumId"),
"songmid": item.get("id"),
"source": "kw",
"interval": format_play_time(_to_int(item.get("duration"))),
"img": item.get("pic"),
"lrc": None,
"otherSource": None,
"types": types,
"_types": _types,
"typeUrl": {},
}
)
return result
def _to_int(value: Any) -> int:
try:
return int(float(value))
except (TypeError, ValueError):
return 0
async def get_boards() -> dict[str, Any]:
return {"list": BOARD_LIST, "source": "kw"}
async def get_list(board_id: str, page: int = 1, retry_num: int = 0) -> dict[str, Any]:
retry_num += 1
if retry_num > 3:
raise RuntimeError("try max num")
body = {
"uid": "",
"devId": "",
"sFrom": "kuwo_sdk",
"user_type": "AP",
"carSource": "kwplayercar_ar_6.0.1.0_apk_keluze.apk",
"id": board_id,
"pn": page - 1,
"rn": LIST_DETAIL_LIMIT,
}
resp = await http_fetch(f"{BANG_API}?{wbd_build_param(body)}")
# 原实现直接调 decodeData,解密失败即 reject、不重试,这里保持一致
raw_data = wbd_decode_data(resp.body) if isinstance(resp.body, str) else {}
data = raw_data.get("data") or {}
if resp.status_code != 200 or raw_data.get("code") != 200 or not data.get("musiclist"):
return await get_list(board_id, page, retry_num)
return {
"total": _to_int(data.get("total")),
"list": _filter_data(data["musiclist"]),
"limit": LIST_DETAIL_LIMIT,
"page": page,
"source": "kw",
}