跳转至

client

core.client

API 客户端核心实现. 整合网络传输、鉴权与业务模块访问.

Client

Client(
    credential: Credential | None = None,
    *,
    platform: Platform | None = None,
    device_path: str | None = None,
    rate: float | None = None,
    capacity: float | None = None,
    connect_retries: int | None = None,
    proxies: ProxyType | None = None,
    cert: TLSClientCertType | None = None,
    hooks: AsyncHookType[PreparedRequest | Response]
    | None = None,
    verify: TLSVerifyType | None = None,
)

QQMusic API Client.

初始化客户端实例.

PARAMETER DESCRIPTION
credential

全局默认凭证.

TYPE: Credential | None DEFAULT: None

platform

全局默认请求平台.

TYPE: Platform | None DEFAULT: None

device_path

设备信息文件路径.

TYPE: str | None DEFAULT: None

rate

请求速率限制 (请求/秒). 默认为 10.

TYPE: float | None DEFAULT: None

capacity

令牌桶容量, 允许的突发请求数. 默认为 50.

TYPE: float | None DEFAULT: None

connect_retries

连接建立失败时的最大重试次数. 默认为 2.

TYPE: int | None DEFAULT: None

proxies

代理配置, 详见 niquests 文档.

TYPE: ProxyType | None DEFAULT: None

cert

TLS 客户端证书配置, 详见 niquests 文档.

TYPE: TLSClientCertType | None DEFAULT: None

verify

TLS 证书验证配置, 详见 niquests 文档.

TYPE: TLSVerifyType | None DEFAULT: None

hooks

请求/响应钩子, 详见 niquests 文档.

TYPE: AsyncHookType[PreparedRequest | Response] | None DEFAULT: None

Source code in qqmusic_api/core/client.py
def __init__(
    self,
    credential: Credential | None = None,
    *,
    platform: Platform | None = None,
    device_path: str | None = None,
    rate: float | None = None,
    capacity: float | None = None,
    connect_retries: int | None = None,
    proxies: ProxyType | None = None,
    cert: TLSClientCertType | None = None,
    hooks: AsyncHookType[PreparedRequest | Response] | None = None,
    verify: TLSVerifyType | None = None,
):
    """初始化客户端实例.

    Args:
        credential: 全局默认凭证.
        platform: 全局默认请求平台.
        device_path: 设备信息文件路径.
        rate: 请求速率限制 (请求/秒). 默认为 10.
        capacity: 令牌桶容量, 允许的突发请求数. 默认为 50.
        connect_retries: 连接建立失败时的最大重试次数. 默认为 2.
        proxies: 代理配置, 详见 niquests 文档.
        cert: TLS 客户端证书配置, 详见 niquests 文档.
        verify: TLS 证书验证配置, 详见 niquests 文档.
        hooks: 请求/响应钩子, 详见 niquests 文档.
    """
    self._session = AsyncSession(
        multiplexed=True,
        hooks=AsyncTokenBucketLimiter(rate=rate or 10, capacity=capacity or 50),
        happy_eyeballs=True,
        retries=Retry(
            total=connect_retries or 2,
            connect=connect_retries or 2,
            read=0,
            redirect=0,
            status=0,
            other=0,
            backoff_factor=0.2,
        ),
        allow_incoming_cookies=False,
    )
    self.credential = credential or Credential()
    self.platform = platform or Platform.ANDROID

    self.proxies = proxies
    self.cert = cert
    self.verify = verify
    self.hooks = hooks

    self._device_store = DeviceManager(device_path)

    self._version_policy: VersionPolicy = DEFAULT_VERSION_POLICY
    self._session_lock = anyio.Lock()
    self._session_initialized = False
    self._qimei_manager = QimeiManager(
        device_store=self._device_store,
        app_version=self._version_policy.get_qimei_app_version(),
        sdk_version=self._version_policy.get_qimei_sdk_version(),
        session=self._session,
    )

comment cached property

comment: CommentApi

评论模块.

private_message cached property

private_message: PrivateMessageApi

私信模块.

recommend cached property

recommend: RecommendApi

推荐模块.

top cached property

top: TopApi

排行榜模块.

album cached property

album: AlbumApi

专辑模块.

mv cached property

mv: MvApi

MV 模块.

login cached property

login: LoginApi

登录模块.

search cached property

search: SearchApi

搜索模块.

lyric cached property

lyric: LyricApi

歌词模块.

singer cached property

singer: SingerApi

歌手模块.

song cached property

song: SongApi

歌曲模块.

songlist cached property

songlist: SonglistApi

歌单模块.

user cached property

user: UserApi

用户模块.

close async

close()

关闭客户端连接.

Source code in qqmusic_api/core/client.py
async def close(self):
    """关闭客户端连接."""
    await self._session.close()

request async

request(
    method: str,
    url: str,
    credential: Credential | None = None,
    platform: Platform | None = None,
    *,
    lazy: bool = False,
    **kwargs: Any,
)

发送带有凭证和 User-Agent 的 HTTP 请求.

自动装配指定的客户端平台 User-Agent 及对应凭证的 Cookies.

PARAMETER DESCRIPTION
method

HTTP 方法.

TYPE: str

url

URL 地址.

TYPE: str

credential

请求凭证.

TYPE: Credential | None DEFAULT: None

platform

请求平台.

TYPE: Platform | None DEFAULT: None

lazy

是否延迟发送请求.

TYPE: bool DEFAULT: False

**kwargs

其他参数.

TYPE: Any DEFAULT: {}

Source code in qqmusic_api/core/client.py
async def request(
    self,
    method: str,
    url: str,
    credential: Credential | None = None,
    platform: Platform | None = None,
    *,
    lazy: bool = False,
    **kwargs: Any,
):
    """发送带有凭证和 User-Agent 的 HTTP 请求.

    自动装配指定的客户端平台 User-Agent 及对应凭证的 Cookies.

    Args:
        method: HTTP 方法.
        url: URL 地址.
        credential: 请求凭证.
        platform: 请求平台.
        lazy: 是否延迟发送请求.
        **kwargs: 其他参数.
    """
    cred = credential or self.credential
    user_cookies = kwargs.pop("cookies", {})
    cookies: dict[str, str] = {}
    if cred.musicid:
        cookies["uin"] = cred.str_musicid or str(cred.musicid)
        cookies["qqmusic_uin"] = cred.str_musicid or str(cred.musicid)
    if cred.musickey:
        cookies["qm_keyst"] = cred.musickey
        cookies["qqmusic_key"] = cred.musickey
    cookies.update(user_cookies)
    if cookies:
        kwargs["cookies"] = cookies

    headers = kwargs.get("headers", {})
    if "User-Agent" not in headers:
        headers["User-Agent"] = await self._get_user_agent(platform)
    kwargs["headers"] = headers

    try:
        resp = await self._session.request(
            method,
            url,
            **kwargs,
            proxies=self.proxies,
            hooks=self.hooks,
            cert=self.cert,
            verify=self.verify,
        )
        if not lazy:
            await self._session.gather(resp)
        return resp
    except RequestException as exc:
        raise NetworkError(str(exc)) from exc

request_api async

request_api(
    data: list[RequestItem],
    comm: dict[str, Any] | None = None,
    credential: Credential | None = None,
    platform: Platform | None = None,
    *,
    override_comm: bool = False,
    is_jce: bool = False,
    lazy: bool = False,
    sign: bool = False,
) -> Response

发送 API 请求.

Source code in qqmusic_api/core/client.py
async def request_api(
    self,
    data: list[RequestItem],
    comm: dict[str, Any] | None = None,
    credential: Credential | None = None,
    platform: Platform | None = None,
    *,
    override_comm: bool = False,
    is_jce: bool = False,
    lazy: bool = False,
    sign: bool = False,
) -> Response:
    """发送 API 请求."""
    target_platform = Platform.ANDROID if is_jce else platform or self.platform
    if target_platform == Platform.ANDROID:
        await self._ensure_session()
    device = await self._device_store.get_device()
    if override_comm:
        finalcomm = (comm or {}).copy()
    else:
        finalcomm = self._version_policy.build_comm(
            platform=target_platform,
            credential=credential or self.credential,
            device=device,
            qimei=cast("dict[str, str]", await self._qimei_manager.get_cached())
            if target_platform == Platform.ANDROID
            else None,
            guid=device.open_udid,
        )
        if comm:
            finalcomm.update(comm)

    user_agent = await self._get_user_agent(target_platform)

    try:
        if is_jce:
            for k, v in finalcomm.items():
                if not isinstance(v, str):
                    finalcomm[k] = str(v)
            content = JceRequest(
                finalcomm,
                {
                    f"req_{idx}": JceRequestItem(
                        module=req["module"],
                        method=req["method"],
                        param=TarsDict(cast("dict[int, Any]", req["param"])),
                    )
                    for idx, req in enumerate(data)
                },
            ).encode()
            resp = await self._session.post(
                "http://u.y.qq.com/cgi-bin/musicw.fcg",
                data=content,
                headers={"User-Agent": user_agent},
                proxies=self.proxies,
                hooks=self.hooks,
                cert=self.cert,
                verify=self.verify,
            )
            if not lazy:
                await self._session.gather(resp)
            return resp

        payload: dict[str, Any] = {
            "comm": finalcomm,
        }
        params: dict[str, str] = {}
        for idx, req in enumerate(data):
            payload[f"req_{idx}"] = {
                "module": req["module"],
                "method": req["method"],
                "param": req["param"] if req["preserve_bool"] else bool_to_int(req["param"]),
            }

        if sign:
            params["_"] = str(int(time.time() * 1000))
            params["sign"] = zzc_sign(json.dumps(payload))

        resp = await self._session.post(
            "https://u.y.qq.com/cgi-bin/musicu.fcg" if not sign else "https://u.y.qq.com/cgi-bin/musics.fcg",
            json=payload,
            params=params,
            headers={"User-Agent": user_agent},
            proxies=self.proxies,
            hooks=self.hooks,
            cert=self.cert,
            verify=self.verify,
        )
        if not lazy:
            await self._session.gather(resp)

        return resp
    except RequestException as exc:
        raise NetworkError(str(exc)) from exc

gather async

gather(
    requests: list[Request[RequestResultT]],
    *,
    batch_size: int = ...,
    return_exceptions: Literal[False] = False,
) -> list[RequestResultT]
gather(
    requests: list[Request[RequestResultT]],
    *,
    batch_size: int = ...,
    return_exceptions: Literal[True],
) -> list[RequestResultT | Exception]
gather(
    requests: list[Request[Any]],
    *,
    batch_size: int = ...,
    return_exceptions: Literal[False] = False,
) -> list[Any]
gather(
    requests: list[Request[Any]],
    *,
    batch_size: int = ...,
    return_exceptions: Literal[True],
) -> list[Any | Exception]
gather(
    requests: list[Request[Any]],
    *,
    batch_size: int = 20,
    return_exceptions: bool = False,
) -> list[Any]

并发执行多个请求描述符并按输入顺序返回解析结果.

可合并的请求会按协议、平台、公共参数和凭证分组, 每组按
batch_size 拆分为批量请求发送。响应解析失败时, 默认抛出
第一个异常; 当 return_exceptions 为 True 时, 异常会作为对应
位置的结果返回。

PARAMETER DESCRIPTION
requests

待执行的请求描述符列表.

TYPE: list[Request[Any]]

batch_size

每个批量请求包含的最大请求数.

TYPE: int DEFAULT: 20

return_exceptions

是否将单项解析异常作为结果返回.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[Any]

requests 顺序一致的解析结果列表.

RAISES DESCRIPTION
ValueError

batch_size 小于等于 0, 响应为空, 响应缺少对应
请求项, 或结果未能完整回填时抛出.

Source code in qqmusic_api/core/client.py
async def gather(
    self,
    requests: list[Request[Any]],
    *,
    batch_size: int = 20,
    return_exceptions: bool = False,
) -> list[Any]:
    """并发执行多个请求描述符并按输入顺序返回解析结果.

    可合并的请求会按协议、平台、公共参数和凭证分组, 每组按
    `batch_size` 拆分为批量请求发送。响应解析失败时, 默认抛出
    第一个异常; 当 `return_exceptions` 为 True 时, 异常会作为对应
    位置的结果返回。

    Args:
        requests: 待执行的请求描述符列表.
        batch_size: 每个批量请求包含的最大请求数.
        return_exceptions: 是否将单项解析异常作为结果返回.

    Returns:
        与 `requests` 顺序一致的解析结果列表.

    Raises:
        ValueError: 当 `batch_size` 小于等于 0, 响应为空, 响应缺少对应
            请求项, 或结果未能完整回填时抛出.
    """
    if batch_size <= 0:
        raise ValueError("batch_size 必须大于 0")

    if not requests:
        return []

    grouped_indices: dict[Any, list[int]] = defaultdict(list)
    for index, request in enumerate(requests):
        grouped_indices[request._group_key].append(index)

    batch_responses: list[tuple[list[int], Response]] = []

    for indices in grouped_indices.values():
        base_req = requests[indices[0]]

        for start in range(0, len(indices), batch_size):
            batch_indices = indices[start : start + batch_size]
            response_task = await self.request_api(
                data=[
                    {
                        "module": requests[i].module,
                        "method": requests[i].method,
                        "param": requests[i].param,
                        "preserve_bool": requests[i].preserve_bool,
                    }
                    for i in batch_indices
                ],
                comm=base_req.comm,
                override_comm=base_req.override_comm,
                credential=base_req.credential,
                platform=base_req.platform,
                is_jce=base_req.is_jce,
                lazy=True,
                sign=base_req.sign,
            )
            batch_responses.append((batch_indices, response_task))

    try:
        await self._session.gather(*(resp for _, resp in batch_responses))
    except RequestException as exc:
        raise NetworkError(str(exc)) from exc

    results: list[Any] = [_SENTINEL] * len(requests)

    for batch_indices, response in batch_responses:
        data = self._vaildate_resp(response, is_jce=requests[batch_indices[0]].is_jce)
        for batch_index, req_index in enumerate(batch_indices):
            request = requests[req_index]
            try:
                results[req_index] = self._parse_cgi_item(
                    data[f"req_{batch_index}"],
                    request,
                )
            except Exception as exc:
                if return_exceptions:
                    results[req_index] = exc
                else:
                    raise

    missing_indexes = [i for i, res in enumerate(results) if res is _SENTINEL]
    if missing_indexes:
        raise ApiDataError(f"缺少以下索引结果: {missing_indexes}")

    return results

execute async

execute(request: Request[RequestResultT]) -> RequestResultT

执行单个请求描述符并解析响应结果.

PARAMETER DESCRIPTION
request

待执行的请求描述符.

TYPE: Request[RequestResultT]

RETURNS DESCRIPTION
RequestResultT

解析后的响应数据或响应模型.

RAISES DESCRIPTION
ValueError

当响应为空、业务返回码非 0 或响应缺少 req_0 时抛出.

Source code in qqmusic_api/core/client.py
async def execute(self, request: Request[RequestResultT]) -> RequestResultT:
    """执行单个请求描述符并解析响应结果.

    Args:
        request: 待执行的请求描述符.

    Returns:
        解析后的响应数据或响应模型.

    Raises:
        ValueError: 当响应为空、业务返回码非 0 或响应缺少 `req_0` 时抛出.
    """
    resp = await self.request_api(
        data=[
            {
                "module": request.module,
                "method": request.method,
                "param": request.param,
                "preserve_bool": request.preserve_bool,
            }
        ],
        comm=request.comm,
        override_comm=request.override_comm,
        credential=request.credential,
        platform=request.platform,
        is_jce=request.is_jce,
        sign=request.sign,
    )
    return self._parse_cgi_item(self._vaildate_resp(resp, is_jce=request.is_jce)["req_0"], request)