写一个命令行工具:给它一份频道清单,它会告诉你哪些频道正在直播、多少人在看、在播什么。工具用 client credentials 方式拿 app access token,再调用 Helix 的 Get Streams;每 100 个频道合成一次请求,遇到 429 会按
Ratelimit-Reset等待后重试。
先看清成品
pytest 跑了 1 个测试,通过:150 个频道分成 100 + 50 两批,第一次返回 429 后会重试。用官方 Twitch CLI v1.1.24 的 mock-api(本地模拟服务器)完整跑了一遍:拿 token、查询,输出“● marstonkid357 99368 人在看”和“○ twitchdev 未直播”,这些都是 mock 生成的假数据。连真实服务器时我用的是假凭据:token 接口返回
HTTP 400 invalid client,Get Streams 返回401 Invalid OAuth token。没有用真实的 Client ID 和 Secret 查询过真实的直播数据。
注意:只使用 Twitch 官方 API、EventSub 和官方嵌入播放器,并遵守《Twitch 开发者服务协议》。不要抓取或转录直播流,不要遮挡或去掉播放器上的广告,不要共享账号或 Client Secret,也不要用 VPN 或代理绕过地区限制。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
live_check.py |
拿 app token → 分批调用 Get Streams → 打印直播状态 |
channels.txt |
你关注的频道登录名 |
| 可以切换地址 | 设置 TWITCH_AUTH_URL / TWITCH_API_URL 就能改连 mock-api |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Twitch 应用 | 在开发者控制台注册 | 拿到 Client ID 和 Client Secret(Secret 只能放在服务器端) |
| Python | 3.8 以上 | pip install requests |
| Twitch CLI | 可选 | 用 mock-api 离线测试 |
官方要点(已和 dev.twitch.tv 核对)
| 项目 | 内容 |
|---|---|
| 拿 token | POST https://id.twitch.tv/oauth2/token,参数 client_id、client_secret、grant_type=client_credentials |
| 查直播 | GET https://api.twitch.tv/helix/streams,app token 或 user token 都可以;请求头带 Authorization: Bearer 和 Client-Id |
| 参数 | user_login 最多 100 个,每个都要单独写一次(&user_login=a&user_login=b);first 取 1–100,默认 20 |
| 返回 | 只返回正在直播的频道;字段有 viewer_count、game_name、title、started_at 等;tag_ids 已经弃用,改用 tags |
| 限流 | 响应头 Ratelimit-Limit/Remaining/Reset;收到 429 时,按 Reset 时间等待 |
文件结构
35-twitch-A-src/
├── live_check.py
├── channels.txt
└── test_live.py
第 1 步:脚本
live_check.py:
"""查询一组 Twitch 频道当前是否在直播(Helix Get Streams + app access token)。
环境变量:TWITCH_CLIENT_ID、TWITCH_CLIENT_SECRET
可选:TWITCH_AUTH_URL / TWITCH_API_URL(默认指向官方地址;测试时可改成 twitch-cli mock-api)
用法:python live_check.py channels.txt"""
import os, sys, time
import requests
AUTH = os.environ.get("TWITCH_AUTH_URL", "https://id.twitch.tv/oauth2/token")
API = os.environ.get("TWITCH_API_URL", "https://api.twitch.tv/helix")
def app_token(cid, secret):
r = requests.post(AUTH, data={"client_id": cid, "client_secret": secret, "grant_type": "client_credentials"}, timeout=15)
if r.status_code != 200:
raise SystemExit(f"获取 token 失败:HTTP {r.status_code} {r.text[:200]}")
return r.json()["access_token"]
def get_streams(logins, cid, token):
"""user_login 每次最多 100 个;返回 {login: stream}。只有正在直播的频道会出现在结果里。"""
live, h = {}, {"Authorization": f"Bearer {token}", "Client-Id": cid}
for i in range(0, len(logins), 100):
batch = logins[i:i + 100]
while True:
r = requests.get(f"{API}/streams", params=[("user_login", l) for l in batch] + [("first", 100)], headers=h, timeout=15)
if r.status_code == 429: # 等到 Ratelimit-Reset 再重试
time.sleep(max(1, int(r.headers.get("Ratelimit-Reset", time.time() + 1)) - int(time.time())))
continue
if r.status_code != 200:
raise SystemExit(f"Get Streams 失败:HTTP {r.status_code} {r.text[:200]}")
break
for s in r.json()["data"]:
live[s["user_login"].lower()] = s
return live
if __name__ == "__main__":
logins = [l.strip().lower() for l in open(sys.argv[1], encoding="utf-8") if l.strip() and not l.startswith("#")]
cid = os.environ["TWITCH_CLIENT_ID"]
live = get_streams(logins, cid, app_token(cid, os.environ["TWITCH_CLIENT_SECRET"]))
for l in logins:
s = live.get(l)
print(f"● {l} {s['viewer_count']} 人在看 [{s['game_name']}] {s['title']}" if s else f"○ {l} 未直播")
channels.txt:
# 每行一个频道登录名(login),不区分大小写
marstonkid357
twitchdev
第 2 步:单元测试
test_live.py:
import live_check
class R:
def __init__(self, code, js=None, headers=None): self.status_code, self._js, self.headers, self.text = code, js, headers or {}, ""
def json(self): return self._js
def test_batches_and_429(monkeypatch):
calls = []
def fake_get(url, params, headers, timeout):
calls.append([v for k, v in params if k == "user_login"])
if len(calls) == 1: return R(429, headers={"Ratelimit-Reset": "0"})
return R(200, {"data": [{"user_login": "U5", "viewer_count": 1, "game_name": "g", "title": "t"}]})
monkeypatch.setattr(live_check.requests, "get", fake_get)
monkeypatch.setattr(live_check.time, "sleep", lambda s: None)
live = live_check.get_streams([f"u{i}" for i in range(150)], "cid", "tok")
assert [len(c) for c in calls] == [100, 100, 50] # 第一次 429 后重试同一批
assert "u5" in live
第 3 步:用 Twitch CLI 的 mock-api 离线运行
twitch mock-api generate # 生成本地数据库,并打印一组 mock 用的 Client-ID / Secret
twitch mock-api start -p 8765 # 默认端口 8080,如果被占用就换一个
export TWITCH_AUTH_URL=http://localhost:8765/auth/token
export TWITCH_API_URL=http://localhost:8765/mock
export TWITCH_CLIENT_ID=<generate 打印的 Client-ID>
export TWITCH_CLIENT_SECRET=<generate 打印的 Secret>
python live_check.py channels.txt
● marstonkid357 99368 人在看 [Just Making a CLI] Sample stream!
○ twitchdev 未直播
marstonkid357 是 mock 随机生成的用户。换成你自己生成的数据时,请把 channels.txt 里的第一行改成 generate 输出里的用户。连接真实服务器时,去掉两个 URL 变量,填入你自己应用的凭据即可。
检查点:凭据填错时,脚本会立刻停下,并打印接口返回的原文。我用假凭据实测,得到的是:
获取 token 失败:HTTP 400 {"status":400,"message":"invalid client"}
调试示例:多个频道用逗号连在一起
很多 API 习惯用 user_login=a,b 传多个值。我对 mock-api 实测了两种写法:
user_login=marstonkid357,twitchdev -> {"data":[]}
user_login=marstonkid357&user_login=twitchdev -> {"data":[{"user_login":"marstonkid357", ...}]}
用逗号连接时不会报错,只会返回空列表,看起来就像“大家都没在直播”。文档要求每个值单独写一个参数,所以脚本用 [("user_login", l) for l in batch] 这种元组列表,交给 requests 去编码。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
400 invalid client |
Client ID 或 Secret 不对 | 去开发者控制台核对,必要时重新生成 Secret |
401 Invalid OAuth token |
token 过期,或者和 Client-Id 不匹配 | 重新拿 token;两者必须属于同一个应用 |
| 所有频道都显示未直播 | 多个频道用逗号连接了,或者填的不是登录名 | 每个频道单独写一个参数,填 login 而不是显示名 |
| 429 | 请求太频繁 | 按 Ratelimit-Reset 等待;脚本已经处理 |
模板规则
- Client Secret 只放在环境变量或服务器端,不要放进前端代码,也不要提交到仓库。
- 先用 mock-api 测试,再用真实凭据。
- 定时轮询时,间隔要合理;需要实时通知的话,用 EventSub(见 B 篇)。
项目资源
| 用途 | 地址 |
|---|---|
| Get Streams 参考 | https://dev.twitch.tv/docs/api/reference/#get-streams |
| 获取 token | https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/ |
| API 指南(限流、分页) | https://dev.twitch.tv/docs/api/guide/ |
| mock-api 命令 | https://dev.twitch.tv/docs/cli/mock-api-command/ |
| Twitch CLI | https://github.com/twitchdev/twitch-cli |
| CLI 文档 | https://dev.twitch.tv/docs/cli/ |
| Twitch 开发者服务协议 | https://www.twitch.tv/p/legal/developer-agreement/ |
| 注册应用(开发者控制台) | https://dev.twitch.tv/console/apps |
用一个 token 加一个接口,就能做出实用的“开播检查”。最容易踩的坑不是鉴权,而是参数写法:写错了不会报错,只会悄悄返回空结果。