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

123 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.
"""酷狗音乐 取流(对应 sdk/apis/kg.ts)。
通道:`https://gateway.kugou.com/v5/url`(x-router: trackercdn.kugou.com,android 签名)。
登录后自动带 token/userid(见 /login/kg);匿名仅能取部分免费资源,VIP/受限资源返回 status:2。
音质映射:128k→128、320k→320、flac→flac、flac24bit→high。
成功响应播放地址在顶层 url: string[] 与 backupUrl: string[];旧 data:[{url}] 仅作兼容兜底。
album_audio_id 传 NaN 会被网关拒答,必须归零。
注意:TS 原实现在这里 `getDevice()` + `androidSign`,但那两个函数在 `sdk/auth/kg.ts`,
Python 侧 `app/sdk/auth/kg.py` 仍是 stub(不在本次写入范围),因此设备指纹与签名
改为 `app/sdk/platforms/kg/util.py` 的实现 —— 算法逐字一致,指纹同样落在
`auth.json` 的 `_kg_device`,与登录态共用。
"""
from __future__ import annotations
import hashlib
import math
import time
from typing import Any
from app.sdk.auth_store import get_kg_auth
from app.sdk.common.request import http_fetch
from app.sdk.platforms.kg.util import android_sign, encode_uri_component, get_device
SUPPORT_QUALITYS: list[str] = ["128k", "320k", "flac", "flac24bit"]
SALT_KEY = "57ae12eb6890223e355ccfcb74edf70d"
APPID = 1005
CLIENTVER = 11430
QUALITY_MAP = {"128k": "128", "320k": "320", "flac": "flac", "flac24bit": "high"}
URL_API = "https://gateway.kugou.com/v5/url"
def _num(value: Any) -> int:
"""JS 的 Number() 语义:不可解析或非有限值一律归零(网关拒答 NaN)。"""
try:
number = float(value)
except (TypeError, ValueError):
return 0
return int(number) if math.isfinite(number) else 0
async def get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
types = song_info.get("_types") or {}
quality_info = types.get(quality) if isinstance(types, dict) else None
hash_value = str((quality_info or {}).get("hash") or song_info.get("hash") or "").lower()
if not hash_value:
raise RuntimeError("缺少 hash")
dev = get_device()
auth = get_kg_auth()
userid = _num((auth or {}).get("userid") or 0)
params: dict[str, Any] = {
"dfid": dev["dfid"],
"mid": dev["mid"],
"uuid": "-",
"appid": APPID,
"clientver": CLIENTVER,
"clienttime": int(time.time()),
"album_id": _num(song_info.get("albumId")),
"area_code": 1,
"hash": hash_value,
"ssa_flag": "is_fromtrack",
"version": CLIENTVER,
"page_id": 151369488,
"quality": QUALITY_MAP.get(quality, "128"),
"album_audio_id": _num(song_info.get("songmid")),
"behavior": "play",
"pid": 2,
"cmd": 26,
"pidversion": 3001,
"IsFreePart": 0,
"ppage_id": "463467626,350369493,788954147",
"cdnBackup": 1,
"module": "",
}
if auth:
params["token"] = auth["token"]
params["userid"] = userid
params["key"] = hashlib.md5(f"{hash_value}{SALT_KEY}{APPID}{dev['mid']}{userid}".encode()).hexdigest()
params["signature"] = android_sign(params)
qs = "&".join(f"{key}={encode_uri_component(value)}" for key, value in params.items())
resp = await http_fetch(
f"{URL_API}?{qs}",
method="get",
headers={
"dfid": dev["dfid"],
"mid": dev["mid"],
"clienttime": str(params["clienttime"]),
"kg-rc": "1",
"kg-thash": "5d816a0",
"kg-rec": "1",
"kg-rf": "B9EDA08A64250DEFFBCADDEE00F8F25F",
"x-router": "trackercdn.kugou.com",
"User-Agent": "Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi",
},
timeout=15000,
)
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("status") != 1:
raise RuntimeError(str(body.get("error") or "获取失败(该资源需要酷狗登录)"))
candidates: list[Any] = []
if isinstance(body.get("url"), list):
candidates.extend(body["url"])
if isinstance(body.get("backupUrl"), list):
candidates.extend(body["backupUrl"])
url = next((str(item) for item in candidates if item), "")
if not url and isinstance(body.get("data"), list):
for entry in body["data"]:
if isinstance(entry, dict) and (entry.get("url") or entry.get("fallback")):
url = str(entry.get("url") or entry.get("fallback"))
break
if not url:
raise RuntimeError("获取失败(网关未返回播放地址)")
return {"type": quality, "url": url}