Files
MusicServer/app/sdk/platforms/fivesing/music_search.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

74 lines
3.1 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.
"""5sing 歌曲搜索(对应 sdk/5sing/musicSearch.ts,上游 fivesing/song.go 的 Search)。
接口: GET http://search.5sing.kugou.com/home/json?keyword=&sort=1&page=&filter=0&type=0
返回 { list: [...], type, pageInfo: { cur, totalCount, totalPages } },每页 10 条(服务端固定)。
内容种类:typeEname = yc(原创) / fc(翻唱) / bz(伴奏),中文名在 typeName;
本移植把它作为统一种类字段 songType 暴露(songmid 约定仍为 "<songId>|<typeEname>")。
时长:上游用 songSize 估算(songSize*8/320000 秒);实测多数条目 songSize=0 → interval 返回 0。
"""
from __future__ import annotations
from typing import Any
from app.sdk.common.utils import as_dict, as_list, format_play_time
from app.sdk.platforms.fivesing.util import (
SEARCH_API,
build_song_link,
encode_uri_component,
estimate_duration,
fetch_5sing_json,
kind_name,
remove_em_tags,
unescape_html,
)
def map_search_song(item: dict[str, Any]) -> dict[str, Any]:
"""上游 Search 的 list 项 → 统一 Song 形状(source 统一 '5sing')。"""
song_type = str(item.get("typeEname") or "")
duration = estimate_duration(item.get("songSize"))
return {
"songmid": f"{item.get('songId', '')}|{song_type}",
"name": remove_em_tags(unescape_html(item.get("songName"))),
"singer": remove_em_tags(unescape_html(item.get("singer") or item.get("nickName"))),
"albumId": "",
"albumName": "",
"interval": format_play_time(duration) if duration > 0 else 0,
"img": None,
"lrc": None,
"types": [],
"_types": {},
# 统一种类字段(上游 typeEname / typeName)
"songType": song_type,
"typeName": str(item.get("typeName") or kind_name(song_type)),
"size": int(item.get("songSize") or 0),
"link": build_song_link(song_type, item.get("songId")),
"source": "5sing",
}
async def search(keyword: str, page: int = 1, limit: int = 20, retry_num: int = 0) -> dict[str, Any]:
del retry_num # 契约参数,重试已在 fetch_5sing_json 内
word = str(keyword or "").strip()
page_no = max(1, int(page or 1))
page_size = max(1, int(limit or 20))
empty = {"list": [], "allPage": 0, "total": 0, "limit": page_size, "source": "5sing"}
if not word:
return empty
url = f"{SEARCH_API}?keyword={encode_uri_component(word)}&sort=1&page={page_no}&filter=0&type=0"
resp = await fetch_5sing_json(url)
if resp.status_code != 200:
raise RuntimeError(f"5sing 搜索失败(HTTP {resp.status_code})")
body = as_dict(resp.body)
raw_list = as_list(body.get("list"))
songs = [map_search_song(item) for item in raw_list[:page_size] if isinstance(item, dict)]
page_info = as_dict(body.get("pageInfo"))
total = int(page_info.get("totalCount") or 0) or len(songs)
# 服务端每页 10 条(固定),allPage 直接用 pageInfo.totalPages
all_page = int(page_info.get("totalPages") or 0) or (page_no + 1 if len(songs) >= page_size else page_no)
return {"list": songs, "allPage": all_page, "total": total, "limit": page_size, "source": "5sing"}