写一个命令行工具,看家里 Plex 服务器的状态:谁在看、在哪台设备上、是直接播放还是转码;最近添加了哪些片子;也可以让某个媒体库重新扫描(比如刚往 NAS 里拷了新片)。认证用的是 X-Plex-Token。
先看清成品
- 测试: pytest 2 项通过,用的是本地假 Plex 服务器(
fake_plex.py,JSON 结构是我按接口格式写的)。测到了会话显示(直接播放 / 硬件转码)、重新扫描的请求路径,以及 token 错误时的 401。- 真实容器:
identity返回Plex Media Server 1.43.5.11029-6bf8af3f0 已认领=False;sessions返回 0 个会话。- 没有测过: box 上的服务器没有认领,也没有媒体库,所以有真实数据的会话、最近添加和重新扫描都没有测过。
TranscodeSession里的transcodeHwRequested字段名来自假服务器的写法,没有拿真实转码会话核对过。
注意:只给媒体服务器放你有权使用的内容,比如自己拍的视频、自己购买并合法备份的光盘、公共领域或采用开放授权的作品。不要安装或分享盗版插件和资源,不要把媒体库共享给家庭以外的人,硬件转码等付费功能请按官方方式订阅(Plex Pass,价格以官网为准,本文不列)。遵守《Plex 服务条款》。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
plex_status.py |
identity / sessions / recent / sections / refresh 五个命令 |
fake_plex.py |
本地假服务器,用于测试 |
test_plex.py |
会话、扫描请求和 401 的测试 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Plex 服务器 | A 篇部署好并认领 | 局域网地址,例如 http://192.168.1.10:32400 |
| X-Plex-Token | 帮助中心有获取方法 | 在网页版打开任意一个条目的 XML,地址栏里的 X-Plex-Token 就是 |
| Python | 3.8 以上 | pip install requests |
文件结构
36-plex-C-src/
├── plex_status.py
├── fake_plex.py
└── test_plex.py
第 1 步:脚本
plex_status.py:
"""查看家里 Plex 服务器的状态:服务器信息、谁在看(是不是在转码)、最近添加,也可以让某个库重新扫描。
环境变量:PLEX_URL(例如 http://192.168.1.10:32400)、PLEX_TOKEN(X-Plex-Token,查找方法见 Plex 帮助中心)
用法:python plex_status.py identity | sessions | recent [数量] | sections | refresh 库ID
Token 等同于账号权限:不要发给别人,也不要写进代码或提交到仓库。"""
import os, sys
import requests
URL = os.environ.get("PLEX_URL", "http://127.0.0.1:32400").rstrip("/")
def get(path, method="GET", **params):
h = {"Accept": "application/json"}
if os.environ.get("PLEX_TOKEN"): h["X-Plex-Token"] = os.environ["PLEX_TOKEN"]
r = requests.request(method, URL + path, headers=h, params=params, timeout=10)
if r.status_code == 401: raise SystemExit("401:缺少 PLEX_TOKEN,或者 token 已失效")
r.raise_for_status()
return r.json()["MediaContainer"] if r.content else {}
def main(cmd="identity", *a):
if cmd == "identity":
c = get("/identity"); print(f"Plex Media Server {c['version']} 已认领={c.get('claimed')} id={c['machineIdentifier'][:8]}…")
elif cmd == "sessions":
c = get("/status/sessions")
for m in c.get("Metadata", []):
who, dev = m.get("User", {}).get("title", "?"), m.get("Player", {}).get("product", "?")
ts = m.get("TranscodeSession")
how = "直接播放" if not ts else f"转码 {'硬件' if ts.get('transcodeHwRequested') else '软件'} → {ts.get('videoCodec')}"
print(f"▶ {who} @ {dev}:{m.get('grandparentTitle', '') + ' ' if m.get('grandparentTitle') else ''}{m['title']} [{how}]")
print(f"共 {c.get('size', 0)} 个播放会话")
elif cmd == "recent":
for m in get("/library/recentlyAdded", **{"X-Plex-Container-Start": 0, "X-Plex-Container-Size": int(a[0]) if a else 10}).get("Metadata", []):
print(f"+ [{m.get('librarySectionTitle', '')}] {m.get('grandparentTitle', '') + ' - ' if m.get('grandparentTitle') else ''}{m['title']} ({m.get('year', '')})")
elif cmd == "sections":
for d in get("/library/sections").get("Directory", []):
print(f"{d['key']}: {d['title']} ({d['type']}) ← {', '.join(l['path'] for l in d.get('Location', []))}")
elif cmd == "refresh":
get(f"/library/sections/{a[0]}/refresh"); print(f"已请求重新扫描库 {a[0]}")
else: raise SystemExit(__doc__)
if __name__ == "__main__":
main(*sys.argv[1:])
帮助中心的示例是把 token 放在 URL 参数里(?X-Plex-Token=)。这里改成放在请求头里,token 就不会出现在代理日志和 shell 历史中。
第 2 步:测试
fake_plex.py:
"""本地假 Plex 服务器(只用于测试,返回的 JSON 结构仿照真实接口)。需要 X-Plex-Token: test-token"""
import json, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
DATA = {
"/identity": {"MediaContainer": {"claimed": "1", "machineIdentifier": "abcdef1234567890", "version": "1.43.5.11029-6bf8af3f0"}},
"/status/sessions": {"MediaContainer": {"size": 2, "Metadata": [
{"title": "Test Pattern", "User": {"title": "爸爸"}, "Player": {"product": "Plex for Android (TV)"}},
{"title": "s01e01", "grandparentTitle": "Lab Show", "User": {"title": "孩子"}, "Player": {"product": "Plex Web"},
"TranscodeSession": {"videoCodec": "h264", "transcodeHwRequested": True}}]}},
"/library/recentlyAdded": {"MediaContainer": {"Metadata": [{"librarySectionTitle": "Movies", "title": "Test Pattern", "year": 2026}]}},
"/library/sections": {"MediaContainer": {"Directory": [{"key": "1", "title": "Movies", "type": "movie", "Location": [{"path": "/data/Movies"}]}]}},
}
refreshed = []
class H(BaseHTTPRequestHandler):
def do_GET(self):
p = urlparse(self.path).path
if p != "/identity" and self.headers.get("X-Plex-Token") != "test-token":
self.send_response(401); self.end_headers(); return
if p.endswith("/refresh"): refreshed.append(p); self.send_response(200); self.end_headers(); return
body = json.dumps(DATA[p]).encode()
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(body)
def log_message(self, *a): pass
def start():
s = ThreadingHTTPServer(("127.0.0.1", 0), H); threading.Thread(target=s.serve_forever, daemon=True).start(); return s
test_plex.py:
import pytest, fake_plex, plex_status
@pytest.fixture
def srv(monkeypatch):
s = fake_plex.start(); monkeypatch.setattr(plex_status, "URL", f"http://127.0.0.1:{s.server_port}")
yield s; s.shutdown()
def test_sessions(srv, monkeypatch, capsys):
monkeypatch.setenv("PLEX_TOKEN", "test-token")
plex_status.main("sessions"); out = capsys.readouterr().out
assert "爸爸 @ Plex for Android (TV):Test Pattern [直接播放]" in out and "孩子 @ Plex Web:Lab Show s01e01 [转码 硬件 → h264]" in out
def test_refresh_and_401(srv, monkeypatch):
monkeypatch.setenv("PLEX_TOKEN", "test-token"); plex_status.main("refresh", "1")
assert fake_plex.refreshed[-1] == "/library/sections/1/refresh"
monkeypatch.setenv("PLEX_TOKEN", "wrong")
with pytest.raises(SystemExit, match="401"): plex_status.main("sections")
$ python -m pytest -q
2 passed
$ PLEX_URL=http://127.0.0.1:32400 python plex_status.py identity
Plex Media Server 1.43.5.11029-6bf8af3f0 已认领=False id=2a758233…
检查点:认领服务器、填好 PLEX_TOKEN 以后,用手机播放一部片,然后运行 sessions,应该能看到你的用户名、设备名,以及是直接播放还是转码。
调试示例:返回的是 XML,不是 JSON
第一版没有带 Accept 请求头,实际运行时:
text/xml;charset=utf-8
JSONDecodeError Expecting value: line 1 column 1 (char 0)
Plex 默认返回 XML。加上 Accept: application/json 以后就返回 JSON 了,所以脚本在每个请求里都带上这个头。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 401 | token 不对,或者已经失效 | 按帮助中心的方法重新获取 |
JSONDecodeError |
没有带 Accept 头 | 用脚本里的 get() |
| sessions 一直是 0 | 当前没有人在播放 | 先播放一部再查 |
| refresh 之后没有新片 | 文件名不规范 | 先用 A 篇的检查器 |
模板规则
- Token 等同于账号权限:只放在环境变量里。
- 只读命令和会修改数据的命令(refresh)分开。
- 先用假服务器测试,再连接真实服务器。
项目资源
有了 sessions 命令,“谁在转码、为什么卡”一眼就能看出来。服务器状态用这几个命令就能查,不用登录网页。