用手机或电脑上的脚本遥控客厅里的 Kodi(LibreELEC、CoreELEC 或桌面版都可以):查看源、浏览文件夹、播放指定文件、暂停、停止、在电视上弹出通知(比如“8 点暂停一下吃饭”)、触发媒体库扫描。用的是 Kodi 官方的 JSON-RPC 接口。
先看清成品
- 环境: 在 box 上用 Xvfb 跑了真实的 Kodi 21.2(Debian 包),
JSONRPC.Version返回 13.5.0。- 实测命令: ping、notify、sources、ls、play、status、pause、stop 都实测过;密码错误时返回 401。测试视频是 ffmpeg 生成的测试图样。
- box 上的限制: box 没有声卡和 GPU,Kodi 用软件渲染,播放进度走得很慢(5 秒后才到 2%)。这是 box 环境的问题,不代表盒子上的表现。
- 单元测试: pytest 2 项通过。
- 没有测过: 没有在真实的 LibreELEC/CoreELEC 盒子上测试。
注意:Kodi 官方版本本身不带任何内容。只播放你有权使用的媒体(自己拍的、自己购买的、公共领域或开放授权的),不要安装盗版插件或第三方“全能”仓库。Team Kodi 明确表示不认可盗版内容,官方论坛也禁止讨论盗版插件。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
kodi_remote.py |
ping / sources / notify / ls / play / status / pause / stop / scan |
sources.xml |
示例:把 NAS 上的 SMB 共享添加成视频源 |
test_remote.py |
用模拟的接口返回测试状态显示和“不在源里”的提示 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Kodi | 设置 → 服务 → 控制 → 允许通过 HTTP 远程控制 | 一定要设置用户名和密码 |
| 网络 | 和 Kodi 在同一个局域网 | 不要把 Kodi 的端口直接暴露到公网 |
| Python | 3.8 以上 | pip install requests |
文件结构
37-kodi-C-src/
├── kodi_remote.py
├── sources.xml
└── test_remote.py
第 1 步:脚本
kodi_remote.py:
"""用 JSON-RPC 遥控局域网里的 Kodi(LibreELEC / CoreELEC 盒子都一样)。
先在 Kodi 里打开:设置 → 服务 → 控制 → “允许通过 HTTP 远程控制”,并设置用户名和密码。
环境变量:KODI_URL(例如 http://192.168.1.50:8080/jsonrpc)、KODI_USER、KODI_PASS
用法:
python kodi_remote.py ping | sources | notify 标题 内容 | ls 目录 | play 文件 | status | pause | stop | scan"""
import os, sys, time
import requests
URL = os.environ.get("KODI_URL", "http://127.0.0.1:8080/jsonrpc")
AUTH = (os.environ.get("KODI_USER", "kodi"), os.environ.get("KODI_PASS", ""))
def rpc(method, **params):
r = requests.post(URL, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, auth=AUTH, timeout=10)
if r.status_code == 401: raise SystemExit("401:用户名或密码不对(Kodi 设置 → 服务 → 控制)")
r.raise_for_status()
js = r.json()
if "error" in js: raise SystemExit(f"{method} 出错:{js['error']}")
return js["result"]
def active_video():
for p in rpc("Player.GetActivePlayers"):
if p["type"] == "video": return p["playerid"]
return None
def fmt(t): return f"{t['hours']:02d}:{t['minutes']:02d}:{t['seconds']:02d}"
def main(cmd, *args):
if cmd == "ping": print(rpc("JSONRPC.Ping"), rpc("JSONRPC.Version")["version"])
elif cmd == "notify": rpc("GUI.ShowNotification", title=args[0], message=args[1]); print("已发送通知")
elif cmd == "sources":
for s in rpc("Files.GetSources", media="video").get("sources", []): print(f"{s['label']}: {s['file']}")
elif cmd == "ls":
roots = [s["file"] for s in rpc("Files.GetSources", media="video").get("sources", [])]
if not any(args[0].rstrip("/") + "/" == r or args[0].startswith(r) for r in roots):
raise SystemExit(f"{args[0]} 不在 Kodi 的视频源里({roots})。Kodi 只允许浏览已添加的源,请先在 Kodi 里添加")
for f in rpc("Files.GetDirectory", directory=args[0], media="video").get("files", []):
print(("📁 " if f["filetype"] == "directory" else "🎬 ") + f["file"])
elif cmd == "play":
rpc("Player.Open", item={"file": args[0]})
for _ in range(20): # 播放器启动需要一点时间
if active_video() is not None: break
time.sleep(0.5)
main("status")
elif cmd == "status":
pid = active_video()
if pid is None: return print("没有在播放视频")
p = rpc("Player.GetProperties", playerid=pid, properties=["time", "totaltime", "percentage", "speed"])
item = rpc("Player.GetItem", playerid=pid, properties=["file"])["item"]
print(f"{'▶' if p['speed'] else '⏸'} {item.get('label')} {fmt(p['time'])}/{fmt(p['totaltime'])} ({p['percentage']:.0f}%)")
elif cmd == "pause":
pid = active_video(); print("没有在播放" if pid is None else rpc("Player.PlayPause", playerid=pid))
elif cmd == "stop":
pid = active_video(); print("没有在播放" if pid is None else rpc("Player.Stop", playerid=pid))
elif cmd == "scan": print(rpc("VideoLibrary.Scan"))
else: raise SystemExit(__doc__)
if __name__ == "__main__":
main(*sys.argv[1:]) if len(sys.argv) > 1 else print(__doc__)
sources.xml:
<!-- 放到 Kodi 的 userdata 目录(LibreELEC/CoreELEC:/storage/.kodi/userdata/),也可以在界面里添加源。路径换成你自己的 NAS 共享 -->
<sources>
<video>
<default pathversion="1"></default>
<source><name>Movies</name><path pathversion="1">smb://nas.local/media/Movies/</path><allowsharing>true</allowsharing></source>
<source><name>TV Shows</name><path pathversion="1">smb://nas.local/media/TV Shows/</path><allowsharing>true</allowsharing></source>
</video>
</sources>
第 2 步:运行
$ export KODI_URL=http://客厅盒子IP:8080/jsonrpc KODI_USER=kodi KODI_PASS=你设置的密码
$ python kodi_remote.py ping
pong {'major': 13, 'minor': 5, 'patch': 0}
$ python kodi_remote.py notify 晚饭 "8 点暂停一下"
已发送通知
$ python kodi_remote.py ls /tmp/media/Movies/
📁 /tmp/media/Movies/Test Pattern (2026)/
$ python kodi_remote.py play "/tmp/media/Movies/Test Pattern (2026)/Test Pattern (2026).mkv"
▶ Test Pattern (2026).mkv 00:00:00/00:00:20 (0%)
$ python kodi_remote.py pause
{'speed': 0}
$ python kodi_remote.py status
⏸ Test Pattern (2026).mkv 00:00:00/00:00:20 (3%)
$ KODI_PASS=wrong python kodi_remote.py ping
401:用户名或密码不对(Kodi 设置 → 服务 → 控制)
(以上是 box 上 Kodi 21.2 的真实输出,路径是 box 上的测试目录。)
test_remote.py:
import kodi_remote as k
class R:
def __init__(self, js, code=200): self._js, self.status_code = js, code
def json(self): return self._js
def raise_for_status(self): pass
def test_status_and_auth(monkeypatch, capsys):
replies = {"Player.GetActivePlayers": [{"playerid": 1, "type": "video"}],
"Player.GetProperties": {"time": {"hours": 0, "minutes": 1, "seconds": 5}, "totaltime": {"hours": 1, "minutes": 0, "seconds": 0}, "percentage": 1.8, "speed": 1},
"Player.GetItem": {"item": {"label": "x.mkv"}}}
sent = []
def post(url, json, auth, timeout):
sent.append(json); return R({"result": replies[json["method"]]})
monkeypatch.setattr(k.requests, "post", post)
k.main("status")
assert "▶ x.mkv 00:01:05/01:00:00" in capsys.readouterr().out
assert sent[1]["params"] == {"playerid": 1, "properties": ["time", "totaltime", "percentage", "speed"]}
def test_outside_source(monkeypatch):
monkeypatch.setattr(k.requests, "post", lambda *a, **kw: R({"result": {"sources": [{"file": "/media/Movies/", "label": "M"}]}}))
import pytest
with pytest.raises(SystemExit, match="不在 Kodi 的视频源里"): k.main("ls", "/media/TV/")
检查点:notify 之后,电视上会弹出通知;status 显示的时间应该随着播放往前走。
调试示例:目录明明存在,却返回 “Invalid params”
我对 /tmp/media/TV Shows/ 调用 Files.GetDirectory,返回:
Files.GetDirectory 出错:{'code': -32602, 'message': 'Invalid params.'}
同样的调用对 /tmp/media/Movies/ 就是正常的。再用 Files.GetSources 一查,原因找到了:Kodi 里只添加了 Movies 这一个源。在这次实测中,Kodi 不允许 JSON-RPC 浏览已添加的源以外的路径,而返回的错误信息看不出这一点。所以脚本在调用 ls 前先查一遍源,不在源里就直接给出明确的提示:
/tmp/media/TV Shows/ 不在 Kodi 的视频源里(['/tmp/media/Movies/'])。Kodi 只允许浏览已添加的源,请先在 Kodi 里添加
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 连接被拒绝 | 没有打开 HTTP 控制,或者端口不对 | 设置 → 服务 → 控制,查看端口号 |
| 401 | 用户名或密码不对 | 在同一个设置页面里核对 |
Invalid params |
路径不在源里,或者参数名写错了 | 先运行 sources;参数名照 JSON-RPC v13 文档写 |
| 播放 SMB 上的文件失败 | 源需要登录 | 在 Kodi 里添加源的时候保存好凭据 |
模板规则
- 遥控只在局域网里使用,一定要设置密码。
- 只播放合法来源的文件。
- 把错误码翻译成用户能看懂、能照着做的提示。
项目资源
| 用途 | 地址 |
|---|---|
| JSON-RPC API v13 | https://kodi.wiki/view/JSON-RPC_API/v13 |
| 设置 → 服务 → 控制 | https://kodi.wiki/view/Settings/Services/Control |
| Is Kodi legal?(官方) | https://kodi.tv/article/is-kodi-legal/ |
| Kodi 论坛规则与盗版政策 | https://kodi.wiki/view/Official:Forum_rules |
盒子装好以后,日常操作用这个脚本就够了:播放、暂停、提醒都能远程完成。