File size: 4,206 Bytes
d731d8e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | """网易云 API 代理:识曲 / 歌曲 URL / 歌曲详情。
所有调用直连网易云公开接口,不依赖 api-enhanced 子服务。
"""
from __future__ import annotations
import logging
from typing import Any, Optional
import httpx
logger = logging.getLogger(__name__)
_NCM_HEADERS = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36",
"Referer": "https://music.163.com",
}
_TIMEOUT = 10.0
async def audio_match(audio_fp: str, duration: int = 3) -> dict:
"""调用网易云听歌识曲接口。
Args:
audio_fp: base64 编码的音频指纹
duration: 录音时长(秒)
Returns:
网易云原始响应 dict,结构 {data: {result: [{song, startTime, score}]}}
"""
url = "https://interface.music.163.com/api/music/audio/match"
params = {
"sessionId": "0123456789abcdef",
"algorithmCode": "shazam_v2",
"duration": str(duration),
"rawdata": audio_fp,
"times": "1",
"decrypt": "1",
}
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(url, params=params, headers=_NCM_HEADERS)
resp.raise_for_status()
return resp.json()
async def get_song_url(song_id: str, *, unblock: bool = False, level: str = "standard") -> dict:
"""获取试听 URL。
优先调用网易云公开接口(无 cookie,仅试听片段)。
若需要完整播放,由前端开关 unblock=true 触发,但本代理不实现 unblock
(需要 api-enhanced 的 unblockmusic-utils,这里仅返回公开接口结果)。
Returns:
{id, url, type, freeTrialInfo, fee}
"""
url = "https://music.163.com/api/song/enhance/player/url"
params = {"ids": f"[{song_id}]", "br": "128000"}
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(url, params=params, headers=_NCM_HEADERS)
resp.raise_for_status()
data = resp.json()
item = (data.get("data") or [{}])[0]
return {
"id": item.get("id"),
"url": item.get("url"),
"type": item.get("type"),
"freeTrialInfo": None,
"fee": item.get("fee"),
"unblock_supported": False, # 标记给前端:本后端不支持 unblock
}
async def get_song_detail(song_id: str) -> Optional[dict]:
"""获取歌曲详情(封面、歌手等)。"""
url = "https://music.163.com/api/v1/song/detail"
params = {"ids": f"[{song_id}]"}
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(url, params=params, headers=_NCM_HEADERS)
resp.raise_for_status()
data = resp.json()
songs = data.get("songs") or []
if not songs:
return None
s = songs[0]
return {
"id": s.get("id"),
"name": s.get("name"),
"artists": "/".join(a.get("name", "") for a in s.get("artists", [])),
"album": (s.get("album") or {}).get("name"),
"cover": (s.get("album") or {}).get("picUrl"),
"duration": s.get("duration"),
}
async def get_song_detail_batch(song_ids: list[str]) -> list[Optional[dict]]:
"""批量获取歌曲详情。公开接口不支持批量,逐个调用。"""
# 网易云公开接口 ids 参数实际是 JSON 数组字符串,可以传多个
# 但为稳定性考虑,单个查询更可靠
url = "https://music.163.com/api/v1/song/detail"
ids_json = "[" + ",".join(str(i) for i in song_ids) + "]"
params = {"ids": ids_json}
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(url, params=params, headers=_NCM_HEADERS)
resp.raise_for_status()
data = resp.json()
songs = data.get("songs") or []
by_id = {}
for s in songs:
by_id[str(s.get("id"))] = {
"id": s.get("id"),
"name": s.get("name"),
"artists": "/".join(a.get("name", "") for a in s.get("artists", [])),
"album": (s.get("album") or {}).get("name"),
"cover": (s.get("album") or {}).get("picUrl"),
"duration": s.get("duration"),
}
return [by_id.get(str(sid)) for sid in song_ids]
|