├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── example.py ├── kodijson ├── __init__.py └── kodijson.py ├── setup.cfg ├── setup.py ├── tests └── test_kodijson.py ├── tox.ini └── version.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Docstrings and comments use max_line_length = 79 14 | [*.py] 15 | max_line_length = 119 16 | 17 | # Use 2 spaces for the HTML files 18 | [*.html] 19 | indent_size = 2 20 | 21 | # The JSON files contain newlines inconsistently 22 | [*.json] 23 | indent_size = 2 24 | insert_final_newline = ignore 25 | 26 | [**/admin/js/vendor/**] 27 | indent_style = ignore 28 | indent_size = ignore 29 | 30 | # Minified JavaScript files shouldn't be changed 31 | [**.min.js] 32 | indent_style = ignore 33 | insert_final_newline = ignore 34 | 35 | # Makefiles always use tabs for indentation 36 | [Makefile] 37 | indent_style = tab 38 | 39 | # Batch files use tabs for indentation 40 | [*.bat] 41 | indent_style = tab 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,vim 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | 96 | ### Vim ### 97 | # swap 98 | [._]*.s[a-w][a-z] 99 | [._]s[a-w][a-z] 100 | # session 101 | Session.vim 102 | # temporary 103 | .netrwhist 104 | *~ 105 | # auto-generated tag files 106 | tags 107 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | cache: pip 5 | 6 | matrix: 7 | include: 8 | - python: 2.6 9 | env: TOX_ENV=py26 10 | - python: 2.7 11 | env: TOX_ENV=py27 12 | - python: 3.3 13 | env: TOX_ENV=py33 14 | - python: 3.4 15 | env: TOX_ENV=py34 16 | - python: 3.5 17 | env: TOX_ENV=py35 18 | - python: pypy 19 | env: TOX_ENV=pypy 20 | - python: 3.5 21 | env: TOX_ENV=py35 22 | - python: nightly 23 | env: TOX_ENV=py36 24 | 25 | script: tox -e $TOX_ENV 26 | 27 | install: 28 | - pip install tox 29 | 30 | 31 | after_success: 32 | # Report coverage results to codecov.io 33 | # and export tox environment variables 34 | - pip install codecov 35 | - codecov -e TOX_ENV TRAVIS_OS_NAME 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include version.py 2 | include LICENSE 3 | include README.md 4 | 5 | graft kodijson 6 | graft tests 7 | 8 | 9 | global-exclude __pycache__ 10 | global-exclude *.py[co] 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Python Versions| |Wheel status| |Licence| |Travis| |codecov| 2 | 3 | python kodi json client 4 | ======================= 5 | 6 | Simple python module that allow kodi control over HTTP Json API. 7 | Virtually support all availables commands. 8 | 9 | Install it : 10 | 11 | .. code:: bash 12 | 13 | pip install kodi-json 14 | 15 | Usages examples : 16 | 17 | Client instanciation 18 | 19 | .. code:: python 20 | 21 | from kodijson import Kodi, PLAYER_VIDEO 22 | #Login with default kodi/kodi credentials 23 | kodi = Kodi("http://YOURHOST/jsonrpc") 24 | 25 | #Login with custom credentials 26 | kodi = Kodi("http://YOURHOST/jsonrpc", "login", "password") 27 | 28 | Ping kodi 29 | 30 | .. code:: python 31 | 32 | print kodi.JSONRPC.Ping() 33 | 34 | UI interaction : 35 | 36 | .. code:: python 37 | 38 | # Navigate throught windows 39 | kodi.GUI.ActivateWindow({"window":"home"}) 40 | kodi.GUI.ActivateWindow({"window":"weather"}) 41 | 42 | # Show some notifiations : 43 | kodi.GUI.ShowNotification({"title":"Title", "message":"Hello notif"}) 44 | 45 | # ...and so on 46 | 47 | Parameters can alos be passed as python parameters: 48 | 49 | .. code:: python 50 | 51 | kodi.GUI.ActivateWindow(window="home") 52 | kodi.GUI.ActivateWindow(window="weather") 53 | kodi.GUI.ShowNotification(title="Title", message = "Hello notif") 54 | 55 | Library interaction : 56 | 57 | .. code:: python 58 | 59 | kodi.VideoLibrary.Scan() 60 | kodi.VideoLibrary.Clean() 61 | # ...and so on 62 | 63 | Everything to build a script thats act as a full remote 64 | 65 | .. code:: python 66 | 67 | kodi.Application.SetMute({"mute":True}) 68 | kodi.Player.PlayPause([PLAYER_VIDEO]) 69 | kodi.Player.Stop([PLAYER_VIDEO]) 70 | kodi.Input.Left() 71 | kodi.Input.Right() 72 | kodi.Input.Up() 73 | kodi.Input.Down() 74 | kodi.Input.Back() 75 | kodi.Input.Down() 76 | kodi.Input.Info() 77 | # ...and so on 78 | 79 | See http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6 for availables 80 | commands. 81 | 82 | Every kodi namespaces are accessible from the instanciated kodi client. 83 | 84 | Every commands presents in the `API 85 | documentation `__ 86 | should be available. 87 | 88 | You can take a look at 89 | `xbmc-client `__ for an 90 | implementation example. 91 | 92 | Contribute 93 | ---------- 94 | 95 | Please make your PR on the branch develop :) 96 | 97 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/kodi-json.svg?maxAge=2592000 98 | :target: https://pypi.python.org/pypi/kodi-json/ 99 | .. |Wheel status| image:: https://img.shields.io/pypi/wheel/kodi-json.svg?maxAge=2592000 100 | :target: https://pypi.python.org/pypi/kodi-json/ 101 | .. |Licence| image:: https://img.shields.io/pypi/l/kodi-json.svg?maxAge=2592000 102 | :target: https://pypi.python.org/pypi/kodi-json/ 103 | .. |Travis| image:: https://img.shields.io/travis/jcsaaddupuy/python-kodi.svg?maxAge=2592000 104 | :target: https://pypi.python.org/pypi/kodi-json/ 105 | .. |codecov| image:: https://codecov.io/gh/jcsaaddupuy/python-kodi/branch/master/graph/badge.svg 106 | :target: https://codecov.io/gh/jcsaaddupuy/python-kodi 107 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/bin/env/python 2 | 3 | from kodijson import Kodi, PLAYER_VIDEO 4 | 5 | if __name__ == "__main__": 6 | kodi = Kodi("http://YOURHOST/jsonrpc") 7 | # JSON RPC 8 | # Ping 9 | print(kodi.JSONRPC.Ping()) 10 | 11 | # Gui 12 | kodi.GUI.ActivateWindow({"window": "home"}) 13 | kodi.GUI.ActivateWindow({"window": "weather"}) 14 | # Show a notification 15 | kodi.GUI.ShowNotification({"title": "Title", "message": "Hello notif"}) 16 | # Application 17 | kodi.Application.SetMute({"mute": True}) 18 | kodi.Application.SetMute({"mute": False}) 19 | # Video library 20 | kodi.VideoLibrary.Scan() 21 | kodi.VideoLibrary.Clean() 22 | # Query the video library 23 | 24 | print(kodi.VideoLibrary.GetTVShows({ 25 | "filter": {"field": "playcount", "operator": "is", "value": "0"}, 26 | "limits": {"start": 0, "end": 75}, 27 | "properties": ["art", "genre", "plot", "title", "originaltitle", 28 | "year", "rating", "thumbnail", "playcount", "file", 29 | "fanart"], 30 | "sort": {"order": "ascending", "method": "label"} 31 | }, id="libTvShows")) 32 | # Player 33 | kodi.Player.PlayPause([PLAYER_VIDEO]) 34 | kodi.Player.Stop([PLAYER_VIDEO]) 35 | -------------------------------------------------------------------------------- /kodijson/__init__.py: -------------------------------------------------------------------------------- 1 | from .kodijson import * # NOQA 2 | -------------------------------------------------------------------------------- /kodijson/kodijson.py: -------------------------------------------------------------------------------- 1 | """XBMC/Kodi jsonclient library module.""" 2 | import json 3 | import requests 4 | 5 | # this list will be extended with types dynamically defined 6 | __all__ = ["PLAYER_VIDEO", 7 | "KodiTransport", 8 | "KodiJsonTransport", 9 | "Kodi", 10 | "KodiNamespace", ] 11 | 12 | # Kodi constant 13 | PLAYER_VIDEO = 1 14 | 15 | # Dynamic namespace class injection 16 | __KODI_NAMESPACES__ = ( 17 | "Addons", "Application", "AudioLibrary", "Favourites", "Files", "GUI", 18 | "Input", "JSONRPC", "Playlist", "Player", "PVR", "Settings", "System", 19 | "VideoLibrary", "xbmc") 20 | 21 | 22 | class KodiTransport(object): 23 | """Base class for Kodi transport.""" 24 | 25 | def execute(self, method, args): 26 | """Execute method with given args.""" 27 | pass # pragma: no cover 28 | 29 | 30 | class KodiJsonTransport(KodiTransport): 31 | """HTTP Json transport.""" 32 | 33 | def __init__(self, url, username='xbmc', password='xbmc'): 34 | self.url = url 35 | self.username = username 36 | self.password = password 37 | self._id = 0 38 | 39 | def execute(self, method, *args, **kwargs): 40 | headers = { 41 | 'Content-Type': 'application/json', 42 | 'User-Agent': 'python-kodi' 43 | } 44 | # Params are given as a dictionnary 45 | if len(args) == 1: 46 | args = args[0] 47 | params = kwargs 48 | # Use kwargs for param=value style 49 | else: 50 | args = kwargs 51 | params = {} 52 | params['jsonrpc'] = '2.0' 53 | params['id'] = self._id 54 | self._id += 1 55 | params['method'] = method 56 | params['params'] = args 57 | 58 | values = json.dumps(params) 59 | 60 | resp = requests.post(self.url, 61 | values.encode('utf-8'), 62 | headers=headers, 63 | auth=(self.username, self.password)) 64 | resp.raise_for_status() 65 | return resp.json() 66 | 67 | 68 | class Kodi(object): 69 | """Kodi client.""" 70 | 71 | def __init__(self, url, username='xbmc', password='xbmc'): 72 | self.transport = KodiJsonTransport(url, username, password) 73 | # Dynamic namespace class instanciation 74 | # we obtain class by looking up in globals 75 | _globals = globals() 76 | for cl in __KODI_NAMESPACES__: 77 | setattr(self, cl, _globals[cl](self.transport)) 78 | 79 | def execute(self, *args, **kwargs): 80 | """Execute method with given args and kwargs.""" 81 | self.transport.execute(*args, **kwargs) 82 | 83 | 84 | class KodiNamespace(object): 85 | """Base class for Kodi namespace.""" 86 | 87 | def __init__(self, kodi): 88 | self.kodi = kodi 89 | 90 | def __getattr__(self, name): 91 | klass = self.__class__.__name__ 92 | method = name 93 | kodimethod = "%s.%s" % (klass, method) 94 | 95 | def hook(*args, **kwargs): 96 | """Hook for dynamic method definition.""" 97 | return self.kodi.execute(kodimethod, *args, **kwargs) 98 | 99 | return hook 100 | 101 | # inject new type in module locals 102 | _LOCALS_ = locals() 103 | for _classname in __KODI_NAMESPACES__: 104 | # define a new type extending KodiNamespace 105 | # equivalent to 106 | # 107 | # class Y(KodiNamespace): 108 | # pass 109 | _LOCALS_[_classname] = type(_classname, (KodiNamespace, ), {}) 110 | # inject class in __all__ for import * to work 111 | __all__.append(_classname) 112 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = -s --verbose --cov-report html --cov=kodijson --cov=tests 6 | python_files = tests/**.py 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import version 3 | 4 | PACKAGE = 'kodi-json' 5 | 6 | setup(name=PACKAGE, 7 | version=version.VERSION, 8 | license="WTFPL", 9 | description="Python module for controlling kodi over HTTP Json API", 10 | author="Jean-Christophe Saad-Dupuy", 11 | author_email="jc.saaddupuy@fsfe.org", 12 | url="https://github.com/jcsaaddupuy/python-kodijson", 13 | py_modules=["kodijson/kodijson"], 14 | packages=["kodijson"], 15 | install_requires=["requests"], 16 | setup_requires=['pytest-runner'], 17 | tests_require=['pytest', 'pytest-cov', 'responses'], 18 | classifiers=[ 19 | "Development Status :: 4 - Beta", 20 | "Topic :: Utilities", 21 | "Topic :: Multimedia", 22 | "Intended Audience :: Developers", 23 | "Programming Language :: Python :: 2.6", 24 | "Programming Language :: Python :: 2.7", 25 | "Programming Language :: Python :: 3.1", 26 | "Programming Language :: Python :: 3.3", 27 | "Programming Language :: Python :: 3.4", 28 | "Programming Language :: Python :: 3.5", 29 | ]) 30 | -------------------------------------------------------------------------------- /tests/test_kodijson.py: -------------------------------------------------------------------------------- 1 | """Test for kodijson module""" 2 | import json 3 | import responses 4 | 5 | # not used, ensure import is working 6 | from kodijson import PLAYER_VIDEO # NOQA 7 | 8 | from kodijson import Kodi 9 | from kodijson import Addons 10 | 11 | from kodijson import Application 12 | from kodijson import AudioLibrary 13 | 14 | from kodijson import Favourites 15 | from kodijson import Files 16 | from kodijson import GUI 17 | from kodijson import Input 18 | from kodijson import JSONRPC 19 | 20 | from kodijson import Playlist 21 | from kodijson import Player 22 | from kodijson import PVR 23 | 24 | from kodijson import Settings 25 | from kodijson import System 26 | from kodijson import VideoLibrary 27 | from kodijson import xbmc 28 | 29 | from kodijson import KodiJsonTransport 30 | 31 | 32 | class TestKodiJsonTransport(object): 33 | """ Tests for default transport """ 34 | 35 | def test_default(self): 36 | """Tests KodiJsonTransport default values""" 37 | url = "http://localhost/" 38 | transport = KodiJsonTransport(url=url) 39 | assert transport.url == url 40 | assert transport.username == "xbmc" 41 | assert transport.password == "xbmc" 42 | 43 | def test_parameters(self): 44 | """Tests KodiJsonTransport default values""" 45 | 46 | url = "http://localhost/" 47 | transport = KodiJsonTransport(url=url, username="kodi", password="pwd") 48 | assert transport.url == url 49 | assert transport.username == "kodi" 50 | assert transport.password == "pwd" 51 | 52 | @responses.activate 53 | def test_http_call_no_args(self): 54 | """ Test http call with no arguments""" 55 | # the response does not matters 56 | responses.add("POST", "http://localhost", "{}") 57 | url = "http://localhost/" 58 | transport = KodiJsonTransport(url=url, username="kodi", password="pwd") 59 | # test 60 | transport.execute("remote_method") 61 | 62 | expectd_body = { 63 | "method": "remote_method", 64 | "id": 0, 65 | "jsonrpc": "2.0", 66 | "params": {} # no param given 67 | } 68 | assert responses.calls[0].request.url == 'http://localhost/' 69 | assert responses.calls[0].request.method == 'POST' 70 | assert expectd_body == json.loads( 71 | responses.calls[0].request.body.decode("utf-8")) 72 | 73 | @responses.activate 74 | def test_http_call_kwargs(self): 75 | """ Test http call with kwargs arguments""" 76 | 77 | # the response does not matters 78 | responses.add("POST", "http://localhost", "{}") 79 | 80 | url = "http://localhost/" 81 | transport = KodiJsonTransport(url=url, username="kodi", password="pwd") 82 | # test 83 | transport.execute("remote_method", x="y", z="a") 84 | 85 | expectd_body = { 86 | "method": "remote_method", 87 | "id": 0, 88 | "jsonrpc": "2.0", 89 | "params": {'x': 'y', 90 | 'z': 'a'} 91 | } 92 | assert responses.calls[0].request.url == 'http://localhost/' 93 | assert responses.calls[0].request.method == 'POST' 94 | assert expectd_body == json.loads( 95 | responses.calls[0].request.body.decode("utf-8")) 96 | 97 | @responses.activate 98 | def test_http_call_args(self): 99 | """ Test http call with kwargs arguments""" 100 | 101 | # the response does not matters 102 | responses.add("POST", "http://localhost", "{}") 103 | url = "http://localhost/" 104 | transport = KodiJsonTransport(url=url, username="kodi", password="pwd") 105 | # test 106 | transport.execute("remote_method", {"x": "y"}) 107 | 108 | expectd_body = { 109 | 'id': 0, 110 | 'jsonrpc': '2.0', 111 | 'method': 'remote_method', 112 | "params": {'x': 'y'} 113 | } 114 | assert responses.calls[0].request.url == 'http://localhost/' 115 | assert responses.calls[0].request.method == 'POST' 116 | assert expectd_body == json.loads( 117 | responses.calls[0].request.body.decode("utf-8")) 118 | 119 | 120 | class TestKodi(object): 121 | """ Tests for default Kodi class """ 122 | def test_default(self): 123 | """Tests KodiJsonTransport default values""" 124 | 125 | url = "http://localhost/" 126 | x = Kodi(url=url) 127 | assert isinstance(x.Addons, Addons) 128 | 129 | assert isinstance(x.Application, Application) 130 | assert isinstance(x.AudioLibrary, AudioLibrary) 131 | 132 | assert isinstance(x.Favourites, Favourites) 133 | assert isinstance(x.Files, Files) 134 | assert isinstance(x.GUI, GUI) 135 | assert isinstance(x.Input, Input) 136 | assert isinstance(x.JSONRPC, JSONRPC) 137 | 138 | assert isinstance(x.Playlist, Playlist) 139 | assert isinstance(x.Player, Player) 140 | assert isinstance(x.PVR, PVR) 141 | 142 | assert isinstance(x.Settings, Settings) 143 | assert isinstance(x.System, System) 144 | assert isinstance(x.VideoLibrary, VideoLibrary) 145 | assert isinstance(x.xbmc, xbmc) 146 | 147 | @responses.activate 148 | def test_ping(self): 149 | """ Test a call with JSON.Ping """ 150 | # the response does not matters 151 | responses.add("POST", "http://localhost", "{}") 152 | url = "http://localhost/" 153 | 154 | x = Kodi(url=url) 155 | x.JSONRPC.Ping() 156 | 157 | expectd_body = { 158 | 'id': 0, 159 | 'jsonrpc': '2.0', 160 | 'method': 'JSONRPC.Ping', 161 | 'params': {} 162 | } 163 | assert responses.calls[0].request.url == 'http://localhost/' 164 | assert responses.calls[0].request.method == 'POST' 165 | assert expectd_body == json.loads( 166 | responses.calls[0].request.body.decode("utf-8")) 167 | 168 | @responses.activate 169 | def test_execute_ping(self): 170 | """ Test a call with JSON.Ping """ 171 | # the response does not matters 172 | responses.add("POST", "http://localhost", "{}") 173 | url = "http://localhost/" 174 | 175 | x = Kodi(url=url) 176 | x.execute("JSONRPC.Ping", x=1) 177 | 178 | expectd_body = { 179 | 'id': 0, 180 | 'jsonrpc': '2.0', 181 | 'method': 'JSONRPC.Ping', 182 | 'params': {"x": 1} 183 | } 184 | assert responses.calls[0].request.url == 'http://localhost/' 185 | assert responses.calls[0].request.method == 'POST' 186 | assert expectd_body == json.loads( 187 | responses.calls[0].request.body.decode("utf-8")) 188 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py31,py33,py34,py35 3 | 4 | [testenv] 5 | # for coverage 6 | usedevelop = True 7 | # deps : same as test_require in setup.py 8 | deps= 9 | pytest 10 | pytest-cov 11 | responses 12 | commands= 13 | py.test \ 14 | --basetemp={envtmpdir} \ 15 | {posargs} 16 | coverage report -m 17 | 18 | 19 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.0.0" 2 | --------------------------------------------------------------------------------