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 路由验证通过
This commit is contained in:
1 parent
800871ef6e
commit
dfc06bf4de
182 files changed
+26620
No files matched your search
@@ -0,0 +1,133 @@
|
||||
"""酷狗链接解析(对应 sdk/kg/parse.ts)。
|
||||
|
||||
歌曲:`https://www.kugou.com/song/#hash=<32位hash>`(含 &album_id= 等尾串)或裸 hash。
|
||||
元数据取 `m.kugou.com/app/i/getSongInfo.php`(songName/author_name/timeLength/album_img/bitRate),
|
||||
再并发合并音乐信息里的专辑与多档 hash;直链优先用 playInfo 的 url,仍为空时用 apis/kg
|
||||
签名通道尽力补一次 —— 失败只返回元数据(与上游付费曲语义一致)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.common.request import http_fetch
|
||||
from app.sdk.common.utils import decode_name, format_play_time
|
||||
from app.sdk.platforms.kg import album, playlist
|
||||
from app.sdk.platforms.kg.music_info import get_music_info
|
||||
|
||||
_UA = (
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 "
|
||||
"(KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
|
||||
)
|
||||
|
||||
HASH_RE = re.compile(r"hash=([a-fA-F0-9]{32})", re.IGNORECASE)
|
||||
RAW_HASH_RE = re.compile(r"^[a-fA-F0-9]{32}$")
|
||||
|
||||
|
||||
def extract_hash(link: Any) -> str:
|
||||
"""链接/裸 hash → 32 位小写 hash;都不匹配返回空串。"""
|
||||
raw = str(link if link is not None else "").strip()
|
||||
match = HASH_RE.search(raw)
|
||||
if match:
|
||||
return match.group(1).lower()
|
||||
return raw.lower() if RAW_HASH_RE.match(raw) else ""
|
||||
|
||||
|
||||
async def _fetch_song_info_php(hash_value: str) -> dict[str, Any] | None:
|
||||
resp = await http_fetch(
|
||||
f"http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={hash_value}",
|
||||
method="get",
|
||||
headers={"User-Agent": _UA, "Referer": "http://m.kugou.com"},
|
||||
timeout=15000,
|
||||
)
|
||||
if resp.status_code != 200 or not isinstance(resp.body, dict):
|
||||
return None
|
||||
body = resp.body
|
||||
if int(body.get("errcode") or 0) != 0 or not body.get("hash"):
|
||||
return None
|
||||
return body
|
||||
|
||||
|
||||
async def parse_song(link: str) -> dict[str, Any]:
|
||||
import asyncio
|
||||
|
||||
hash_value = extract_hash(link)
|
||||
if not hash_value:
|
||||
raise RuntimeError("无效的酷狗歌曲链接(未找到 32 位 hash)")
|
||||
|
||||
raw, info = await asyncio.gather(
|
||||
_fetch_song_info_php(hash_value), get_music_info(hash_value), return_exceptions=True
|
||||
)
|
||||
raw = None if isinstance(raw, BaseException) else raw
|
||||
info = None if isinstance(info, BaseException) else info
|
||||
if not raw and not info:
|
||||
raise RuntimeError("酷狗歌曲不存在或已下架")
|
||||
|
||||
raw = raw or {}
|
||||
info = info or {}
|
||||
file_name = str(raw.get("fileName") or "")
|
||||
fallback_name = " - ".join(file_name.split(" - ")[1:]) if " - " in file_name else ""
|
||||
name = decode_name(str(raw.get("songName") or "").strip() or fallback_name or info.get("name") or "")
|
||||
singer = decode_name(str(raw.get("author_name") or "").strip() or info.get("singer") or "")
|
||||
duration = int(raw.get("timeLength") or 0) or 0
|
||||
|
||||
song: dict[str, Any] = {
|
||||
"songmid": str(info.get("songmid") or raw.get("audio_id") or "").strip(),
|
||||
"hash": str(raw.get("hash") or hash_value).lower(),
|
||||
"name": name,
|
||||
"singer": singer,
|
||||
"albumName": decode_name(str(info.get("albumName") or "").strip()),
|
||||
"albumId": str(info.get("albumId") or raw.get("albumid") or raw.get("req_albumid") or "").strip(),
|
||||
"interval": format_play_time(duration) if duration > 0 else (info.get("interval") or "--/--"),
|
||||
"img": str(raw.get("album_img") or "").replace("{size}", "240") or info.get("img") or None,
|
||||
"lrc": None,
|
||||
"otherSource": None,
|
||||
"types": info.get("types") or [],
|
||||
"_types": info.get("_types") or {},
|
||||
"typeUrl": {},
|
||||
"source": "kg",
|
||||
"extra": {
|
||||
"hash": str(raw.get("hash") or hash_value).lower(),
|
||||
"url": str(raw.get("url") or ""),
|
||||
"backup_url": raw.get("backup_url") or [],
|
||||
"bitrate": int(raw.get("bitRate") or 0) or 0,
|
||||
"link": f"https://www.kugou.com/song/#hash={hash_value}",
|
||||
},
|
||||
}
|
||||
|
||||
# playInfo 的 url/backup_url 是官方直链(付费曲为空),优先使用
|
||||
if not song.get("url"):
|
||||
backup = raw.get("backup_url") or []
|
||||
raw_url = str(raw.get("url") or "") or str(backup[0] if backup else "")
|
||||
if raw_url:
|
||||
song["url"] = raw_url
|
||||
song["type"] = "320k" if (int(raw.get("bitRate") or 0) or 0) >= 320000 else "128k"
|
||||
# 仍无直链时(风控/付费)用签名通道尽力补一次
|
||||
if not song.get("url"):
|
||||
try:
|
||||
from app.sdk.apis.kg import get_music_url
|
||||
|
||||
hit = await get_music_url(
|
||||
{
|
||||
"hash": song["hash"],
|
||||
"albumId": song["albumId"],
|
||||
"songmid": song["songmid"],
|
||||
"_types": song["_types"],
|
||||
},
|
||||
"128k",
|
||||
)
|
||||
if hit.get("url"):
|
||||
song["url"] = hit["url"]
|
||||
song["type"] = hit["type"]
|
||||
except Exception: # noqa: BLE001 - 与上游付费曲无直链的失败语义一致:元数据仍返回
|
||||
pass
|
||||
return song
|
||||
|
||||
|
||||
async def parse_album(link: str) -> dict[str, Any]:
|
||||
return await album.parse_album_link(link)
|
||||
|
||||
|
||||
async def parse_playlist(link: str) -> dict[str, Any]:
|
||||
return await playlist.parse_playlist_link(link)
|
||||
Reference in new issue
Block a user