Files
MusicServer/app/sdk/platforms/kg/song_list.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

845 lines
31 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.
"""酷狗歌单广场与歌单解析(对应 sdk/kg/songList.ts,31KB 的最大模块)。
三个入口:
get_list(sort_id, tag_id, page) —— 歌单广场(special/getSpecial + 详情 + 推荐)
get_list_detail(id, page) —— 歌单解析,覆盖 special/single、酷狗码、gcid、
share chain、zlist、global_collection_id、网页链接
search(keyword, page, limit) —— 歌单搜索(msearchretry v3 search/special)
原实现的 `_requestObj*` 取消句柄在 Python 侧无对应物(http_fetch 自身不可取消),
已省略;其余降级链、重试次数、错误文案与原实现保持一致。
"""
from __future__ import annotations
import asyncio
import json
import re
import time
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.common.utils import date_format, decode_name, format_play_count, format_play_time, size_formate, to_md5
from app.sdk.platforms.kg.util import create_http_fetch, signature_params
LIST_DETAIL_LIMIT = 10000
SORT_LIST: list[dict[str, str]] = [
{"name": "推荐", "id": "5"},
{"name": "最热", "id": "6"},
{"name": "最新", "id": "7"},
{"name": "热藏", "id": "3"},
{"name": "飙升", "id": "8"},
]
_LIST_DATA_EXP = re.compile(r"global\.data = (\[.+\]);")
_LIST_INFO_EXP = re.compile(r'global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};')
_LIST_DETAIL_LINK_EXP = re.compile(r"^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)")
# chain → 分享信息缓存(原实现是 Map,跨请求复用同一进程缓存)
_CACHE: dict[str, Any] = {}
_CURRENT_TAG_INFO: dict[str, Any] = {"id": None, "info": None}
def _handle_signature(id: Any, page: int, limit: int) -> str:
"""H5 签名:业务参数按键排序拼接后以 key 包裹取 MD5(与 signature_params 同构)。"""
params = {
"srcappid": "2919",
"clientver": "20000",
"clienttime": int(time.time() * 1000),
"mid": "0",
"uuid": "0",
"dfid": "-",
"appid": 1058,
"type": 0,
"module": "playlist",
"page": page,
"pagesize": limit,
"specialid": id,
}
key = "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"
sorted_params = "".join(f"{k}={params[k]}" for k in sorted(params))
return to_md5(f"{key}{sorted_params}{key}")
def parse_html_desc(html: str) -> str | None:
prefix = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
index = html.find(prefix)
if index < 0:
return None
after = html[index + len(prefix) :]
end = after.find("</div>")
if end < 0:
return None
return decode_name(after[:end])
def _filter_info_hot_tag(raw: dict[str, Any]) -> list[dict[str, Any]]:
if raw.get("status") != 1:
return []
return [
{"id": tag.get("special_id"), "name": tag.get("special_name"), "source": "kg"}
for tag in (raw.get("data") or {}).values()
]
def _filter_tag_info(raw: dict[str, Any]) -> list[dict[str, Any]]:
return [
{
"name": name,
"list": [
{
"parent_id": tag.get("parent_id"),
"parent_name": tag.get("pname"),
"id": tag.get("id"),
"name": tag.get("name"),
"source": "kg",
}
for tag in (raw[name].get("data") or [])
],
}
for name in raw
]
def _filter_list(raw_data: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
{
"play_count": item.get("total_play_count") or format_play_count(item.get("play_count")),
"id": "id_" + str(item.get("specialid")),
"author": item.get("nickname"),
"name": item.get("specialname"),
"time": date_format(item.get("publish_time") or item.get("publishtime"), "Y-M-D"),
"img": item.get("img") or item.get("imgurl"),
"total": item.get("songcount"),
"grade": item.get("grade"),
"desc": item.get("intro"),
"source": "kg",
}
for item in raw_data
]
def _create_task_body(hashs: list[dict[str, Any]]) -> list[dict[str, Any]]:
base = {
"area_code": "1",
"show_privilege": 1,
"show_album_info": "1",
"is_publish": "",
"appid": 1005,
"clientver": 11451,
"mid": "1",
"dfid": "-",
"clienttime": int(time.time() * 1000),
"key": "OIlwieks28dk2k092lksi2UIkp",
"fields": "album_info,author_name,audio_info,ori_audio_name,base,songname",
}
tasks: list[dict[str, Any]] = []
rest = list(hashs)
while rest:
tasks.append({"data": rest[:100], **base})
if len(rest) < 100:
break
rest = rest[100:]
return tasks
async def _create_task(hashs: list[dict[str, Any]]) -> list[list[Any]]:
url = "http://gateway.kugou.com/v2/album_audio/audio"
headers = {
"KG-THash": "13a3164",
"KG-RC": "1",
"KG-Fake": "0",
"KG-RF": "00869891",
"User-Agent": "Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi",
"x-router": "kmr.service.kugou.com",
}
async def one(task: dict[str, Any]) -> list[Any]:
data = await create_http_fetch(url, {"method": "POST", "body": task, "headers": headers})
items = data if isinstance(data, list) else []
return [entry[0] if entry else None for entry in items]
return await asyncio.gather(*(one(task) for task in _create_task_body(hashs)))
def _de_duplication(datas: list[dict[str, Any]]) -> list[dict[str, Any]]:
seen: set[Any] = set()
result: list[dict[str, Any]] = []
for item in datas:
hash_value = item.get("hash")
if hash_value in seen:
continue
seen.add(hash_value)
result.append(item)
return result
async def _get_music_infos(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""hash 列表 → SongInfo(走 v2 album_audio 批量接口,与 musicInfo.ts 的 v3 不同)。"""
deduped = _de_duplication(items)
if not deduped:
return []
chunks = await _create_task([{"hash": item.get("hash")} for item in deduped])
return _filter_data2([entry for chunk in chunks for entry in chunk])
def _filter_data2(raw_list: list[Any]) -> list[dict[str, Any]]:
ids: set[Any] = set()
result: list[dict[str, Any]] = []
for item in raw_list:
if not item:
continue
audio = item.get("audio_info") or {}
if audio.get("audio_id") in ids:
continue
ids.add(audio.get("audio_id"))
types: list[dict[str, Any]] = []
_types: dict[str, dict[str, Any]] = {}
for quality, size_key, hash_key in (
("128k", "filesize", "hash"),
("320k", "filesize_320", "hash_320"),
("flac", "filesize_flac", "hash_flac"),
("flac24bit", "filesize_high", "hash_high"),
):
if str(audio.get(size_key, "0")) == "0":
continue
size = size_formate(int(audio.get(size_key) or 0))
types.append({"type": quality, "size": size, "hash": audio.get(hash_key)})
_types[quality] = {"size": size, "hash": audio.get(hash_key)}
album = item.get("album_info") or {}
result.append(
{
"singer": decode_name(item.get("author_name")),
"name": decode_name(item.get("songname")),
"albumName": decode_name(album.get("album_name")),
"albumId": album.get("album_id"),
"songmid": audio.get("audio_id"),
"source": "kg",
"interval": format_play_time(int(audio.get("timelength") or 0) / 1000),
"img": None,
"lrc": None,
"hash": audio.get("hash"),
"otherSource": None,
"types": types,
"_types": _types,
"typeUrl": {},
}
)
return result
async def get_list_detail_by_special_id(id: Any, page: int | None = None, try_num: int = 0) -> dict[str, Any]:
if try_num > 2:
raise RuntimeError("try max num")
resp = await http_fetch(get_song_list_detail_url(id))
html = resp.body if isinstance(resp.body, str) else ""
list_data = _LIST_DATA_EXP.search(html)
list_info = _LIST_INFO_EXP.search(html)
if not list_data:
return await get_list_detail_by_special_id(id, page, try_num + 1)
songs = await _get_music_infos(json.loads(list_data.group(1)))
name = list_info.group(1) if list_info else None
pic = list_info.group(2) if list_info else None
return {
"list": songs,
"page": 1,
"limit": LIST_DETAIL_LIMIT,
"total": len(songs),
"source": "kg",
"info": {"name": name, "img": pic, "desc": parse_html_desc(html)},
}
def get_info_url(tag_id: Any = None) -> str:
if tag_id:
return f"http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c={tag_id}"
return "http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&"
def get_song_list_url(sort_id: Any, tag_id: Any, page: int) -> str:
tag = "" if tag_id is None else tag_id
return f"http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t={sort_id}&c={tag}&p={page}"
def get_song_list_detail_url(id: Any) -> str:
return f"http://www2.kugou.kugou.com/yueku/v9/special/single/{id}-5-9999.html"
async def _get_song_list(sort_id: Any, tag_id: Any, page: int, try_num: int = 0) -> list[dict[str, Any]]:
if try_num > 2:
raise RuntimeError("try max num")
resp = await http_fetch(get_song_list_url(sort_id, tag_id, page))
body = resp.body if isinstance(resp.body, dict) else {}
if not body or body.get("status") != 1:
return await _get_song_list(sort_id, tag_id, page, try_num + 1)
return _filter_list(body.get("special_db") or [])
async def _get_song_list_recommend(try_num: int = 0) -> list[dict[str, Any]]:
if try_num > 2:
raise RuntimeError("try max num")
resp = await http_fetch(
"http://everydayrec.service.kugou.com/guess_special_recommend",
method="post",
headers={"User-Agent": "KuGou2012-8275-web_browser_event_handler"},
body={
"appid": 1001,
"clienttime": 1566798337219,
"clientver": 8275,
"key": "f1f93580115bb106680d2375f8032d96",
"mid": "21511157a05844bd085308bc76ef3343",
"platform": "pc",
"userid": "262643156",
"return_min": 6,
"return_max": 15,
},
)
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("status") != 1:
return await _get_song_list_recommend(try_num + 1)
return _filter_list((body.get("data") or {}).get("special_list") or [])
async def _get_list_info(tag_id: Any, try_num: int = 0) -> dict[str, Any]:
if try_num > 2:
raise RuntimeError("try max num")
resp = await http_fetch(get_info_url(tag_id))
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("status") != 1:
return await _get_list_info(tag_id, try_num + 1)
params = (body.get("data") or {}).get("params") or {}
return {
"limit": params.get("pagesize"),
"page": params.get("p"),
"total": params.get("total"),
"source": "kg",
}
async def get_list(sort_id: Any, tag_id: Any, page: int = 1) -> dict[str, Any]:
"""歌单广场:首屏(无标签、第 1 页、默认排序)额外混入推荐歌单。"""
tasks: list[Any] = [_get_song_list(sort_id, tag_id, page)]
if _CURRENT_TAG_INFO["id"] == tag_id:
tasks.append(asyncio.sleep(0, result=_CURRENT_TAG_INFO["info"]))
else:
async def load_info() -> dict[str, Any]:
info = await _get_list_info(tag_id)
_CURRENT_TAG_INFO["id"] = tag_id
_CURRENT_TAG_INFO["info"] = dict(info)
return info
tasks.append(load_info())
if not tag_id and page == 1 and sort_id == SORT_LIST[0]["id"]:
tasks.append(_get_song_list_recommend())
results = await asyncio.gather(*tasks)
songs: list[dict[str, Any]] = results[0]
info: dict[str, Any] = results[1]
if len(results) > 2 and results[2]:
songs = [*results[2], *songs]
return {"list": songs, **info}
async def get_tags(try_num: int = 0) -> dict[str, Any]:
if try_num > 2:
raise RuntimeError("try max num")
resp = await http_fetch(get_info_url())
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("status") != 1:
return await get_tags(try_num + 1)
data = body.get("data") or {}
return {
"hotTag": _filter_info_hot_tag(data.get("hotTag") or {}),
"tags": _filter_tag_info(data.get("tagids") or {}),
"source": "kg",
}
def get_detail_page_url(id: Any) -> str:
value = id
if isinstance(value, str):
if re.search(r"^https?://", value):
return value
value = value.replace("id_", "")
return f"https://www.kugou.com/yy/special/single/{value}.html"
async def search(keyword: str, page: int = 1, limit: int = 20) -> dict[str, Any]:
from app.sdk.platforms.kg.util import encode_uri_component
url = (
f"http://msearchretry.kugou.com/api/v3/search/special?keyword={encode_uri_component(keyword)}"
f"&page={page}&pagesize={limit}&showtype=10&filter=0&version=7910&sver=2"
)
resp = await http_fetch(url)
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("errcode") != 0:
raise RuntimeError("filed")
data = body.get("data") or {}
return {
"list": [
{
"play_count": format_play_count(item.get("playcount")),
"id": "id_" + str(item.get("specialid")),
"author": item.get("nickname"),
"name": item.get("specialname"),
"time": date_format(item.get("publishtime"), "Y-M-D"),
"img": item.get("imgurl"),
"grade": item.get("grade"),
"desc": item.get("intro"),
"total": item.get("songcount"),
"source": "kg",
}
for item in data.get("info") or []
],
"limit": limit,
"total": data.get("total"),
"page": page,
"source": "kg",
}
# ---- 歌单详情解析链 ----
async def _decode_gcid(gcid: str) -> str | None:
params = "dfid=-&appid=1005&mid=0&clientver=20109&clienttime=640612895&uuid=-"
body = {"ret_info": 1, "data": [{"id": gcid, "id_type": 2}]}
result = await create_http_fetch(
f"https://t.kugou.com/v1/songlist/batch_decode?{params}"
f"&signature={signature_params(params, 'android', json.dumps(body, separators=(',', ':')))}",
{
"method": "POST",
"headers": {
"User-Agent": (
"Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36"
),
"Referer": "https://m.kugou.com/",
},
"body": body,
},
)
items = (result or {}).get("list") or []
return items[0].get("global_collection_id") if items else None
async def _get_user_list_detail_2(global_collection_id: str) -> dict[str, Any]:
if len(global_collection_id) > 1000:
raise RuntimeError("get list error")
params = (
"appid=1058&specialid=0&global_specialid="
+ global_collection_id
+ "&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519"
"&mid=1586163242519&uuid=1586163242519&dfid=-"
)
headers = {
"mid": "1586163242519",
"Referer": "https://m3ws.kugou.com/share/index.php",
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 "
"(KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
),
"dfid": "-",
"clienttime": "1586163242519",
}
info = await create_http_fetch(
f"https://mobiles.kugou.com/api/v5/special/info_v2?{params}&signature={signature_params(params, 'web')}",
{"headers": headers},
)
info = info or {}
total = info.get("songcount") or 0
tasks: list[Any] = []
page = 0
while total:
limit = 300 if total > 300 else total
total -= limit
page += 1
song_params = (
f"appid=1058&global_specialid={global_collection_id}&specialid=0&plat=0&version=8000"
f"&page={page}&pagesize={limit}&srcappid=2919&clientver=20000"
"&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-"
)
tasks.append(
create_http_fetch(
f"https://mobiles.kugou.com/api/v5/special/song_v2?{song_params}"
f"&signature={signature_params(song_params, 'web')}",
{
"headers": {
"mid": "1586163263991",
"Referer": "https://m3ws.kugou.com/share/index.php",
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 "
"(KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
),
"dfid": "-",
"clienttime": "1586163263991",
}
},
)
)
song_rows: list[Any] = []
for chunk in await asyncio.gather(*tasks):
song_rows.extend((chunk or {}).get("info") or [])
songs = await _get_music_infos(song_rows)
img = info.get("imgurl")
return {
"list": songs,
"page": 1,
"limit": LIST_DETAIL_LIMIT,
"total": len(songs),
"source": "kg",
"info": {
"name": info.get("specialname"),
"img": img.replace("{size}", "240") if img else None,
"desc": info.get("intro"),
"author": info.get("nickname"),
"play_count": format_play_count(info.get("playcount")),
},
}
async def _get_list_info_by_chain(chain: str) -> Any:
if chain in _CACHE:
return _CACHE[chain]
resp = await http_fetch(
f"https://m.kugou.com/share/?chain={chain}&id={chain}",
headers={
"User-Agent": (
"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"
)
},
)
html = resp.body if isinstance(resp.body, str) else ""
match = re.search(r"var\sphpParam\s=\s({.+?});", html)
result = json.loads(match.group(1)) if match else None
_CACHE[chain] = result
return result
async def _get_user_list_detail_by_pc_chain(chain: str) -> list[dict[str, Any]]:
resp = await http_fetch(
f"http://www.kugou.com/share/{chain}.html",
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
)
},
)
html = resp.body if isinstance(resp.body, str) else ""
match = re.search(r"var\sdataFromSmarty\s=\s(\[.+?\])", html)
result = json.loads(match.group(1)) if match else []
return await _get_music_infos(result)
async def _get_user_list_detail_by_id(id: Any, page: int, limit: int) -> list[dict[str, Any]]:
signature = _handle_signature(id, page, limit)
info = await create_http_fetch(
"https://pubsongscdn.kugou.com/v2/get_other_list_file?"
f"srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist"
f"&page={page}&pagesize={limit}&specialid={id}&signature={signature}",
{
"headers": {
"Referer": "https://m3ws.kugou.com/share/index.php",
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 "
"(KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
),
"dfid": "-",
}
},
)
return await _get_music_infos((info or {}).get("info") or [])
async def _get_user_list_detail_4(song_info: dict[str, Any], chain: str, page: int) -> dict[str, Any]:
limit = 100
list_info, songs = await asyncio.gather(
_get_list_info_by_chain(chain),
_get_user_list_detail_by_id(song_info.get("id"), page, limit),
)
list_info = list_info or {}
img = list_info.get("imgurl")
return {
"list": songs or [],
"page": page,
"limit": limit,
"total": len(songs or []),
"source": "kg",
"info": {
"name": list_info.get("specialname"),
"img": img.replace("{size}", "240") if img else None,
"author": list_info.get("nickname"),
},
}
async def _get_user_list_detail_5(chain: str) -> dict[str, Any]:
list_info, songs = await asyncio.gather(_get_list_info_by_chain(chain), _get_user_list_detail_by_pc_chain(chain))
list_info = list_info or {}
img = list_info.get("imgurl")
return {
"list": songs or [],
"page": 1,
"limit": LIST_DETAIL_LIMIT,
"total": len(songs or []),
"source": "kg",
"info": {
"name": list_info.get("specialname"),
"img": img.replace("{size}", "240") if img else None,
"author": list_info.get("nickname"),
},
}
async def _get_user_list_detail_3(chain: str, page: int) -> dict[str, Any]:
song_info = await create_http_fetch(
f"http://m.kugou.com/schain/transfer?pagesize={LIST_DETAIL_LIMIT}&chain={chain}"
f"&su=1&page={page}&n=0.7928855356604456",
{
"headers": {
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 "
"(KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
)
}
},
)
song_info = song_info or {}
if not song_info.get("list"):
if song_info.get("global_collection_id"):
return await _get_user_list_detail_2(song_info["global_collection_id"])
try:
return await _get_user_list_detail_4(song_info, chain, page)
except Exception: # noqa: BLE001 - 降级到 PC 分享页
return await _get_user_list_detail_5(chain)
songs = await _get_music_infos(song_info.get("list") or [])
info = song_info.get("info") or {}
return {
"list": songs,
"page": 1,
"limit": LIST_DETAIL_LIMIT,
"total": len(songs),
"source": "kg",
"info": {"name": info.get("name"), "img": info.get("img"), "author": info.get("username")},
}
async def _get_user_list_detail_by_code(id: str) -> dict[str, Any]:
song_info = await create_http_fetch(
"http://t.kugou.com/command/",
{
"method": "POST",
"headers": {
"KG-RC": 1,
"KG-THash": "network_super_call.cpp:3676261689:379",
"User-Agent": "",
},
"body": {
"appid": 1001,
"clientver": 9020,
"mid": "21511157a05844bd085308bc76ef3343",
"clienttime": 640612895,
"key": "36164c4015e704673c588ee202b9ecb8",
"data": id,
},
},
)
song_info = song_info or {}
info = song_info.get("info") or {}
# type 1 单曲 / 2 歌单 / 3 电台 / 4 酷狗码 / 5 别人的播放队列
if info.get("type") == 2 and not info.get("global_collection_id"):
return await get_list_detail_by_special_id(info.get("id"))
if info.get("global_collection_id"):
return await _get_user_list_detail_2(info["global_collection_id"])
song_list: Any = None
if info.get("userid") is not None:
song_list = await create_http_fetch(
"http://www2.kugou.kugou.com/apps/kucodeAndShare/app/",
{
"method": "POST",
"headers": {
"KG-RC": 1,
"KG-THash": "network_super_call.cpp:3676261689:379",
"User-Agent": "",
},
"body": {
"appid": 1001,
"clientver": 9020,
"mid": "21511157a05844bd085308bc76ef3343",
"clienttime": 640612895,
"key": "36164c4015e704673c588ee202b9ecb8",
"data": {
"id": info.get("id"),
"type": 3,
"userid": info.get("userid"),
"collect_type": 0,
"page": 1,
"pagesize": info.get("count"),
},
},
},
)
rows = song_list or song_info.get("list") or []
songs = await _get_music_infos(rows)
img_size = info.get("img_size")
return {
"list": songs,
"page": 1,
"limit": info.get("count"),
"total": len(songs),
"source": "kg",
"info": {
"name": info.get("name"),
"img": img_size.replace("{size}", "240") if img_size else info.get("img"),
"author": info.get("username"),
},
}
async def _get_user_list_detail_by_link(body: dict[str, Any], link: str) -> dict[str, Any]:
list_info = (body.get("info") or {}).get("0") or {}
total = list_info.get("count") or 0
tasks: list[Any] = []
page = 0
while total:
limit = 90 if total > 90 else total
total -= limit
page += 1
url = re.sub(r"pagesize=\d+", f"pagesize={limit}", link)
url = re.sub(r"page=\d+", f"page={page}", url)
tasks.append(
create_http_fetch(
url,
{
"headers": {
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 "
"(KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
),
"Referer": link,
}
},
)
)
rows: list[Any] = []
for chunk in await asyncio.gather(*tasks):
rows.extend(((chunk or {}).get("list") or {}).get("info") or [])
songs = await _get_music_infos(rows)
pic = list_info.get("pic")
return {
"list": songs,
"page": page,
"limit": LIST_DETAIL_LIMIT,
"total": len(songs),
"source": "kg",
"info": {
"name": list_info.get("name"),
"img": pic.replace("{size}", "240") if pic else None,
"author": list_info.get("list_create_username"),
},
}
async def _get_user_list_detail(link: str, page: int, retry_num: int = 0) -> dict[str, Any]:
if retry_num > 3:
raise RuntimeError("link try max num")
if "#" in link:
link = re.sub(r"#.*$", "", link)
if "global_collection_id" in link:
return await _get_user_list_detail_2(re.sub(r"^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)", r"\1", link))
if "gcid_" in link:
match = re.search(r"gcid_\w+", link)
if match:
gcid_id = await _decode_gcid(match.group(0))
if gcid_id:
return await _get_user_list_detail_2(gcid_id)
if "chain=" in link:
return await _get_user_list_detail_3(re.sub(r"^.*?chain=(\w+)(?:&.*$|#.*$|$)", r"\1", link), page)
if ".html" in link:
if "zlist.html" in link:
link = re.sub(r"^(.*)zlist\.html", "https://m3ws.kugou.com/zlist/list", link)
if "pagesize" in link:
link = link.replace("pagesize=30", f"pagesize={LIST_DETAIL_LIMIT}").replace("page=1", f"page={page}")
else:
link += f"&pagesize={LIST_DETAIL_LIMIT}&page={page}"
elif "song.html" not in link:
return await _get_user_list_detail_3(re.sub(r".+\/(\w+).html(?:\?.*|&.*$|#.*$|$)", r"\1", link), page)
resp = await http_fetch(
link,
headers={
"User-Agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 "
"(KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"
),
"Referer": link,
},
)
location = resp.header("location") or None
if resp.status_code > 400:
return await _get_user_list_detail(link, page, retry_num + 1)
if location:
if "global_collection_id" in location:
return await _get_user_list_detail_2(
re.sub(r"^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)", r"\1", location)
)
if "gcid_" in location:
match = re.search(r"gcid_\w+", link)
if match:
gcid_id = await _decode_gcid(match.group(0))
if gcid_id:
return await _get_user_list_detail_2(gcid_id)
if "chain=" in location:
return await _get_user_list_detail_3(re.sub(r"^.*?chain=(\w+)(?:&.*$|#.*$|$)", r"\1", location), page)
if ".html" in location:
if "zlist.html" in location:
next_link = re.sub(r"^(.*)zlist\.html", "https://m3ws.kugou.com/zlist/list", location)
if "pagesize" in next_link:
next_link = next_link.replace("pagesize=30", f"pagesize={LIST_DETAIL_LIMIT}").replace(
"page=1", f"page={page}"
)
else:
next_link += f"&pagesize={LIST_DETAIL_LIMIT}&page={page}"
return await _get_user_list_detail(next_link, page, retry_num + 1)
return await _get_user_list_detail_3(re.sub(r".+\/(\w+).html(?:\?.*|&.*$|#.*$|$)", r"\1", location), page)
return await _get_user_list_detail(location, page, retry_num + 1)
if isinstance(resp.body, str):
body_text = resp.body
global_id = re.search(r'"global_collection_id":"(\w+)"', body_text)
gcid_id = global_id.group(1) if global_id else None
if not gcid_id:
match = re.search(r'"encode_gic":"(\w+)"', body_text) or re.search(r'"encode_src_gid":"(\w+)"', body_text)
if match:
gcid_id = await _decode_gcid(match.group(1))
if not gcid_id:
raise RuntimeError("get list error")
return await _get_user_list_detail_2(gcid_id)
body = resp.body if isinstance(resp.body, dict) else {}
if body.get("errcode") != 0:
return await _get_user_list_detail(link, page, retry_num + 1)
return await _get_user_list_detail_by_link(body, link)
async def get_list_detail(id: Any, page: int | None = None) -> dict[str, Any] | None:
"""歌单详情入口:按 id 形态分派到特殊页 / 链接 / 酷狗码。"""
value = str(id)
page_no = page or 1
if "special/single/" in value:
value = _LIST_DETAIL_LINK_EXP.sub(r"\1", value)
elif re.search(r"https?:", value):
# 修复形如 https://www.kugou.com/songlist/xxx/?uid=xxx&cover=http%3A%2F%2F... 的脏链接
return await _get_user_list_detail(re.sub(r"^.*?http", "http", value), page_no)
elif re.fullmatch(r"\d+", value):
return await _get_user_list_detail_by_code(value)
elif value.startswith("id_"):
value = value.replace("id_", "")
return await get_list_detail_by_special_id(value, page_no)