网络协议

用手机或电脑上的脚本遥控 Kodi:JSON-RPC 实战

本文用 Python 和 Kodi 官方 JSON-RPC API 远程浏览视频源、播放与控制媒体,并演示通知和媒体库扫描;提供局域网访问与凭据保护建议。

BINBHO · 图文教程

用手机或电脑上的脚本遥控客厅里的 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

盒子装好以后,日常操作用这个脚本就够了:播放、暂停、提醒都能远程完成。