跳转至

login

登录相关 API

check_expired async

check_expired(credential: Credential) -> bool

检查凭据是否过期

参数 描述
credential

用户凭证

类型: Credential

源代码位于: qqmusic_api/login.py
async def check_expired(credential: Credential) -> bool:
    """检查凭据是否过期

    Args:
        credential: 用户凭证
    """
    api = ApiRequest(
        "music.UserInfo.userInfoServer",
        "GetLoginUserInfo",
        params={},
        credential=credential,
    )

    try:
        await api()
        return False
    except CredentialExpiredError:
        return True

refresh_cookies async

refresh_cookies(credential: Credential) -> bool

刷新 Cookies

Note

刷新无效 cookie 需要 refresh_keyrefresh_token 字段

参数 描述
credential

用户凭证

类型: Credential

返回 描述
bool

是否刷新成功

源代码位于: qqmusic_api/login.py
async def refresh_cookies(credential: Credential) -> bool:
    """刷新 Cookies

    Note:
        刷新无效 cookie 需要 `refresh_key` 和 `refresh_token` 字段

    Args:
        credential: 用户凭证

    Returns:
        是否刷新成功
    """
    params = {
        "refresh_key": credential.refresh_key,
        "refresh_token": credential.refresh_token,
        "musickey": credential.musickey,
        "musicid": credential.musicid,
    }

    api = ApiRequest[[], dict[str, Any]](
        "music.login.LoginServer",
        "Login",
        common={"tmeLoginType": str(credential.login_type)},
        params=params,
        credential=credential,
    )

    try:
        resp = await api()
        c = credential.from_cookies_dict(resp)
        credential.__dict__.update(c.__dict__)
        return True
    except CredentialExpiredError:
        return False

QRCodeLoginEvents

Bases: Enum

二维码登录状态

  • SCAN: 等待扫描二维码
  • CONF: 已扫码未确认登录
  • TIMEOUT: 二维码已过期
  • DONE: 扫码成功
  • REFUSE: 拒绝登录
  • OTHER: 未知情况

get_by_value classmethod

get_by_value(value: int)

根据传入的值查找对应的枚举成员

源代码位于: qqmusic_api/login.py
@classmethod
def get_by_value(cls, value: int):
    """根据传入的值查找对应的枚举成员"""
    for member in cls:
        if value in member.value:
            return member
    return cls.OTHER

PhoneLoginEvents

Bases: Enum

手机登录状态

  • SEND: 发送成功
  • CAPTCHA: 需要滑块验证
  • FREQUENCY: 频繁操作
  • OTHER: 未知情况

QRLoginType

Bases: Enum

登录类型

  • QQ: QQ登录
  • WX: 微信登录
  • MOBILE: 手机客户端登录

QR dataclass

QR(data: bytes, qr_type: QRLoginType, mimetype: str, identifier: str)

二维码

属性 描述
data

二维码图像数据

类型: bytes

qr_type

二维码类型

类型: QRLoginType

mimetype

二维码图像类型

类型: str

identifier

标识符

类型: str

save

save(path: str | Path = '.')

保存二维码

参数 描述
path

保存文件夹

类型: str | Path 默认: '.'

源代码位于: qqmusic_api/login.py
def save(self, path: Path | str = "."):
    """保存二维码

    Args:
        path: 保存文件夹
    """
    if not self.data:
        return None
    path = Path(path)
    path.mkdir(parents=True, exist_ok=True)
    file_path = (
        path
        / f"{self.qr_type.value}-{uuid4()}{mimetypes.guess_extension(self.mimetype) if self.mimetype else None or '.png'}"
    )
    file_path.write_bytes(self.data)
    return file_path

get_qrcode async

get_qrcode(login_type: QRLoginType) -> QR

获取登录二维码

源代码位于: qqmusic_api/login.py
async def get_qrcode(login_type: QRLoginType) -> QR:
    """获取登录二维码"""
    if login_type == QRLoginType.WX:
        return await _get_wx_qr()
    if login_type == QRLoginType.MOBILE:
        return await _get_mobile_qr()
    return await _get_qq_qr()

check_qrcode async

check_qrcode(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]

检查二维码状态

源代码位于: qqmusic_api/login.py
async def check_qrcode(qrcode: QR) -> tuple[QRCodeLoginEvents, Credential | None]:
    """检查二维码状态"""
    if qrcode.qr_type == QRLoginType.WX:
        return await _check_wx_qr(qrcode)
    return await _check_qq_qr(qrcode)

check_mobile_qr async

check_mobile_qr(qrcode: QR) -> AsyncGenerator[tuple[QRCodeLoginEvents, Credential | None], None]

检查手机客户端登录二维码状态

参数 描述
qrcode

二维码对象

类型: QR

源代码位于: qqmusic_api/login.py
async def check_mobile_qr(qrcode: QR) -> AsyncGenerator[tuple[QRCodeLoginEvents, Credential | None], None]:  # noqa: C901
    """检查手机客户端登录二维码状态

    Args:
        qrcode: 二维码对象
    """
    if qrcode.qr_type != QRLoginType.MOBILE:
        raise ValueError(f"不支持{qrcode.qr_type}类型的二维码")
    client_id = f"{int(time() * 1000)}{random.randint(1000, 9999)}"
    client = MqttClient(client_id=client_id, host="mu.y.qq.com", port=443, path="/ws/handshake", keep_alive=45)
    async with client:
        max_redirects = 3
        for attempt in range(max_redirects + 1):
            try:
                await client.connect(
                    properties={
                        PropertyId.AUTH_METHOD: "pass",
                        PropertyId.USER_PROPERTY: [
                            ("tmeAppID", "qqmusic"),
                            ("business", "management"),
                            ("hashTag", qrcode.identifier),
                            ("clientTag", "management.user"),
                            ("userID", qrcode.identifier),
                        ],
                    },
                    headers={
                        "Origin": "https://y.qq.com",
                        "Referer": "https://y.qq.com/",
                        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
                    },
                )
                break
            except MqttRedirectError as e:
                if attempt == max_redirects:
                    raise LoginError("[MobileLogin] 重定向次数过多") from e

                client.path = f"/ws/handshake/{e.new_address}"
                continue
        await client.subscribe(
            f"management.qrcode_login/{qrcode.identifier}",
            properties={
                PropertyId.USER_PROPERTY: [
                    ("authorization", "tmelogin"),
                    ("pubsub", "unicast"),
                ]
            },
        )
        yield QRCodeLoginEvents.SCAN, None
        async for msg in client.messages():
            event_type = msg.properties.get("type")

            match event_type:
                case "scanned":
                    yield QRCodeLoginEvents.CONF, None

                case "cookies":
                    data = msg.json
                    if data:
                        cookies: dict[str, Any] = {
                            k: v.get("value") if isinstance(v, dict) else v for k, v in data.get("cookies", {}).items()
                        }
                        res = await ApiRequest[[], dict[str, Any]](
                            "music.login.LoginServer",
                            "Login",
                            params={
                                "musicid": int(cookies.get("qqmusic_uin", 0)),
                                "qrCodeID": qrcode.identifier,
                                "token": cookies.get("qqmusic_key", ""),
                            },
                            common={"tmeLoginType": "6"},
                        )()

                        yield QRCodeLoginEvents.DONE, Credential.from_cookies_dict(res)
                    else:
                        yield QRCodeLoginEvents.OTHER, None
                    break
                case "canceled":
                    yield QRCodeLoginEvents.REFUSE, None
                    break
                case "timeout":
                    yield QRCodeLoginEvents.TIMEOUT, None
                    break
                case "loginFailed":
                    yield QRCodeLoginEvents.OTHER, None
                    break
                case _:
                    pass

send_authcode async

send_authcode(phone: int, country_code: int = 86) -> tuple[PhoneLoginEvents, str | None]

发送验证码

参数 描述
phone

手机号

类型: int

country_code

国家码

类型: int 默认: 86

源代码位于: qqmusic_api/login.py
async def send_authcode(phone: int, country_code: int = 86) -> tuple[PhoneLoginEvents, str | None]:
    """发送验证码

    Args:
        phone: 手机号
        country_code: 国家码
    """
    resp = await ApiRequest[[], dict[str, Any]](
        "music.login.LoginServer",
        "SendPhoneAuthCode",
        common={"tmeLoginMethod": "3"},
        params={
            "tmeAppid": "qqmusic",
            "phoneNo": str(phone),
            "areaCode": str(country_code),
        },
        ignore_code=True,
    )()

    match resp["code"]:
        case 20276:
            return PhoneLoginEvents.CAPTCHA, resp["data"]["securityURL"]
        case 100001:
            return PhoneLoginEvents.FREQUENCY, None
        case 0:
            return PhoneLoginEvents.SEND, None
    return PhoneLoginEvents.OTHER, resp["data"]["errMsg"]

phone_authorize async

phone_authorize(phone: int, auth_code: int, country_code: int = 86) -> Credential

验证码鉴权

参数 描述
phone

手机号

类型: int

auth_code

验证码

类型: int

country_code

国家码

类型: int 默认: 86

源代码位于: qqmusic_api/login.py
async def phone_authorize(phone: int, auth_code: int, country_code: int = 86) -> Credential:
    """验证码鉴权

    Args:
        phone: 手机号
        auth_code: 验证码
        country_code: 国家码
    """
    resp = await ApiRequest[[], dict[str, Any]](
        "music.login.LoginServer",
        "Login",
        common={"tmeLoginMethod": "3", "tmeLoginType": "0"},
        params={"code": str(auth_code), "phoneNo": str(phone), "areaCode": str(country_code), "loginMode": 1},
        ignore_code=True,
    )()
    match resp["code"]:
        case 20274:
            raise LoginError("[PhoneLogin] 设备数量限制")
        case 20271:
            raise LoginError("[PhoneLogin] 验证码错误或已鉴权")
        case 0:
            return Credential.from_cookies_dict(resp["data"])
    raise LoginError("[PhoneLogin] 未知原因导致鉴权失败")