- 新增 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 路由验证通过
151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
"""酷狗专辑(对应 sdk/kg/album.ts)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.sdk.common.request import http_fetch
|
|
from app.sdk.platforms.kg.music_info import get_music_infos_by_list
|
|
from app.sdk.platforms.kg.util import create_http_fetch, encode_uri_component
|
|
|
|
ALBUM_INFO_URL = (
|
|
"http://kmrserviceretry.kugou.com/container/v1/album"
|
|
"?dfid=1tT5He3kxrNC4D29ad1MMb6F&mid=22945702112173152889429073101964063697&userid=0&appid=1005&clientver=11589"
|
|
)
|
|
|
|
ALBUM_SONG_URL = "http://mobiles.kugou.com/api/v3/album/song"
|
|
|
|
ALBUM_SEARCH_URL = "http://mobilecdn.kugou.com/api/v3/search/album"
|
|
|
|
_UA_MOBILE = (
|
|
"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"
|
|
)
|
|
|
|
|
|
def _album_cover(url: Any) -> str:
|
|
return str(url or "").replace("{size}", "240")
|
|
|
|
|
|
async def _get_album_info(id: Any) -> dict[str, Any]:
|
|
result = await create_http_fetch(
|
|
ALBUM_INFO_URL,
|
|
{
|
|
"method": "POST",
|
|
"body": {
|
|
"appid": 1005,
|
|
"clienttime": 1681833686,
|
|
"clientver": 11589,
|
|
"data": [{"album_id": id}],
|
|
"fields": (
|
|
"language,grade_count,intro,mix_intro,heat,category,sizable_cover,cover,album_name,type,"
|
|
"quality,publish_company,grade,special_tag,author_name,publish_date,language_id,album_id,"
|
|
"exclusive,is_publish,trans_param,authors,album_tag"
|
|
),
|
|
"isBuy": 0,
|
|
"key": "e6f3306ff7e2afb494e89fbbda0becbf",
|
|
"mid": "22945702112173152889429073101964063697",
|
|
"show_album_tag": 0,
|
|
},
|
|
},
|
|
)
|
|
if not result:
|
|
raise RuntimeError("get album info failed.")
|
|
info = result[0] if isinstance(result, list) else result
|
|
return {
|
|
"name": info.get("album_name"),
|
|
"image": str(info.get("sizable_cover") or "").replace("{size}", "240"),
|
|
"desc": info.get("intro"),
|
|
"authorName": info.get("author_name"),
|
|
}
|
|
|
|
|
|
async def get_album_detail(album_id: str, page: int = 1, limit: int = 200) -> dict[str, Any]:
|
|
album_list = await create_http_fetch(
|
|
f"{ALBUM_SONG_URL}?version=9108&albumid={album_id}&plat=0&pagesize={limit}"
|
|
f"&area_code=0&page={page}&with_res_tag=0"
|
|
)
|
|
album_list = album_list or {}
|
|
if not album_list.get("info"):
|
|
raise RuntimeError("Get album list failed.")
|
|
songs = await get_music_infos_by_list(album_list.get("info") or [])
|
|
info = await _get_album_info(album_id)
|
|
return {
|
|
"list": songs or [],
|
|
"page": page,
|
|
"limit": limit,
|
|
"total": album_list.get("total"),
|
|
"source": "kg",
|
|
"info": {
|
|
"name": info["name"],
|
|
"img": info["image"],
|
|
"desc": info["desc"],
|
|
"author": info["authorName"],
|
|
},
|
|
}
|
|
|
|
|
|
async def search_album(keyword: str, page: int = 1, limit: int = 10) -> dict[str, Any]:
|
|
word = str(keyword or "").strip()
|
|
if not word:
|
|
return {"source": "kg", "list": [], "page": page, "limit": limit, "total": 0}
|
|
page_no = max(1, int(page or 1))
|
|
page_size = max(1, int(limit or 10))
|
|
url = f"{ALBUM_SEARCH_URL}?keyword={encode_uri_component(word)}&format=json&page={page_no}&pagesize={page_size}"
|
|
resp = await http_fetch(url, headers={"User-Agent": _UA_MOBILE, "Referer": "http://m.kugou.com"}, timeout=15000)
|
|
body = resp.body if isinstance(resp.body, dict) else {}
|
|
if resp.status_code != 200 or body.get("status") != 1 or body.get("errcode") != 0:
|
|
raise RuntimeError(str(body.get("error") or "酷狗专辑搜索失败"))
|
|
data = body.get("data") or {}
|
|
songs: list[dict[str, Any]] = []
|
|
for item in data.get("info") or []:
|
|
album_id = str(item.get("albumid") or "").strip()
|
|
if not album_id or album_id == "0":
|
|
continue
|
|
songs.append(
|
|
{
|
|
"id": album_id,
|
|
"name": str(item.get("albumname") or "").strip(),
|
|
"cover": _album_cover(item.get("imgurl")),
|
|
"trackCount": int(item.get("songcount") or 0) or 0,
|
|
"creator": str(item.get("singername") or "").strip(),
|
|
"description": str(item.get("intro") or "").strip(),
|
|
"publishTime": str(item.get("publishtime") or "").strip(),
|
|
"link": f"https://www.kugou.com/album/{album_id}.html",
|
|
"source": "kg",
|
|
"type": "album",
|
|
}
|
|
)
|
|
return {
|
|
"source": "kg",
|
|
"list": songs,
|
|
"page": page_no,
|
|
"limit": page_size,
|
|
"total": int(data.get("total") or 0) or len(songs),
|
|
}
|
|
|
|
|
|
async def get_album_songs(id: Any) -> list[dict[str, Any]]:
|
|
album_id = str(id or "").strip()
|
|
if not album_id:
|
|
raise RuntimeError("酷狗专辑 id 为空")
|
|
result = await get_album_detail(album_id, 1, 10000)
|
|
return result["list"]
|
|
|
|
|
|
async def parse_album_link(link: str) -> dict[str, Any]:
|
|
"""album/single/<id>.html、yy/album/single/<id>.html、album/<id>.html、albumid=<id>。"""
|
|
import re
|
|
|
|
raw = str(link or "")
|
|
for pattern in (
|
|
r"album/single/(\d+)\.html",
|
|
r"yy/album/single/(\d+)\.html",
|
|
r"album/(\d+)\.html",
|
|
r"albumid=(\d+)",
|
|
):
|
|
match = re.search(pattern, raw)
|
|
if match:
|
|
return await get_album_detail(match.group(1), 1)
|
|
raise RuntimeError("无效的酷狗专辑链接")
|