- 新增 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 路由验证通过
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
"""酷狗 KRC 歌词解密(对应 sdk/kg/krcDecode.ts)。
|
|
|
|
密文是 base64 → 去掉 4 字节头 → 与 16 字节固定 key 循环异或 → zlib inflate。
|
|
解密后是带 `[start,duration]` 行级标签与 `<offset,duration,0>` 字级标签的 KRC 文本,
|
|
另外 `[language:<base64>]` 行里藏着 base64(JSON) 的逐行翻译,需按行号拼回 LRC。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import re
|
|
import zlib
|
|
from typing import Any
|
|
|
|
from app.sdk.common.utils import decode_name
|
|
|
|
ENC_KEY = bytes([0x40, 0x47, 0x61, 0x77, 0x5E, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2D, 0xCE, 0xD2, 0x6E, 0x69])
|
|
|
|
_HEAD_EXP = re.compile(r"^.*\[id:\$\w+\]\n")
|
|
_TRANS_EXP = re.compile(r"\[language:([\w=\\/+]+)\]")
|
|
_TRANS_LINE_EXP = re.compile(r"\[language:[\w=\\/+]+\]\n")
|
|
_LINE_EXP = re.compile(r"\[((\d+),\d+)\].*")
|
|
_WORD_TAG_EXP = re.compile(r"<(\d+,\d+),\d+>")
|
|
_PLAIN_TAG_EXP = re.compile(r"<\d+,\d+>")
|
|
|
|
|
|
def _xor_decode(data: str) -> str:
|
|
if not data:
|
|
return ""
|
|
buf = bytearray(base64.b64decode(data)[4:])
|
|
for i in range(len(buf)):
|
|
buf[i] ^= ENC_KEY[i % 16]
|
|
return zlib.decompress(bytes(buf)).decode("utf-8", errors="replace")
|
|
|
|
|
|
def _format_time(ms: int) -> str:
|
|
"""毫秒 → `mm:ss.ms`(毫秒段与原文一致不补零)。"""
|
|
minutes, rem = divmod(ms, 60000)
|
|
return f"{minutes:02d}:{rem // 1000:02d}.{ms % 1000}"
|
|
|
|
|
|
def _lrc_line(value: Any) -> str:
|
|
"""rlyric/tlyric 的单项是字符串数组(逐字),字符串形态也容忍。"""
|
|
if isinstance(value, list):
|
|
return "".join(str(item) for item in value)
|
|
return "" if value is None else str(value)
|
|
|
|
|
|
def parse_lyric(text: str) -> dict[str, str]:
|
|
text = text.replace("\r", "")
|
|
if _HEAD_EXP.match(text):
|
|
text = _HEAD_EXP.sub("", text, count=1)
|
|
|
|
rlyric: list[Any] | None = None
|
|
tlyric: list[Any] | None = None
|
|
trans = _TRANS_EXP.search(text)
|
|
if trans:
|
|
text = _TRANS_LINE_EXP.sub("", text, count=1)
|
|
try:
|
|
payload = json.loads(base64.b64decode(trans.group(1)).decode("utf-8", errors="replace"))
|
|
except ValueError:
|
|
payload = {}
|
|
for item in payload.get("content") or []:
|
|
if item.get("type") == 0:
|
|
rlyric = item.get("lyricContent")
|
|
elif item.get("type") == 1:
|
|
tlyric = item.get("lyricContent")
|
|
|
|
index = 0
|
|
|
|
def repl(match: re.Match[str]) -> str:
|
|
nonlocal index
|
|
line_match = _LINE_EXP.match(match.group(0))
|
|
time_ms = int(line_match.group(2)) if line_match else 0
|
|
stamp = _format_time(time_ms)
|
|
if rlyric is not None:
|
|
rlyric[index] = f"[{stamp}]{_lrc_line(rlyric[index]) if index < len(rlyric) else ''}"
|
|
if tlyric is not None:
|
|
tlyric[index] = f"[{stamp}]{_lrc_line(tlyric[index]) if index < len(tlyric) else ''}"
|
|
index += 1
|
|
return match.group(0).replace(match.group(1), stamp, 1)
|
|
|
|
yrc = re.sub(r"\[((\d+),\d+)\].*", repl, text)
|
|
rlyric_text = "\n".join(str(item) for item in rlyric) if rlyric else ""
|
|
tlyric_text = "\n".join(str(item) for item in tlyric) if tlyric else ""
|
|
yrc = _WORD_TAG_EXP.sub(r"<\1>", yrc)
|
|
yrc = decode_name(yrc)
|
|
lyric = _PLAIN_TAG_EXP.sub("", yrc)
|
|
return {
|
|
"lyric": lyric,
|
|
"tlyric": decode_name(tlyric_text),
|
|
"rlyric": decode_name(rlyric_text),
|
|
"yrc": yrc,
|
|
}
|
|
|
|
|
|
async def decode_krc(data: str) -> dict[str, str]:
|
|
return parse_lyric(_xor_decode(data))
|
|
|
|
|
|
__all__ = ["decode_krc", "parse_lyric"]
|