云端部署

写一个命令行工具查看 Plex 服务器状态

用 Python 编写 Plex 状态 CLI,查看服务器信息、播放会话、最近添加和媒体库,并通过本地假服务器测试认证错误与扫描请求。

BINBHO · 图文教程

写一个命令行工具,看家里 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)分开。
  • 先用假服务器测试,再连接真实服务器。

项目资源

用途 地址
获取 X-Plex-Token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
远程访问(官方) https://support.plex.tv/articles/200289506-remote-access/
Plex 服务条款 https://www.plex.tv/about/privacy-legal/plex-terms-of-service/

有了 sessions 命令,“谁在转码、为什么卡”一眼就能看出来。服务器状态用这几个命令就能查,不用登录网页。