- 新增 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 路由验证通过
156 lines
6.6 KiB
Python
156 lines
6.6 KiB
Python
"""酷我榜单(对应 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",
|
||
}
|