Files
MusicServer/app/sdk/platforms/joox/lyric.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

93 lines
3.9 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.
"""JOOX 歌词(对应 sdk/joox/lyric.ts,上游 joox/lyric.go 移植)。
主通道:GET https://api.joox.com/web-fcgi-bin/web_lyric?musicid=<id>&country=sg&lang=zh_cn
响应形如 ``MusicJsonCallback({"code":0,"lyric":"<base64>"})``,lyric 为 base64 UTF-8 歌词。
实测(2026-10):该旧接口仍可用(同域的 web_get_songinfo 已 404,但 web_lyric 未下线)。
兜底:openjoox2 /track 详情的 ``lrc_content``(同为 base64)。
解码:base64 → UTF-8 → HTML 实体(&amp; 等)→ ``{ lyric, tlyric: "" }``。
QRC(``qrc_content``,XML 格式)上游与 TS 均未解析,此处同样不解析,避免引入未验证逻辑。
"""
from __future__ import annotations
import json
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.platforms.joox.music_info import build_track_url
from app.sdk.platforms.joox.util import (
JOOX_TIMEOUT,
decode_joox_lyric,
encode_uri_component,
first_non_empty,
joox_headers,
normalize_joox_id,
)
LEGACY_LYRIC_URL = "https://api.joox.com/web-fcgi-bin/web_lyric"
def parse_lyric_payload(body: Any) -> dict[str, Any] | None:
"""解析 MusicJsonCallback(...) / 裸 JSON 包裹。"""
if isinstance(body, dict):
return body
text = str(body if body is not None else "")
index = text.find("MusicJsonCallback(")
if index >= 0:
text = text[index + len("MusicJsonCallback(") :]
text = text.rstrip()
if text.endswith(";"):
text = text[:-1].rstrip()
if text.endswith(")"):
text = text[:-1].rstrip()
try:
parsed = json.loads(text)
except ValueError:
return None
return parsed if isinstance(parsed, dict) else None
def parse_legacy_lyric(status_code: int, body: Any) -> str:
"""上游 GetLyrics 的纯逻辑部分:web_lyric 响应体 → 歌词文本。"""
if status_code != 200:
raise RuntimeError(f"joox 歌词获取失败(HTTP {status_code})")
payload = parse_lyric_payload(body)
if not payload:
raise RuntimeError("joox 歌词解析失败")
raw = str(first_non_empty(payload.get("lyric"), ""))
if int(payload.get("code") or 0) != 0 or not raw:
raise RuntimeError("joox 歌词不存在或为空")
return decode_joox_lyric(raw)
async def get_legacy_lyric(song_id: str) -> str:
"""上游 GetLyrics 通道(仅旧接口,测试/内部用)。"""
url = f"{LEGACY_LYRIC_URL}?musicid={encode_uri_component(song_id)}&country=sg&lang=zh_cn"
res = await http_fetch(url, headers=joox_headers(), timeout=JOOX_TIMEOUT)
return parse_legacy_lyric(res.status_code, res.body)
async def get_lyric(song_info: dict[str, Any], *_rest: Any) -> dict[str, str]:
"""歌词:旧接口优先,失败回退 openjoox2 详情的 lrc_content。"""
song_id = normalize_joox_id(song_info.get("songmid") or song_info.get("id") or song_info.get("songid") or "")
if not song_id:
raise RuntimeError("joox 歌曲 id 无效")
# 1) 上游 web_lyric 通道
legacy_err: Exception | None = None
try:
url = f"{LEGACY_LYRIC_URL}?musicid={encode_uri_component(song_id)}&country=sg&lang=zh_cn"
res = await http_fetch(url, headers=joox_headers(), timeout=JOOX_TIMEOUT)
return {"lyric": parse_legacy_lyric(res.status_code, res.body), "tlyric": ""}
except Exception as err: # noqa: BLE001 - 降级链:任一失败都转 openjoox2 兜底
legacy_err = err
# 2) 兜底:openjoox2 详情 lrc_content(base64)
res = await http_fetch(build_track_url(song_id), headers=joox_headers(), timeout=JOOX_TIMEOUT)
body = res.body if isinstance(res.body, dict) else {}
raw = str(first_non_empty(body.get("lrc_content"), ""))
if res.status_code != 200 or not raw:
raise legacy_err or RuntimeError("joox 歌词不存在或为空")
return {"lyric": decode_joox_lyric(raw), "tlyric": ""}