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

79 lines
3.4 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.
"""猫耳FM URL 获取(对应 sdk/apis/me.ts,App API 通道)。
链路:HMAC-SHA256(appsecret) 签名 → app.missevan.com/sound/sound → 免签名明文直链
soundurl/soundurl_64 = 原文件(码率视源,常见 192k AAC)
soundurl_128 = 128k m4a;soundurl_32 = 192k 变体(字段命名与实际码率错位,以实测为准)
实测(2026-10-02)匿名可取免费声音,直链首字节 ftyp M4A 明文可播;
付费声音需 /login/me 绑定含 token 的 cookie 后由服务端携带。
songmid 约定:裸 id = 声音 id(声音搜索/剧集列表产出);
"d:<drama_id>" = 广播剧(剧搜索产出)——取流时自动选剧的首个免费集(预告/第一集)。
"""
from __future__ import annotations
import re
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.common.utils import as_dict, as_list
from app.sdk.platforms.me.music_info import get_sound_info
#: 音质 → 字段优先级(原文件可能高于 128k,128k 档固定 m4a)
QUALITY_FIELDS: dict[str, list[str]] = {
"128k": ["soundurl_128", "soundurl", "soundurl_64", "soundurl_32"],
"64k": ["soundurl_64", "soundurl", "soundurl_128", "soundurl_32"],
}
SUPPORT_QUALITYS: list[str] = ["64k", "128k"]
def pick_url(info: dict[str, Any], quality: str) -> dict[str, str]:
fields = QUALITY_FIELDS.get(quality, QUALITY_FIELDS["128k"])
url = ""
for field in fields:
value = info.get(field)
if value and str(value).startswith("http"):
url = str(value)
break
if not url:
raise RuntimeError("无可用音频资源(付费内容需 POST /login/me 绑定 token)")
hit_128 = "-128k" in url or quality == "128k"
return {"type": "128k" if hit_128 else "64k", "url": url}
async def _resolve_drama_first_free(drama_id: str) -> str:
"""剧条目 "d:<drama_id>" → 首个免费集的 sound_id(getdrama episodes.episode[])。"""
resp = await http_fetch(
f"https://www.missevan.com/dramaapi/getdrama?drama_id={drama_id}",
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
),
"Accept": "application/json",
"Referer": f"https://www.missevan.com/mdrama/{drama_id}",
},
timeout=15_000,
)
info = as_dict(resp.body).get("info") if resp.status_code == 200 else None
episodes = as_list(as_dict(as_dict(info).get("episodes")).get("episode"))
if not episodes:
raise RuntimeError("剧集列表为空或已下线")
first = next((item for item in episodes if int(as_dict(item).get("pay_type") or 0) == 0), episodes[0])
return str(as_dict(first).get("sound_id"))
async def get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
del retry_num # 契约参数,无重试语义
songmid = str(song_info.get("songmid") or "")
sound_id = songmid
if songmid.startswith("d:"):
if not re.fullmatch(r"\d+", songmid[2:]):
raise RuntimeError("剧 id 无效")
sound_id = await _resolve_drama_first_free(songmid[2:])
elif not re.fullmatch(r"\d+", sound_id):
raise RuntimeError("me id 需为声音 id 或 d:<剧id>(见 /search?source=me 与 /album/me)")
info = await get_sound_info(sound_id)
return pick_url(info, quality)