如何在B站签名展示正在玩的Steam游戏


本项目参考了 WormwakerJackal Client

众所周知,B站的个性签名是静态的,大多数时候内容只随用户偶尔更新而改变。而Steam好友界面的游戏状态是动态的——你经常能看见右下角时不时弹出来“XXX 正在玩:XXX”。

那有没有办法让B站签名像Steam游戏状态一样动态展现一些东西,比如正在玩的游戏呢?

这就是今天这个玩具做的事情。

这个玩具由三个模块组合而成,实现了从本机读取正在游玩的Steam游戏,到将这个游戏更新到B站的签名中去的过程、

下面会依次讲解每个模块的思路和实现逻辑


从注册表里获取SteamAppID

Steam 在运行游戏时会往 Windows 注册表里写一个值:

HKCU\Software\Valve\Steam\RunningAppID

这个值是当前正在运行的游戏的 AppID。如果你在玩星露谷物语,那AppID就是4313150;如果你在玩火山的女儿,那AppID就是1669980

import winreg

def get_running_steam_appid():
    try:
        key = winreg.OpenKey(
            winreg.HKEY_CURRENT_USER,
            r"Software\Valve\Steam"
        )
        value, _ = winreg.QueryValueEx(key, "RunningAppID")
        winreg.CloseKey(key)

        if value and value != 0:
            return int(value)  
        return None  
    except FileNotFoundError:
        return None  

AppID 变游戏名

拿到App ID还不够,你不能把一串数字贴签名上,你得把它翻译成人类能读的游戏名

Steam 把已安装游戏的名字也写进了注册表:

HKCU\Software\Valve\Steam\Apps\{AppID}\Name

直接读就完事了

import winreg

def lookup_game_name(appid):
    if appid is None:
        return None

    try:
        key = winreg.OpenKey(
            winreg.HKEY_CURRENT_USER,
            rf"Software\Valve\Steam\Apps\{appid}"
        )
        name, _ = winreg.QueryValueEx(key, "Name")
        winreg.CloseKey(key)
        return name  
    except FileNotFoundError:
        return None  

把游戏名贴到签名上

B站有一个接口可以修改当前登录用户的签名:

POST https://api.bilibili.com/x/member/web/sign/update

需要带上 bili_jctSESSDATA,这两个都可以从浏览器 Cookie 里获取。

签名的格式设计成换行分隔,上面是正在玩的游戏名,下面是原本的签名

正在玩:Wallpaper Engine
音为源于真心

这样关闭游戏时只要删掉第一行就行

import requests

SIGN_MAX_LENGTH = 70  # B站签名字数上限

def update_signature(sessdata, bili_jct, game_name, original_sign):
    if game_name:
        prefix = f"正在玩:{game_name}"
        new_sign = f"{prefix}\n{original_sign}"[:SIGN_MAX_LENGTH]
    else:
        new_sign = original_sign  # 没在玩,恢复原样

    resp = requests.post(
        "https://api.bilibili.com/x/member/web/sign/update",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Referer": "https://www.bilibili.com",
        },
        data={"user_sign": new_sign, "csrf": bili_jct},
        cookies={"SESSDATA": sessdata, "bili_jct": bili_jct},
        timeout=10,
    )
    return resp.json().get("code") == 0

主循环:事件驱动

这个玩具现在拥有了”查当前游戏——更新签名“的功能,但是怎样让它能在游戏开启或者关闭的时候自主地完成”查当前游戏“呢?

你当然可以选择每隔一段时间查询一次,但Win32 API 提供了RegNotifyChangeKeyValue。你可以将它理解为,注册表不变时线程直接挂在内核的等待队列里,注册表一旦发生变化立马完成“查当前游戏”。比起轮询,它的好处是CPU 占用显著的低。

import ctypes
from ctypes import wintypes

_advapi32 = ctypes.windll.advapi32
_kernel32 = ctypes.windll.kernel32

def wait_for_registry_change():
    # 打开注册表键的监听句柄
    raw_hkey = ctypes.c_void_p()
    _advapi32.RegOpenKeyExW(
        0x80000001, r"Software\Valve\Steam",
        0, 0x0010, ctypes.byref(raw_hkey),
    )
    # 创建 Win32 事件
    h_event = _kernel32.CreateEventW(None, True, False, None)
    # 注册异步通知:Steam 改值 → 内核唤醒
    _advapi32.RegNotifyChangeKeyValue(
        raw_hkey, False, 0x00000004, h_event, True,
    )
    # 挂起等待,每 1 秒超时检查一次 Python 的关闭信号
    while True:
        ret = _kernel32.WaitForSingleObject(h_event, 1000)
        if ret == 0:    # 注册表变了
            return True
        # 超时就返回,让主循环检查关闭标志
        return False

主循环变成:

while not shutdown_flag:
    if wait_for_registry_change():
        appid = get_running_steam_appid()
        if appid != last_appid:
            last_appid = appid
            game_name = lookup_game_name(appid) if appid else None
            update_signature(..., game_name, original_sign)

实现游戏启动 → 内核唤醒进程 → 签名改变

游戏关闭 → 内核唤醒进程 → 签名恢复。


开机自启

用注册表 HKCU\Software\Microsoft\Windows\CurrentVersion\Run 加一条启动项,指向一个 .vbs 脚本:

Set fso = CreateObject("Scripting.FileSystemObject")
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "pythonw.exe """ & scriptDir & "\main.py""", 0, False

最后

B站用的 SESSDATA 是短期令牌,几天到几周后就会过期,但每次过期就手动去浏览器复制新的 SESSDATA 太过繁琐

好在B站登录接口除了返回 SESSDATA ,还会返回一个叫 refresh_token 的长寿令牌。 refresh_token 不会过期,且可以帮我们自动更新 SESSDATA
每次更新流程如下:

检查要不要更新

def _needs_refresh(sessdata, bili_jct):
 resp = _session.get(_INFO_URL,
     params={"csrf": bili_jct},
     cookies={"SESSDATA": sessdata})
 data = resp.json()
 refresh = data["data"].get("refresh", False)
 ts = data["data"].get("timestamp", 0)
 return refresh, ts

可以每次启动/每隔一小时检查一次

用B站的公钥给时间戳加密

def _generate_correspond_path(timestamp_ms):
 cipher = PKCS1_OAEP.new(_RSA_KEY, SHA256)
 encrypted = cipher.encrypt(f"refresh_{timestamp_ms}".encode())
 return binascii.b2a_hex(encrypted).decode()

提取新的 SESSDATAbili_jct

for chunk in set_cookie.split(", "):
 for field in chunk.split("; "):
     if field.startswith("SESSDATA="):
         new_sessdata = field[len("SESSDATA="):]
     elif field.startswith("bili_jct="):
         new_bili_jct = field[len("bili_jct="):]

PC端签名换行显示有点问题


文章作者: C26H52
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 C26H52 !
  目录