- 新增 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 路由验证通过
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""猫耳FM(MissEvan)App API 签名(对应 sdk/me/sign.ts)—— app.missevan.com 域鉴权。
|
||
|
||
算法(知乎 p/359692208 + zunley/gofm pkg/missevan/utils.go,2026-10-02 实测可用):
|
||
authorization = "MissEvan " + base64(HMAC-SHA256(appsecret, DATA))
|
||
DATA = METHOD\\n + url(冒号转义%3A)\\n + querystring\\n + equip_id:<id>\\n [+ token:<t>\\n]
|
||
+ x-m-date:<UTC毫秒ISO>\\n + x-m-nonce:<uuid>\\n [+ base64(sha256(body))\\n]
|
||
头:User-Agent MissEvanApp、x-m-date、x-m-nonce、Cookie(equip_id[+token])
|
||
|
||
该通道返回的是免签名 CDN 明文直链(sound-ali*-cdn-cn.maoercdn.com,m4a 原文件/-128k 变体);
|
||
网页版 hls.m3u8 为 Widevine 加密,与此无关。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import hashlib
|
||
import hmac
|
||
import re
|
||
import uuid
|
||
from datetime import UTC, datetime
|
||
from typing import Any
|
||
|
||
from app.sdk.auth_store import get_credentials
|
||
from app.sdk.common.request import http_fetch
|
||
from app.sdk.common.utils import as_dict
|
||
|
||
APP_SECRET = "f8Yw1M3u0e5MV7z34PcbY9wgP56YwJ"
|
||
APP_UA = "MissEvanApp/4.7.4 (iOS;15.1;iPhone11,6)"
|
||
EQUIP_ID = "52e76e9a-7592-43af-ae72-8ec755094e41"
|
||
|
||
|
||
def _utc_millis() -> str:
|
||
"""``new Date().toISOString().replace(/\\.\\d+Z$/, '.000Z')`` 的等价实现。"""
|
||
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||
|
||
|
||
def build_auth(
|
||
method: str,
|
||
url: str,
|
||
query: str,
|
||
*,
|
||
equip_id: str,
|
||
token: str,
|
||
date: str,
|
||
nonce: str,
|
||
) -> str:
|
||
"""构造 authorization 签名(GET 便捷封装;POST 需补 body 摘要行)。"""
|
||
lines = [
|
||
method,
|
||
url.replace(":", "%3A"),
|
||
query,
|
||
f"equip_id:{equip_id}",
|
||
]
|
||
if token:
|
||
lines.append(f"token:{token}")
|
||
lines.extend([f"x-m-date:{date}", f"x-m-nonce:{nonce}"])
|
||
digest = hmac.new(APP_SECRET.encode(), ("\n".join(lines) + "\n").encode(), hashlib.sha256).digest()
|
||
return "MissEvan " + base64.b64encode(digest).decode()
|
||
|
||
|
||
def get_me_token() -> str:
|
||
"""登录 token(/login/me 绑定的 cookie 中提取;未登录为空)。"""
|
||
cookie = str((get_credentials("me") or {}).get("cookie") or "")
|
||
match = re.search(r"(?:^|;\s*)token=([^;\s]+)", cookie)
|
||
return match.group(1) if match else ""
|
||
|
||
|
||
async def app_api_get(path: str, query: str = "") -> Any:
|
||
"""App API GET 请求(自动携带登录态 token)。"""
|
||
full = f"https://app.missevan.com{path}"
|
||
date = _utc_millis()
|
||
nonce = str(uuid.uuid4())
|
||
token = get_me_token()
|
||
cookie = f"equip_id={EQUIP_ID}" + (f"; token={token}" if token else "")
|
||
resp = await http_fetch(
|
||
f"{full}?{query}" if query else full,
|
||
headers={
|
||
"User-Agent": APP_UA,
|
||
"Accept": "application/json",
|
||
"Accept-Language": "zh-Hans-CN;q=1",
|
||
"x-m-date": date,
|
||
"x-m-nonce": nonce,
|
||
"Cookie": cookie,
|
||
"Authorization": build_auth("GET", full, query, equip_id=EQUIP_ID, token=token, date=date, nonce=nonce),
|
||
},
|
||
timeout=15_000,
|
||
)
|
||
if resp.status_code != 200:
|
||
raise RuntimeError(f"猫耳 App API HTTP {resp.status_code}")
|
||
body = as_dict(resp.body)
|
||
code = body.get("code")
|
||
if body.get("success") is False or (code is not None and code != 0):
|
||
raise RuntimeError(str(body.get("info") or f"猫耳 App API code {code}"))
|
||
info = body.get("info")
|
||
return info if info is not None else body
|
||
|
||
|
||
ME_UA = APP_UA
|