├── kodi_cli
├── json-defs
│ ├── version.txt
│ └── types.json
├── utils
│ ├── kodi_common.py
│ ├── kodi_output_factory.py
│ └── cfg.py
├── kodi_help_tester.py
├── kodi_libraray_inventory.py
├── driver.py
└── kodi_interface.py
├── pyproject.toml
├── LICENSE
├── CHANGELOG.md
├── .gitignore
└── README.md
/kodi_cli/json-defs/version.txt:
--------------------------------------------------------------------------------
1 | JSONRPC_VERSION 12.3.0
2 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "kodi-cli"
3 | version = "0.2.6"
4 | description = "Control your Kodi instance(s) via the commandline"
5 | authors = [
6 | {name = "Al D'Amico",email = "JavaWiz1@hotmail.com"}
7 | ]
8 | license = {text = "MIT"}
9 | readme = "README.md"
10 | requires-python = ">=3.8"
11 | dependencies = [
12 | "requests (>=2.32.3,<3.0.0)",
13 | "loguru"
14 | ]
15 |
16 | [tool.poetry.scripts]
17 | kodi-cli = "kodi_cli.driver:main"
18 |
19 | [build-system]
20 | requires = ["poetry-core>=2.0.0,<3.0.0"]
21 | build-backend = "poetry.core.masonry.api"
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 JavaWiz1
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.2.1 02/15/2024
2 | - Switched to loguru for logging
3 | - changed how config works, default location is ~/.kodi_cli/kodi_cli.cfg
4 | - location of the log file will be in same directory as config (~/.kodi_cli)
5 |
6 | # 0.1.9 10/24/2023
7 | - Fix bug for locating json definition files
8 | - Fix bug for handling boolean input parameters
9 |
10 | # 0.1.8 10/23/2023
11 | - README.md documentation updates
12 | - logging updates -v INFO -vv TRACE (new) -vvv DEBUG
13 |
14 | # 0.1.7 10/23/2022
15 | - Use official kodi repo json files for methods and type definitions (replacing kodi_namespaces.json)
16 | - Updated parsing for help messages
17 | - bug fix for building paraneter strings when calling the RPC endpoints
18 |
19 | # 0.1.6 8/13/2022
20 | - Fixed AddOn.GetAddons
21 | - Set following as defaults in GetAddons: property=[name] limit={start=0,max=99}
22 | - Handle objects in parameters (treat as dictionary)
23 |
24 | # 0.1.3 6/29/2022
25 | - Fix distribution
26 | - Add kodi_help_tester.py - used to check the help output
27 |
28 | # 0.1.2 6/19/2022
29 | - repackage code, split KodiObj into own module
30 | - clean up distribution
31 | - README updates
32 |
--------------------------------------------------------------------------------
/kodi_cli/utils/kodi_common.py:
--------------------------------------------------------------------------------
1 | # === Validation routines =================================================
2 | def is_integer(token: str) -> bool:
3 | """Return true if string is an integer"""
4 | is_int = True
5 | try:
6 | int(token)
7 | except (ValueError, TypeError):
8 | is_int = False
9 | return is_int
10 |
11 | def is_boolean(token: str) -> bool:
12 | """Return true if string is a boolean"""
13 | is_bool_str = False
14 | if token in ["True", "true", "False", "false"]:
15 | is_bool_str = True
16 | return is_bool_str
17 |
18 | def is_list(token: str) -> bool:
19 | """Return true if string represents a list"""
20 | if token.startswith("[") and token.endswith("]"):
21 | return True
22 | return False
23 |
24 | def is_dict(token: str) -> bool:
25 | """Return true if string represents a dictionary"""
26 | if token.startswith("{") and token.endswith("}"):
27 | return True
28 | return False
29 |
30 | def make_list_from_string(token: str) -> list:
31 | """Translate list formatted string to a list obj"""
32 | text = token[1:-1]
33 | return text.split(",")
34 |
35 | def make_dict_from_string(token: str) -> dict:
36 | """Translate dict formatted string to a dict obj"""
37 | text = token[1:-1]
38 | entry_list = text.split(",")
39 | result_dict = {}
40 | for entry in entry_list:
41 | key_val = entry.split(":", 1)
42 | key = key_val[0].strip()
43 | value = key_val[1].strip()
44 | if is_integer(value):
45 | value=int(value)
46 | elif is_boolean(value):
47 | value = value in ['True', 'true']
48 | result_dict[key] = value
49 |
50 | return result_dict
51 |
--------------------------------------------------------------------------------
/kodi_cli/kodi_help_tester.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from kodi_cli.kodi_interface import KodiObj
4 |
5 |
6 | def get_input(prompt: str = "> ", choices: list = [], required = False) -> str:
7 | ret_val = input(prompt)
8 | if choices:
9 | while ret_val not in choices:
10 | print(f'Invalid selection. Valid entries: {"/".join(choices)}')
11 | ret_val = input(prompt)
12 | elif required:
13 | while not ret_val:
14 | print('You MUST enter a value.')
15 | ret_val = input(prompt)
16 |
17 | return ret_val
18 |
19 | # def setup_logging(log_level = logging.ERROR):
20 | # lg_format='[%(levelname)-5s] %(message)s'
21 | # logging.basicConfig(format=lg_format, level=log_level,)
22 |
23 | # def set_loglevel(log_level:str):
24 | # if log_level == "E":
25 | # lg_lvl = logging.ERROR
26 | # elif log_level == "I":
27 | # lg_lvl = logging.INFO
28 | # else:
29 | # lg_lvl = logging.DEBUG
30 | # logging.getLogger().setLevel(lg_lvl)
31 |
32 | def dump_methods(kodi: KodiObj):
33 | namespaces = kodi.get_namespace_list()
34 | for ns in namespaces:
35 | resp = get_input(f"Display: {ns} (y|n|q)> ",['y','n','Y','N','Q','q']).lower()
36 | if resp == "q":
37 | break
38 | elif resp == 'y':
39 | ns_methods = kodi.get_namespace_method_list(ns)
40 | for method in ns_methods:
41 | resp = get_input(f'{ns}.{method} (E,I,D,n,q)> ',['E','I','D','y','n','q',''])
42 | if resp in ['E','I','D']:
43 | # set_loglevel(resp)
44 | pass
45 | elif resp == 'q':
46 | sys.exit()
47 | elif resp == 'n':
48 | break
49 | cmd = f'{ns}.{method}'
50 | print(cmd)
51 | kodi.help(cmd)
52 | print()
53 | print('\n=========================================================================')
54 |
55 | def main():
56 | # setup_logging()
57 | # log_level = "E"
58 | # set_loglevel(log_level)
59 |
60 | kodi = KodiObj()
61 | # kodi.help("")
62 | # pause()
63 | # kodi.help("Application")
64 | # pause()
65 | # kodi.help('AudioLibrary.GetArtists')
66 | dump_methods(kodi)
67 |
68 | if __name__ == "__main__":
69 | main()
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 | *.cfg
113 |
114 | # Spyder project settings
115 | .spyderproject
116 | .spyproject
117 |
118 | # Rope project settings
119 | .ropeproject
120 |
121 | # mkdocs documentation
122 | /site
123 |
124 | # mypy
125 | .mypy_cache/
126 | .dmypy.json
127 | dmypy.json
128 |
129 | # Pyre type checker
130 | .pyre/
131 |
132 | # Stuff
133 | *.csv
134 | *.lock
--------------------------------------------------------------------------------
/kodi_cli/utils/kodi_output_factory.py:
--------------------------------------------------------------------------------
1 | # Json Raw
2 | # Json Formatted
3 | # CSV Formatted
4 | # CSV only available for specific commands
5 |
6 | # https://www.geeksforgeeks.org/convert-json-to-csv-in-python/
7 |
8 |
9 | # Python program to convert
10 | # JSON file to CSV
11 |
12 | import json
13 | import csv
14 | import sys
15 |
16 | # ===========================================================================
17 | class ObjectFactory:
18 | def __init__(self):
19 | self._builders = {}
20 |
21 | def register_builder(self, key, builder):
22 | self._builders[key] = builder
23 |
24 | def create(self, key, **kwargs):
25 | builder = self._builders.get(key)
26 | if not builder:
27 | raise ValueError(key)
28 | return builder(**kwargs)
29 |
30 | # ===========================================================================
31 | class JSON_OutputServiceBuilder:
32 | def __init__(self):
33 | self._instance = None
34 |
35 | def __call__(self, response_text: str, pretty: bool = False, **_ignored):
36 | if not self._instance:
37 | self._instance = JSON_OutputService(response_text, pretty)
38 | return self._instance
39 |
40 |
41 | class JSON_OutputService:
42 | def __init__(self, output_text: str, pretty: bool = False):
43 | self._output_text = output_text
44 | self._pretty = pretty
45 |
46 | def output_result(self) -> str:
47 | if self._pretty:
48 | result = json.dumps(json.loads(self._output_text), indent=2)
49 | else:
50 | result = json.dumps(json.loads(self._output_text))
51 | print(result)
52 |
53 |
54 | # ===========================================================================
55 | class CSV_OutputServiceBuilder:
56 | def __init__(self):
57 | self._instance = None
58 |
59 | def __call__(self, response_text: str, list_key: str, **_ignored):
60 | if not self._instance:
61 | self._instance = CSV_OutputService(response_text, list_key)
62 | return self._instance
63 |
64 | class CSV_OutputService:
65 | def __init__(self, output_text: str, list_key: str):
66 | self._output_text = output_text
67 | self._list_key = list_key
68 |
69 | def output_result(self):
70 | data = json.loads(self._output_text)
71 | json_list = data['result'][self._list_key]
72 | sys.stdout.reconfigure(encoding='utf-8')
73 | csv_writer = csv.writer(sys.stdout, lineterminator='\n')
74 | # Counter variable used for writing headers to the CSV file
75 | count = 0
76 | for item in json_list:
77 | if count == 0:
78 | # Writing headers of CSV file
79 | header = item.keys()
80 | csv_writer.writerow(header)
81 | count += 1
82 |
83 | # Writing data of CSV file
84 | csv_writer.writerow(item.values())
85 |
86 |
--------------------------------------------------------------------------------
/kodi_cli/kodi_libraray_inventory.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import pathlib
4 | import textwrap
5 | from typing import List
6 |
7 | from loguru import logger as LOGGER
8 |
9 | import kodi_cli.utils.cfg as cfg
10 | import kodi_cli.utils.kodi_common as util
11 | from kodi_cli.kodi_interface import KodiObj
12 |
13 | EPISODE_FILE='./episodes.csv'
14 | MOVIE_FILE='./movies.csv'
15 |
16 | class KodiCommand:
17 | TVSHOW_OPTIONS = ['watchedepisodes','season','episode']
18 | TVSHOW_EPISODE_OPTIONS = ['dateadded','episode','firstaired','playcount','plot','runtime','season','showtitle','title','rating','votes']
19 | MOVIE_OPTIONS = ['dateadded','genre','lastplayed','mpaa','playcount','plot','plotoutline','premiered','rating','runtime','title','userrating','votes','year']
20 |
21 | def __init__(self, namespace: str, method: str, parms: dict = None) -> None:
22 | self.namespace = namespace
23 | self.method = method
24 | self.parms = parms
25 |
26 | def _call_kodi(kodi: KodiObj, cmd: KodiCommand) -> bool:
27 | success = False
28 | if kodi.check_command(cmd.namespace, cmd.method, cmd.parms):
29 | kodi.send_request(cmd.namespace, cmd.method, cmd.parms)
30 | success = kodi.request_success
31 | return success
32 |
33 | def get_tv_shows(kodi: KodiObj) -> List[dict]:
34 | LOGGER.info(f'Retrieve TVShows from {kodi._host}')
35 | tv_shows = []
36 | cmd = KodiCommand('VideoLibrary', 'GetTVShows', {'properties': KodiCommand.TVSHOW_OPTIONS})
37 | if not _call_kodi(kodi, cmd):
38 | LOGGER.error(f'ERROR: {kodi.response_status_code} - {kodi.response_text}')
39 | else:
40 | r_json = json.loads(kodi.response_text)
41 | for show in r_json['result']['tvshows']:
42 | # if show['episode'] > 0 and show['episode'] > show['watchedepisodes']:
43 | if show['episode'] > 0:
44 | tv_shows.append(show)
45 | # print(show)
46 | LOGGER.info(f' Total shows: {r_json["result"]["limits"]["total"]}')
47 | LOGGER.info(f' Returned shows: {len(tv_shows)}')
48 |
49 | return tv_shows
50 |
51 | def get_tv_show_episodes(kodi: KodiObj, show_name: str, tvshow_id: int, season: int) -> List[dict]:
52 | episodes = list()
53 | LOGGER.info(f'- Retrieving {show_name} season {season}...')
54 | cmd = KodiCommand('VideoLibrary', 'GetEpisodes', {'tvshowid': tvshow_id, 'season': season, 'properties': KodiCommand.TVSHOW_EPISODE_OPTIONS})
55 | if not _call_kodi(kodi, cmd):
56 | LOGGER.error(f' ERROR: {kodi.response_status_code} - {kodi.response_text}')
57 | else:
58 | r_json = json.loads(kodi.response_text)
59 | for episode in r_json['result']['episodes']:
60 | entry:dict = {"show_name": show_name}
61 | entry.update(episode)
62 | episodes.append(entry)
63 | # print(episode)
64 | LOGGER.trace(f' {len(episodes)} loaded.')
65 | return episodes
66 |
67 | def get_movies(kodi: KodiObj) -> list:
68 | LOGGER.info('Retrieve Movies...')
69 | movies = []
70 | cmd = KodiCommand('VideoLibrary', 'GetMovies', {'properties': KodiCommand.MOVIE_OPTIONS})
71 | if not _call_kodi(kodi, cmd):
72 | LOGGER.error(f'ERROR: {kodi.response_status_code} - {kodi.response_text}')
73 | else:
74 | r_json = json.loads(kodi.response_text)
75 | for movie in r_json['result']['movies']:
76 | movies.append(movie)
77 | LOGGER.info(f' Returned movies: {len(movies)}')
78 |
79 | return movies
80 |
81 | def create_csv(file_nm: str, entries: list) -> bool:
82 | header_list = [x for x in entries[0].keys()]
83 | header_line = ','.join(header_list)
84 | with pathlib.Path(file_nm).open('w', encoding='UTF-8') as csv_out:
85 | csv_out.write(f'{header_line}\n')
86 | for entry in entries:
87 | detail_list = list()
88 | for v in entry.values():
89 | if util.is_integer(v):
90 | detail_list.append(str(v))
91 | elif isinstance(v, dict):
92 | v = ','.join(v.values())
93 | detail_list.append(f'"{v}"')
94 | elif isinstance(v, list):
95 | v = ','.join(v)
96 | detail_list.append(f'"{v}"')
97 | else:
98 | v = v.replace('"', "'")
99 | detail_list.append(f'"{v}"')
100 | detail_line = ",".join(detail_list)
101 | csv_out.write(f'{detail_line}\n')
102 |
103 | def initialize_loggers():
104 | log_filename = pathlib.Path(cfg.logging_filename) # pathlib.Path('./logs/da-photo.log')
105 |
106 | if cfg.logging_level.upper() == 'INFO':
107 | console_format = cfg.DEFAULT_CONSOLE_LOGFMT
108 | else:
109 | console_format = cfg.DEBUG_CONSOLE_LOGFMT
110 |
111 | c_handle = cfg.configure_logger(log_format=console_format, log_level=cfg.logging_level)
112 | f_handle = -1
113 | if cfg.logging_enabled:
114 | f_handle = cfg.configure_logger(log_filename, log_format=cfg.DEFAULT_FILE_LOGFMT, log_level=cfg.logging_level,
115 | rotation=cfg.logging_rotation, retention=cfg.logging_retention)
116 |
117 | # Hack for future ability to dynamically change logging levels
118 | LOGGER.c_handle = c_handle
119 | LOGGER.f_handle = f_handle
120 |
121 | if len(cfg.logger_blacklist.strip()) > 0:
122 | for logger_name in cfg.logger_blacklist.split(','):
123 | LOGGER.disable(logger_name)
124 |
125 | def apply_overrides(args: argparse.Namespace):
126 | for key, val in args._get_kwargs():
127 | cfg_val = getattr(cfg, key, None)
128 | if cfg_val is not None and cfg_val != val:
129 | LOGGER.trace(f'CmdLine Override: {key}: {val}')
130 | setattr(cfg, key, val)
131 |
132 | if args.verbose:
133 | if args.verbose == 1:
134 | cfg.logging_level = 'INFO'
135 | elif args.verbose == 2:
136 | cfg.logging_level = 'DEBUG'
137 | elif args.verbose > 2:
138 | cfg.logging_level = 'TRACE'
139 |
140 |
141 | def main():
142 | parser = argparse.ArgumentParser(description=f'Kodi CLI controller v{cfg.__version__}')
143 | parser.formatter_class = argparse.RawDescriptionHelpFormatter
144 | parser.description = textwrap.dedent('''\
145 | command is formatted as follows:
146 | Namespace.Method [parameter [parameter] [...]]
147 |
148 | example - Retrieve list of 5 addons:
149 | kodi-cli -H myHost Addons.GetAddons properties=[name,version,summary] limits={start:0,end:5}
150 | ''')
151 | parser.add_argument("-H","--host", type=str, default=cfg.host, help="Kodi hostname")
152 | parser.add_argument("-P","--port", type=int, default=cfg.port,help="Kodi RPC listen port")
153 | parser.add_argument("-u","--kodi-user", type=str, default=cfg.kodi_user,help="Kodi authenticaetion username")
154 | parser.add_argument("-p","--kodi_pw", type=str, default=cfg.kodi_pw,help="Kodi autentication password")
155 | parser.add_argument('-t',"--tv-shows", action='store_true')
156 | parser.add_argument('-m',"--movies", action='store_true')
157 | parser.add_argument("-v","--verbose", action='count', help="Verbose output, -v = INFO, -vv = DEBUG, -vvv TRACE")
158 |
159 | args = parser.parse_args()
160 | apply_overrides(args)
161 | initialize_loggers() # Incase loggers got overridden
162 |
163 | kodi = KodiObj(cfg.host)
164 |
165 | # TODO: set options for watched, unwatched, all (default)
166 | if args.tv_shows:
167 | LOGGER.info('='*40)
168 | episodes = list()
169 | tv_shows = get_tv_shows(kodi)
170 | for tvshow in tv_shows:
171 | # show_name = tvshow['label']
172 | for season in range(1,tvshow['season']):
173 | season_episodes = get_tv_show_episodes(kodi, tvshow['label'], tvshow['tvshowid'], season )
174 | if len(season_episodes) > 0:
175 | episodes.extend(season_episodes)
176 |
177 | create_csv(EPISODE_FILE, episodes)
178 | LOGGER.success(f'{len(episodes)} episodes loaded into {EPISODE_FILE}')
179 |
180 | if args.movies:
181 | LOGGER.info('='*40)
182 | movies = get_movies(kodi)
183 | create_csv(MOVIE_FILE, movies)
184 | LOGGER.success(f'{len(movies)} movies loaded into {MOVIE_FILE}')
185 |
186 | if __name__ == "__main__":
187 | main()
--------------------------------------------------------------------------------
/kodi_cli/utils/cfg.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import pathlib
3 | import platform
4 | import sys
5 | from datetime import datetime as dt
6 | from importlib.metadata import version
7 | from typing import List, Tuple
8 |
9 | from loguru import logger as LOGGER
10 |
11 | PACKAGE_NAME = "kodi_cli"
12 |
13 |
14 | # -- Version routines ----------------------------------------------------------------------------------
15 | def get_version() -> str:
16 | try:
17 | ver = version(PACKAGE_NAME)
18 | except Exception as ex:
19 | ver = None
20 | LOGGER.debug(f'Unable to retrieve version from package name [{PACKAGE_NAME}]')
21 | LOGGER.debug(f'Error: {ex}')
22 |
23 | if ver is None:
24 | toml = pathlib.Path(resolve_config_location('pyproject.toml'))
25 | if toml.exists():
26 | toml_list = toml.read_text().splitlines()
27 | token = [ x for x in toml_list if x.startswith('version') ]
28 | if len(token) == 1:
29 | ver = token[0].replace('version','' ).replace('=','').replace('"','').strip()
30 | if ver is None:
31 | ver = _get_version_from_mod_time()
32 |
33 | return ver
34 |
35 | def _get_version_from_mod_time() -> str:
36 | # version based on the mod timestamp of the most current updated python code file
37 | file_list = list(pathlib.Path(__file__).parent.glob("**/*.py"))
38 | ver_date = dt(2000,1,1,0,0,0,0)
39 | for file_nm in file_list:
40 | ver_date = max(ver_date, dt.fromtimestamp(file_nm.stat().st_mtime))
41 | ver = f'{ver_date.year}.{ver_date.month}.{ver_date.day}'
42 | return ver
43 |
44 | def resolve_config_location(file_name: str) -> str:
45 | LOGGER.trace(f'Attempting to resolve location for: {file_name}')
46 | found_location = file_name
47 | first_prefix = pathlib.Path(file_name).parent
48 | config_prefixes = [first_prefix, './config', '../config', '.', '..', f'~/.{PACKAGE_NAME}']
49 | for prefix in config_prefixes:
50 | file_loc = pathlib.Path(f'{prefix}/{file_name}').expanduser().absolute()
51 | LOGGER.trace(f'- {file_loc}')
52 | if file_loc.exists():
53 | found_location = str(file_loc.absolute())
54 | LOGGER.trace(f' - FOUND: {found_location}')
55 | break
56 | # Default to found location (or last entry i.e. ~/.kodi_cli/kodi_.cfg)
57 | found_location = file_loc.absolute()
58 | return found_location
59 |
60 | def get_host_info() -> dict:
61 | """
62 | Return dictionary of host info:
63 | - name
64 | - Processor
65 | - OS Release, Type and Version
66 | - Python version
67 | """
68 | host_info = {}
69 | host_info['Hostname'] = platform.node()
70 | host_info['Processor'] = platform.processor()
71 | host_info['Release'] = platform.release()
72 | host_info['OS Type'] = platform.system()
73 | host_info['OS Version'] = platform.version()
74 | host_info['Python'] = platform.python_version()
75 | return host_info
76 |
77 | # -- Config file routines ------------------------------------------------------------------------------
78 | def _get_section_desc(key: str) -> Tuple[str, str]:
79 | entry = _KEYWORD_SECTIONS.get(key, None)
80 | if entry is None:
81 | raise KeyError(f"'{key}' does NOT exist in _KEYWORD_SECTIONS")
82 |
83 | section = entry['section']
84 | desc = entry['desc']
85 | return section, desc
86 |
87 | def _config_notes_block() -> List[str]:
88 | notes = []
89 | notes.append(f'# {"="*80}\n')
90 | notes.append(f'# {PACKAGE_NAME} configuration file (auto-generated)\n')
91 | notes.append(f'# {"-"*80}\n')
92 | notes.append('# NOTES:\n')
93 | for keyword in _KEYWORD_SECTIONS.keys():
94 | section, desc = _get_section_desc(keyword)
95 | if len(desc) > 0:
96 | notes.append(f'# - {keyword:25} : {desc}\n')
97 | notes.append(f'# {"-"*80}\n')
98 |
99 | return notes
100 |
101 | def create_template_config(overwrite: bool = False):
102 | this_module = sys.modules[__name__]
103 | filename = pathlib.Path(resolve_config_location(f'{PACKAGE_NAME}.cfg')).absolute()
104 | # filename = pathlib.Path(f'./config/{PACKAGE_NAME}.cfg').absolute()
105 | LOGGER.trace(f'Config location identified as: {filename}')
106 | if not filename.parent.exists():
107 | LOGGER.info(f'Creating directory: {filename.parent}')
108 | filename.parent.mkdir()
109 | if filename.exists():
110 | if not overwrite:
111 | raise FileExistsError(f'{filename}, to overwrite, use -CO option')
112 |
113 | new_config = configparser.ConfigParser()
114 | for keyword in _KEYWORD_SECTIONS.keys():
115 | section, _ = _get_section_desc(keyword)
116 | if not new_config.has_section(section):
117 | new_config[section] = {}
118 | val = getattr(this_module, keyword, 'TBD')
119 | new_config[section][keyword] = str(val)
120 |
121 | notes = _config_notes_block()
122 | with open(filename, 'w',) as h_file:
123 | for line in notes:
124 | h_file.write(line)
125 | new_config.write(h_file)
126 |
127 | LOGGER.info('')
128 | LOGGER.info(f'Config file [{filename}] created/updated.')
129 | LOGGER.info(' - Values are current settings (or default setting if not defined)')
130 | if '_NEW' in str(filename):
131 | LOGGER.warning(' - You must rename the file (to kodi_cli.cfg) for changes to take effect')
132 | LOGGER.info('')
133 |
134 |
135 | # == Logger Setup ========================================================================
136 | def configure_logger(log_target = sys.stderr, log_level: str = "INFO", log_format: str = None, log_handle: int = 0, **kwargs) -> int:
137 | """
138 | Configure logger via loguru.
139 | - should be done once for each logger (console, file,..)
140 | - if reconfiguring a logger, pass the log_handle
141 |
142 | Parameters:
143 | log_target: defaults to stderr, but can supply filename as well
144 | log_level : TRACE|DEBUG|INFO(dflt)|ERROR|CRITICAL
145 | log_format: format for output log line
146 | log_handle: handle of log being re-initialized.
147 | other : keyword args related to loguru logger.add() function
148 | Returns:
149 | logger_handle_id: integer representing logger handle
150 | """
151 | try:
152 | LOGGER.remove(log_handle)
153 | except Exception as ex:
154 | LOGGER.trace(f'Unable to remove log handle: {log_handle} [{ex}]')
155 |
156 | if not log_format:
157 | if isinstance(log_target, str):
158 | log_format = DEFAULT_FILE_LOGFMT
159 | else:
160 | log_format = DEFAULT_CONSOLE_LOGFMT
161 |
162 | hndl = LOGGER.add(sink=log_target, level=log_level, format=log_format, **kwargs)
163 |
164 | return hndl
165 |
166 |
167 | # == Config Settings =====================================================================
168 | FILE_CONFIG = resolve_config_location(f'{PACKAGE_NAME}.cfg')
169 |
170 | CONFIG_EXISTS = pathlib.Path(FILE_CONFIG).exists()
171 | _CONFIG = configparser.ConfigParser()
172 | _CONFIG.read(FILE_CONFIG)
173 |
174 | # ===================================================================================================================
175 | _KEYWORD_SECTIONS = {
176 | "logging_enabled": {"section": "LOGGING", "desc": "Turn on/off file logging"},
177 | "logging_filename": {"section": "LOGGING", "desc": ""},
178 | "logging_rotation": {"section": "LOGGING", "desc": "Limit on log file size (i.e. '15 mb')"},
179 | "logging_retention": {"section": "LOGGING", "desc": "How many copies to retain (i.e. 3)"},
180 | "logging_level": {"section": "LOGGING", "desc": "Log level (ERROR, WARNING, INFO, DEBUG, TRACE)"},
181 | "logger_blacklist": {"section": "LOGGING", "desc": "Comma seperated list of Loggers to disable"},
182 |
183 | "host": {"section": "SERVER", "desc": "Target Kodi Host"},
184 | "port": {"section": "SERVER", "desc": "Kodi service listening port"},
185 | "kodi_user": {"section": "LOGIN", "desc": "Kodi username"},
186 | "kodi_pw": {"section": "LOGIN", "desc": "Kodi password"},
187 | "format_output": {"section": "OUTPUT", "desc": "Output in JSON readable format"},
188 | "csv_output": {"section": "OUTPUT", "desc": "Output in CSV format"},
189 | }
190 |
191 | # ===================================================================================================================
192 | __version__ = get_version()
193 |
194 | DEFAULT_FILE_LOGFMT = "{time:MM/DD/YY HH:mm:ss} |{level: <8}|{name:12}|{line:3}| {message}"
195 | DEFAULT_CONSOLE_LOGFMT = "{message}"
196 | DEBUG_CONSOLE_LOGFMT = "[{level: <8}] {name:15}[{line:3}] {message}"
197 |
198 | logging_enabled: str = _CONFIG.getboolean(_get_section_desc('logging_enabled')[0], 'logging_enabled', fallback=False)
199 | _default_log_name = resolve_config_location(f'{PACKAGE_NAME}.log')
200 | logging_filename: str = _CONFIG.get(_get_section_desc('logging_filename')[0], 'logging_filename', fallback=_default_log_name)
201 | logging_rotation: str = _CONFIG.get(_get_section_desc('logging_rotation')[0], 'logging_rotation', fallback='1 MB')
202 | logging_retention: int = _CONFIG.getint(_get_section_desc('logging_retention')[0], 'logging_retention', fallback=3)
203 | logging_level: str = _CONFIG.get(_get_section_desc('logging_level')[0], 'logging_level', fallback='INFO')
204 | logger_blacklist: str = _CONFIG.get(_get_section_desc('logger_blacklist')[0], 'logger_blacklist', fallback='')
205 |
206 | host: str = _CONFIG.get(_get_section_desc('host')[0], 'host', fallback='localhost')
207 | port: int = _CONFIG.getint(_get_section_desc('port')[0], 'port', fallback=8080)
208 |
209 | kodi_user: str = _CONFIG.get(_get_section_desc('kodi_user')[0], 'kodi_user', fallback='kodi')
210 | kodi_pw: str =_CONFIG.get(_get_section_desc('kodi_pw')[0], 'kodi_pw', fallback='kodi')
211 |
212 | format_output: bool =_CONFIG.getboolean(_get_section_desc('format_output')[0], 'format_output', fallback=False)
213 | csv_output: bool =_CONFIG.getboolean(_get_section_desc('csv_output')[0], 'csv_output', fallback=False)
214 | _json_rpc_loc: str = _CONFIG.get(section='SERVER', option='_json_rpc_loc', fallback='./json-defs')
215 |
--------------------------------------------------------------------------------
/kodi_cli/driver.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import pathlib
4 | import sys
5 | import textwrap
6 | from typing import Tuple
7 |
8 | from loguru import logger as LOGGER
9 |
10 | import kodi_cli.utils.cfg as cfg
11 | import kodi_cli.utils.kodi_common as util
12 | import kodi_cli.utils.kodi_output_factory as output_factory
13 | from kodi_cli.kodi_interface import KodiObj
14 |
15 |
16 | __version__ = cfg.get_version()
17 |
18 | def build_kwargs_from_args(args: list) -> dict:
19 | kwargs = {}
20 | LOGGER.debug(f"build_kwargs_from_args('{args}')")
21 | for parm_block in args:
22 | # param_key=param_value OR param_key=[a,list,of,stuff]
23 | tokens = parm_block.split("=", 1)
24 | LOGGER.debug(f' parm_block: {parm_block}')
25 | LOGGER.debug(f' tokens: {tokens}')
26 | if len(tokens) == 1:
27 | kwargs[tokens[0]] = ""
28 | else:
29 | if util.is_list(tokens[1]):
30 | kwargs[tokens[0]] = util.make_list_from_string(tokens[1])
31 | elif util.is_dict((tokens[1])):
32 | kwargs[tokens[0]] = util.make_dict_from_string(tokens[1])
33 | elif util.is_integer(tokens[1]):
34 | kwargs[tokens[0]] = int(tokens[1])
35 | elif util.is_boolean(tokens[1]):
36 | kwargs[tokens[0]] = tokens[1] in ['True', 'true']
37 | else:
38 | kwargs[tokens[0]] = tokens[1]
39 |
40 | LOGGER.debug(f'build_kwargs_from_args() returns: {kwargs}')
41 | return kwargs
42 |
43 | def parse_input(args: list) -> Tuple[str, str, str, dict]:
44 | """Parse program CLI command parameters, return Namespace, Method, Parms as a tuple"""
45 | # cmd = None
46 | # sub_cmd = None
47 | parm_kwargs = {}
48 | namespace = None
49 | method = None
50 | reference = None
51 | if len(args) > 0:
52 | namespace = args[0]
53 | if args[0].count('.') > 1:
54 | reference = args[0]
55 | LOGGER.info(f'Looks like a reference: {reference}')
56 | # method = 'help'
57 | elif "." in namespace:
58 | # namespace.method
59 | tokens = namespace.split(".")
60 | namespace = tokens[0]
61 | method = tokens[1]
62 | if len(args) > 1:
63 | parm_kwargs = build_kwargs_from_args(args[1:])
64 |
65 | LOGGER.debug('Parsed Command Input:')
66 | LOGGER.debug(f' Namespace : {namespace}')
67 | LOGGER.debug(f' Method : {method}')
68 | LOGGER.debug(f' Reference : {reference}')
69 | LOGGER.debug(f' kwargs : {parm_kwargs}')
70 |
71 | return reference, namespace, method, parm_kwargs
72 |
73 | # === Intro help page ================================================================================
74 | def display_script_help(usage: str):
75 | """Display script help dialog to explain how program works"""
76 | print()
77 | print(f'kodi_cli: v{__version__}\n')
78 | print(usage)
79 | print('Commands are based on Kodi namespaces and methods for each namespace. When executing a command')
80 | print('you supply the namespace, the method and any parameters (if required).\n')
81 | print('For example, to display the mute and volume level settings on host kodi001, type:\n')
82 | print(' kodi_cli -H kodi001 Application.GetProperties properties=[muted,volume]\n')
83 | print('TIPS - When calling the script')
84 | print(' - add -h to display script syntax and list of option parameters')
85 | print(' - enter HELP as the command for a list of available commands (namespaces)')
86 | print(' - add -C to create a config file with parameter defaults.\n')
87 | print('To create a configfile')
88 | print(' - Compose the command line with all the values desired as defaults')
89 | print(' - Append a -C to the end of the commandline, the file will be created (if it does not already exist)')
90 | print(' - Any future runs will use the defaults, which can be overridden if needed.\n')
91 | print('Help commands')
92 | print(' - list of namespaces: kodi_cli Help')
93 | print(' - Methods for Namespace: kodi_cli Help Application')
94 | print(' - Parameters for Method: kodi_cli Help Application.GetProperties\n')
95 | print('Details for namespaces, methods and parameters may be found at https://kodi.wiki/view/JSON-RPC_API/v12')
96 |
97 |
98 | # === Misc. routines =================================================================================
99 | def setup_logging(settings_dict: dict):
100 | # TODO: Externalize logging settings
101 | import logging
102 | lg_format = settings_dict['log_format']
103 | lg_level = settings_dict['log_level']
104 | # logging.TRACE = logging.DEBUG + 5
105 | logging.basicConfig(format=lg_format, level=lg_level,)
106 |
107 | def dump_args(args):
108 | LOGGER.debug('Runtime Settings:')
109 | LOGGER.debug(' Key Value')
110 | LOGGER.debug(' --------------- -----------------------------------------------')
111 | if isinstance(args, dict):
112 | for k,v in args.items():
113 | if k == 'password':
114 | v = '*'*len(v)
115 | LOGGER.debug(f' {k:15} {v}')
116 | else:
117 | for entry in args._get_kwargs():
118 | LOGGER.debug(f' {entry[0]:15} {entry[1]}')
119 | LOGGER.debug(' ---------------------------------------------------------------')
120 |
121 |
122 | def display_program_info():
123 | kodi = KodiObj()
124 | # LOGGER.setLevel(logging.DEBUG)
125 | this_path = pathlib.Path(__file__).absolute().parent
126 |
127 | LOGGER.success('Calling Info-')
128 | LOGGER.info(f' Command Line : {" ".join(sys.argv)}')
129 | LOGGER.info(f' Current dir : {os.getcwd()}')
130 | LOGGER.info(f' Current root : {this_path}')
131 | LOGGER.success('Program Info-')
132 | LOGGER.info(f' Version : {cfg.__version__}')
133 | LOGGER.info(f' Installed at : {this_path}')
134 | exists = 'exists' if cfg.CONFIG_EXISTS else 'does not exist'
135 | LOGGER.info(f' Config : {cfg.FILE_CONFIG} [{exists}]')
136 | LOGGER.success('Host Info-')
137 | host_info = cfg.get_host_info()
138 | for k,v in host_info.items():
139 | LOGGER.info(f' {k:13}: {v}')
140 | LOGGER.info(f' API version : {kodi._kodi_api_version}')
141 | LOGGER.info('')
142 |
143 | def initialize_loggers(args: argparse.Namespace):
144 | log_filename = pathlib.Path(cfg.logging_filename) # pathlib.Path('./logs/da-photo.log')
145 |
146 | log_level = cfg.logging_level
147 | if log_level.upper() == 'INFO':
148 | console_format = cfg.DEFAULT_CONSOLE_LOGFMT
149 | else:
150 | console_format = cfg.DEBUG_CONSOLE_LOGFMT
151 |
152 | c_handle = cfg.configure_logger(log_format=console_format, log_level=log_level)
153 | f_handle = -1
154 | if cfg.logging_enabled:
155 | f_handle = cfg.configure_logger(log_filename, log_format=cfg.DEFAULT_FILE_LOGFMT, log_level=log_level,
156 | rotation=cfg.logging_rotation, retention=cfg.logging_retention)
157 |
158 | # Hack for future ability to dynamically change logging levels
159 | LOGGER.c_handle = c_handle
160 | LOGGER.f_handle = f_handle
161 |
162 | if len(cfg.logger_blacklist.strip()) > 0:
163 | for logger_name in cfg.logger_blacklist.split(','):
164 | LOGGER.disable(logger_name)
165 |
166 | def apply_overrides(args: argparse.Namespace):
167 | for key, val in args._get_kwargs():
168 | cfg_val = getattr(cfg, key, None)
169 | if cfg_val is not None and cfg_val != val:
170 | # LOGGER.debug(f'CmdLine Override: {key}: {val}')
171 | setattr(cfg, key, val)
172 |
173 | if args.verbose:
174 | if args.verbose == 1:
175 | cfg.logging_level = 'INFO'
176 | elif args.verbose == 2:
177 | cfg.logging_level = 'DEBUG'
178 | elif args.verbose > 2:
179 | cfg.logging_level = 'TRACE'
180 |
181 | # ==== Main script body =================================================================================
182 | def main() -> int:
183 | parser = argparse.ArgumentParser(description=f'Kodi CLI controller v{__version__}')
184 | parser.formatter_class = argparse.RawDescriptionHelpFormatter
185 | parser.description = textwrap.dedent('''\
186 | command is formatted as follows:
187 | Namespace.Method [parameter [parameter] [...]]
188 |
189 | example - Retrieve list of 5 addons:
190 | kodi-cli -H myHost Addons.GetAddons properties=[name,version,summary] limits={start:0,end:5}
191 | ''')
192 | parser.add_argument("-H","--host", type=str, default=cfg.host, help="Kodi hostname")
193 | parser.add_argument("-P","--port", type=int, default=cfg.port,help="Kodi RPC listen port")
194 | parser.add_argument("-u","--kodi-user", type=str, default=cfg.kodi_user,help="Kodi authenticaetion username")
195 | parser.add_argument("-p","--kodi_pw", type=str, default=cfg.kodi_pw,help="Kodi autentication password")
196 | parser.add_argument('-C','--create_config', action='store_true', help='Create default config')
197 | parser.add_argument('-CO','--create_config_overwrite', action='store_true', help='Create default config, overwrite if exists')
198 | parser.add_argument("-f","--format_output", action="store_true", default=cfg.format_output,help="Format json output")
199 | parser.add_argument('-c',"--csv-output", action="store_true", default=cfg.csv_output,help="Format csv output (only specific commands)")
200 | parser.add_argument("-v","--verbose", action='count', help="Verbose output, -v = INFO, -vv = DEBUG, -vvv TRACE")
201 | parser.add_argument("-i","--info", action='store_true', help='display program info and quit')
202 | parser.add_argument("command", type=str, nargs='*', help="RPC command namespace.method (help namespace to list)")
203 | args = parser.parse_args()
204 |
205 | apply_overrides(args)
206 | initialize_loggers(args)
207 |
208 | if args.create_config or args.create_config_overwrite:
209 | # create_config(args)
210 | try:
211 | if args.create_config_overwrite:
212 | cfg.create_template_config(overwrite=True)
213 | else:
214 | cfg.create_template_config(overwrite=False)
215 | return 0
216 | except Exception as ex:
217 | LOGGER.critical(repr(ex))
218 | return -1
219 |
220 | if args.info:
221 | display_program_info()
222 | # dump_args(args_dict)
223 | return 0
224 |
225 | if not args.command:
226 | display_script_help(parser.format_usage())
227 | return -1 # missing arguments
228 |
229 | kodi = KodiObj(cfg.host, cfg.port, cfg.kodi_user, cfg.kodi_pw, cfg._json_rpc_loc)
230 |
231 | # Process command-line input
232 | reference, namespace, method, param_dict = parse_input(args.command)
233 | if reference:
234 | kodi.help(reference)
235 | return 0
236 |
237 | if namespace == "help":
238 | kodi.help()
239 | return 0
240 |
241 | method_sig = f'{namespace}.{method}'
242 | if 'help' in param_dict.keys():
243 | kodi.help(method_sig)
244 | return 0
245 |
246 | if not kodi.check_command(namespace, method):
247 | kodi.help(method_sig)
248 | return -2
249 |
250 | factory = output_factory.ObjectFactory()
251 | factory.register_builder("CSV", output_factory.CSV_OutputServiceBuilder())
252 | factory.register_builder("JSON", output_factory.JSON_OutputServiceBuilder())
253 |
254 | kodi.send_request(namespace, method, param_dict)
255 | response = kodi.response_text
256 | if cfg.csv_output and method_sig in kodi.CSV_CAPABLE_COMMANDS.keys():
257 | json_node = kodi.CSV_CAPABLE_COMMANDS[method_sig]
258 | output_obj = factory.create("CSV", response_text=response, list_key=json_node)
259 | else:
260 | if cfg.csv_output:
261 | LOGGER.info('csv option is not available for this command...')
262 | if cfg.format_output:
263 | pretty = True
264 | else:
265 | pretty = False
266 | output_obj = factory.create("JSON", response_text=response, pretty=pretty)
267 |
268 | output_obj.output_result()
269 | return kodi.response_status_code
270 |
271 | if __name__ == "__main__":
272 | sys.exit(main())
273 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kodi-cli
2 |
3 | ## Command Line Interface for Kodi
4 |
5 | This tool can be used from the command line to execute commands against a target Kodi host via the RPC interface defined at https://kodi.wiki/view/JSON-RPC_API/v12.
6 |
7 | The available commands are defined via jsons files ([**methods.json and types.json**](https://github.com/JavaWiz1/kodi-cli/blob/develop/json-defs/)) which describes all the namespaces, methods, parameters (methods.json) and reference types (types.json) for managing the kodi device remotely. These files are copied from the kodi source repository for kodi v19 (matrix).
8 |
9 | **Note**
10 | - Namespace Methods and Types are case-sensitive. Use help parameter or refer to the [Kodi RPC page](https://kodi.wiki/view/JSON-RPC_API/v12) for proper capitalization.
11 |
12 | - An *entrypoint* is created on install that allows the script to be run without specifying ***python kodi_cli/driver.py***, simply type ***kodi-cli*** to execute the script.
13 |
14 | - [Poetry](https://python-poetry.org/) was used in development for dependency and package management. If you are running directly from source:
15 | - first do a ***poetry install*** to create a virtual environment with the dependencies.
16 | - then to run: ***poetry run python kodi_cli.driver.py***.
17 |
18 | The documentation will reflect calls using the entrypoint (*kodi-cli*) as described above.
19 |
20 |
21 |
22 |
23 |
24 | ---
25 | ## Overall Description
26 |
27 | *Commands* are based on Kodi **namespaces, methods and parameters**. Each namespace (i.e. Application, System,...) has a set of pre-defned methods. When executing a command you supply the **Namespace.Method parameter(s)** (case-sensitive).
28 |
29 | For example, to display the mute and volume level settings on host kodi001, the command is constructed as follows:
30 |
31 | `kodi-cli -H kodi001 -f Application.GetProperties properties=[muted,volume]`
32 | - **-H kodi001** identifies the target host
33 | - **-f** indicates the output should be formatted
34 | - **Application.GetProperties** is the parameter
35 | - *Application* is the namespace
36 | - *GetProperties* is the method
37 | - *properties=[muted,volume]* are the parameters
38 |
39 | The output of the tool is the json response from the Kodi endpoint the command was targeted to.
40 | ```
41 | {
42 | "id": 1,
43 | "jsonrpc": "2.0",
44 | "result": {
45 | "muted": false,
46 | "volume": 100
47 | }
48 | }
49 | ```
50 |
51 | When defining an object type parameter, create it as a pseudo dictionary as below:
52 |
53 | `kodi-cli -H kodi001 Addons.GetAddons properties=[name,version,summary] limits={start:0,end:99}`
54 |
55 | - **limits** is an object, that contains two values start and end.
56 | ## Some terms:
57 |
58 | | Term | Description |
59 | | ------------- | ---------------------------- |
60 | | namespace | The data model is split into namespace components which can be called via the API |
61 | | methods | Each namespace has a number of methods which perform some function within that namespace |
62 | | parameter(s) | Each namespace.method command may have required or optional parameters to control the output |
63 | | command | A command is a namespace, method, parameter combiniation used to control Kodi function (fmt: Namespace.Method) |
64 |
65 |
66 | Parameters can be one of the following types:
67 |
68 | - string (default)
69 | - boolean (true / false)
70 | - integer
71 | - list (`[ ... , ... ]`)
72 | - dict (`{ key: value, ... }`)
73 |
74 | ---
75 |
76 | ## Usage
77 |
78 | ```
79 | usage: kodi-cli [-h] [-H HOST] [-P PORT] [-u USER] [-p PASSWORD] [-C] [-f] [-v] [-i] [command [parameter ...]]
80 |
81 | Kodi CLI controller v0.2.1
82 |
83 | positional arguments:
84 | command RPC command namespace.method (help namespace to list)
85 |
86 | optional arguments:
87 | -h, --help show this help message and exit
88 | -H HOST, --host HOST Kodi hostname
89 | -P PORT, --port PORT Kodi RPC listen port
90 | -u USER, --kodi-user USER
91 | Kodi authenticaetion username
92 | -p PASSWORD, --kodi-password PASSWORD
93 | Kodi autentication password
94 | -C, --create_config Create empty config
95 | -CO, --create_config_overwrite
96 | Create default config, overwrite if exists
97 | -f, --format_output Format json output
98 | -c, --csv-output Format csv output (only specific commands)
99 | -v, --verbose Verbose output, -v = INFO, -vv = DEBUG
100 | -i, --info display program info and quit
101 | ```
102 |
103 |
104 | **TIPS - When calling the script:**
105 | | action | description |
106 | | ------ | ----------- |
107 | | add -h option | to display script syntax and list of option parameters |
108 | | enter help | as a parameter for help on namespace or namespace.method or namespace.type
add -v to get json defintion|
109 | | add -i | to output runtime and program information |
110 | | add -f | to format the json output into a friendly format |
111 | | add -C or (-CO) | to create a config file with runtime defaults (see "Create config file to store defaults" below)|
112 |
113 |
114 | **Help commands:**
115 | You can get help from the command line to view namespaces, namespace methods and calling requirements.
116 |
117 | Help Examples
118 | | action | example |
119 | | ------ | ------- |
120 | | list of namespaces | `kodi-cli help` |
121 | | Methods for Namespace | `kodi-cli Application help` |
122 | | Parameters for Method | `kodi-cli Application.GetProperties help` |
123 | | Type/References | `kodi-cli List.Filter.Albums` to get the type defintion for the reference |
124 |
125 | Details for namespaces, methods and type parameters may be found at https://kodi.wiki/view/JSON-RPC_API/v12
126 |
127 |
128 |
129 | ---
130 | ## Prerequsites:
131 |
132 | **Python 3.8+**
133 | **Python packages**
134 | - requests package
135 | - loguru
136 |
137 | Code can be installed via pip or [pipx](https://github.com/pypa/pipx):
138 | - pip install kodi-cli [--user]
139 | - [pipx](https://github.com/pypa/pipx) install kodi-cli
140 |
141 | **Kodi configuration**
142 | - Remote control via HTTP must be enabled.
143 | - Enable in Kodi UI - Settings -> Services -> Control
144 |
145 |
146 | ---
147 | ## Usage Examples
148 | ---
149 | ### Create a config file to store defaults
150 | To minimize command-line entry, you can store defaults in a config file which will default values on startup. The values can be over-ridded at run-time by providing the optional command-line argument.
151 |
152 | To create a default config file, type your standard defaults as if you were going to execute the CLI and add -C (or -CO) at the end.
153 | The config file will be written with the values.
154 | ```
155 | SYNTAX:
156 | kodi-cli -H LibreElec1 -u myId -p myPassword -P 8080 -C
157 |
158 | OUTPUT:
159 | a file kodi_cli.cfg will be written as:
160 | ...
161 | [LOGGING]
162 | logging_enabled = False
163 | logging_filename = ./logs/kodi_cli.log
164 | logging_rotation = 1 MB
165 | logging_retention = 3
166 | logging_level = INFO
167 | logger_blacklist =
168 |
169 | [SERVER]
170 | host = LibreElec1
171 | port = 8080
172 |
173 | [LOGIN]
174 | kodi_user = myId
175 | kodi_pw = myPassword
176 |
177 | [OUTPUT]
178 | format_output = False
179 | csv_output = False
180 | ```
181 | NOTE:
182 | - if current file does not exist, it will be created as ~/.kodi_cli/kodi_cli.cfg
183 | - if current file does exist error will be thrown unless -CO option is used.
184 | - user and password is stored in clear text
185 |
186 |
187 | ---
188 | ### List all **namespaces**
189 |
190 | Namespaces are modules in Kodi, each namespace manages differ aspects of the Kodi interface
191 |
192 | ```
193 | SYNTAX:
194 | kodi-cli help
195 |
196 | OUTPUT:
197 |
198 | Kodi namespaces -
199 | Namespace Methods
200 | --------------- ----------------------------------------------------------------------------
201 | Addons ExecuteAddon, GetAddonDetails, GetAddons, SetAddonEnabled
202 |
203 | Application GetProperties, Quit, SetMute, SetVolume
204 |
205 | AudioLibrary Clean, GetAlbumDetails, GetAlbums, GetArtistDetails,
206 | GetArtists, Scan
207 |
208 | Favorites AddFavorite, GetFavorites
209 |
210 | GUI ActivateWindow, ShowNotification
211 |
212 | Input Back, ButtonEvent, ContextMenu, Down, ExecuteAction,
213 | Home, Info, Left, Right, Select, SendText,
214 | ShowCodec, ShowOSD, ShowPlayProcessInfo, Up
215 | ...
216 | ```
217 |
218 |
219 | ---
220 | ### List all namespace ***methods***
221 |
222 | Each namespace has a number of methods that can be called.
223 |
224 | To get a list of supported of supported methods for the ***Application*** namespace
225 |
226 | ```
227 | SYNTAX:
228 | kodi-cli Application help
229 | or
230 | kodi-cli Application
231 |
232 | OUTPUT:
233 |
234 | Application Namespace Methods:
235 |
236 | Method Description
237 | ------------------------- --------------------------------------------
238 | Application.GetProperties Retrieves the values of the given properties
239 | Application.Quit Quit application
240 | Application.SetMute Toggle mute/unmute
241 | Application.SetVolume Set the current volume
242 | ```
243 |
244 |
245 | ---
246 | ### List the ***method calling signature*** for a namespace method
247 |
248 | List the sytax for the Application.SetMute command
249 |
250 | ```
251 | ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
252 | Signature : Application.SetMute(mute)
253 | Description : Toggle mute/unmute
254 | ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
255 | mute
256 | Required : True
257 | Reference : Global.Toggle
258 | Type : boolean: True,False
259 | string: enum [toggle]
260 | Values : True, False
261 |
262 | ```
263 | To use:
264 | ``` kodi-cli -H kodi001 Application.SetMute mute=toggle ```
265 |
266 |
267 |
268 | ---
269 | ## Command Execution Examples
270 |
271 | ### Toggle Mute on/off
272 |
273 | To toggle the mute on, then off
274 |
275 | First call will toggle mute on, 2nd call will toggle mute off.
276 |
277 | ```
278 | SYNTAX:
279 | kodi-cli -H ServerName Application.SetMute mute=toggle
280 |
281 | OUTPUT:
282 | kodi-cli -H MyKodiServer Application.SetMute mute=toggle
283 | {"id":1,"jsonrpc":"2.0","result":true}
284 |
285 | kodi-cli -H MyKodiServer Application.SetMute mute=toggle
286 | {"id":1,"jsonrpc":"2.0","result":false}
287 | ```
288 |
289 |
290 | ---
291 | ### Retrieve Application Properties
292 |
293 | To retrieve the muted status and volume level for server kodi001
294 | ```
295 | SYNTAX:
296 | kodi-cli -H kodi001 Application.GetProperties properties=[muted,volume] -f
297 |
298 | OUTPUT:
299 | {
300 | "id": 1,
301 | "jsonrpc": "2.0",
302 | "result": {
303 | "muted": false,
304 | "volume": 100
305 | }
306 | }
307 |
308 | ```
309 |
310 |
311 | ---
312 | ### List Addons
313 |
314 | To retrieve the list of the first five Addons
315 | ```
316 | SYNTAX:
317 | kodi-cli -H kodi001 Addons.GetAddons properties=[name,version,summary] limits={start:0,end:5} -f
318 |
319 | OUTPUT:
320 | {
321 | "id": 1,
322 | "jsonrpc": "2.0",
323 | "result": {
324 | "addons": [
325 | {
326 | "addonid": "audioencoder.kodi.builtin.aac",
327 | "name": "AAC encoder",
328 | "summary": "AAC Audio Encoder",
329 | "type": "kodi.audioencoder",
330 | "version": "1.0.2"
331 | },
332 |
333 | ...
334 |
335 | {
336 | "addonid": "webinterface.default",
337 | "name": "Kodi web interface - Chorus2",
338 | "summary": "Default web interface",
339 | "type": "xbmc.webinterface",
340 | "version": "19.x-2.4.8"
341 | }
342 | ],
343 | "limits": {
344 | "end": 5,
345 | "start": 0,
346 | "total": 34
347 | }
348 | }
349 | }
350 |
351 | ```
352 |
353 |
354 | ---
355 | ### Display a notification on Kodi UI
356 | To display a warning message on Kodi running on kodi001 for 5 seconds
357 | ```
358 | SYNTAX:
359 | kodi-cli -H kodi001 GUI.ShowNotification title="Dinner Time" message="Time to eat!" image="warning" displaytime=5000
360 |
361 | OUTPUT:
362 | {"id":1,"jsonrpc":"2.0","result":"OK"}
363 |
364 | ```
365 |
366 |
367 | ---
368 | Still TODO:
369 | - Edit parameters prior to call to avoid runtime error.
370 | - Provide additional help/runtime detail on parameters
371 | - Different output formats (rather than just raw and formatted json)
372 |
373 | ---
374 | ### Stream a URL
375 |
376 | ```
377 | SYNTAX:
378 | kodi-cli Player.Open item='{file:https://getsamplefiles.com/download/mkv/sample-1.mkv}'
379 |
380 | OUTPUT:
381 | {"id":1,"jsonrpc":"2.0","result":"OK"}
382 | ```
--------------------------------------------------------------------------------
/kodi_cli/kodi_interface.py:
--------------------------------------------------------------------------------
1 | import json
2 | from loguru import logger as LOGGER
3 | import os
4 | import pathlib
5 | import socket
6 | import time
7 | import requests
8 | from typing import Tuple
9 |
10 | # TODO:
11 | # Edit parameters prior to call for cleaner error messages
12 | # Parse parameter json better
13 |
14 | class HelpParameter():
15 | _max_rows = 0
16 | _max_cols = 0
17 |
18 | def __init__(self, parameter_dict: dict = None, reference_dict: dict = None):
19 | self._parameter_block = parameter_dict
20 | self._reference_block = reference_dict
21 |
22 | self.name = None
23 | self.description = None
24 | self.default = ""
25 | self.minItems = None
26 | self.minLength = None
27 | self.minimum = None
28 | self.properties = None
29 | self.required = None
30 | self.types = None
31 | self.uniqueItems = None
32 | self.reference = None
33 | self.values = ""
34 |
35 | self._parameter_block = parameter_dict
36 | self._reference_block = reference_dict
37 | self._get_console_size()
38 |
39 |
40 | def populate(self):
41 | LOGGER.trace(f'populate() - block: {self._parameter_block}')
42 | self._populate_from_param_dict()
43 | if self.reference:
44 | self.values += self._populate_values_from_reference_dict(self.reference)
45 |
46 |
47 | def _populate_from_param_dict(self):
48 | LOGGER.trace('_populate_from_param_dict()')
49 | self.name = self._get_parameter_value(self._parameter_block,'name')
50 | self.description = self._get_parameter_value(self._parameter_block,'description')
51 | self.default = self._get_parameter_value(self._parameter_block,'default')
52 | self.minItems = self._get_parameter_value(self._parameter_block,'minItems')
53 | self.minLength = self._get_parameter_value(self._parameter_block,'minLength')
54 | self.minimum = self._get_parameter_value(self._parameter_block,'minimum')
55 | self.properties = self._get_parameter_value(self._parameter_block,'properties')
56 | self.required = self._get_parameter_value(self._parameter_block,'required')
57 | self.types = self._get_types(self._parameter_block)
58 | self.uniqueItems = self._get_parameter_value(self._parameter_block,'uniqueItems')
59 | self.reference = self._get_parameter_value(self._parameter_block, '$ref')
60 | if not self.reference:
61 | items = self._get_parameter_value(self._parameter_block, "items", None)
62 | if items:
63 | self.reference = self._get_parameter_value(items, "$ref")
64 |
65 | def _populate_values_from_reference_dict(self, ref_id: str) -> str:
66 | LOGGER.trace(f'_populate_from_reference_dict({ref_id})')
67 | values = ""
68 | ref_block = self._get_reference_id_definition(ref_id)
69 | LOGGER.debug(f'{ref_block}')
70 | if ref_block:
71 | if 'type' not in ref_block:
72 | if 'items' in ref_block:
73 | ref_block = ref_block.get('items')
74 |
75 | if 'type' in ref_block:
76 | r_types = ref_block.get('type')
77 | if not self.description:
78 | self.description = self._get_parameter_value(ref_block, 'description')
79 | r_enums = []
80 | if isinstance(r_types, str):
81 | if r_types == "string":
82 | r_enums = ref_block.get('enums',[])
83 | self.types = self._get_types(ref_block)
84 | elif r_types == "boolean":
85 | r_enums = ['True','False']
86 | elif isinstance(r_types, list):
87 | for r_type in r_types:
88 | if isinstance(r_type) is dict:
89 | if r_type.get('enums'):
90 | r_enums.extend(r_type['enums'])
91 | elif r_type.get('type') == 'boolean':
92 | r_enums.extend(['True','False'])
93 | elif r_type.get('type') == 'integer':
94 | r_max = r_type.get('maximum')
95 | r_min = r_type.get('minimum')
96 | if r_max:
97 | r_enums.extend([f'{r_min}..{r_max}'])
98 | r_enums = sorted(set(r_enums)) # unique list
99 | values = ', '.join(r_enums)
100 |
101 | return values
102 |
103 | def print_parameter_definition(self):
104 | print(f'{self.name}')
105 | self._print_parameter_line(' Desc ', self.description)
106 | self._print_parameter_line(' Min Items', self.minItems)
107 | self._print_parameter_line(' Required ', self.required)
108 | self._print_parameter_line(' Reference', self.reference)
109 | # self._print_parameter_line(' Maximum ', p_max)
110 | if self.types:
111 | self._print_types()
112 | self._print_parameter_line(' UniqItems', self.uniqueItems)
113 | self._print_parameter_line(' Minimum ', self.minimum)
114 | self._print_parameter_line(' Values ', self.values)
115 | self._print_parameter_line(' Default ', self.default)
116 | print()
117 |
118 |
119 | def _print_types(self):
120 | indent = 8
121 | type_list = self.types.split('|')
122 | type_list = list(filter(None, type_list))
123 | type_set = sorted(set(type_list)) # Remove duplicates
124 | caption = ' Type '
125 | for p_type in type_set:
126 | if len(p_type) > self._max_cols - len(caption):
127 | token = p_type.split('[')
128 | if len(token) > 1:
129 | self._print_parameter_line(caption, token[0])
130 | p_type = f'{" "*indent}[{token[1]}'
131 | caption = ' '
132 | self._print_parameter_line(caption, p_type, indent)
133 | caption = ' '
134 |
135 | def _get_types(self, block_dict: dict) -> str:
136 | return_type = ""
137 | LOGGER.trace(f'_get_types() - bloc_dict\n {block_dict}')
138 | type_token = self._get_parameter_value(block_dict, "type", "")
139 | if not type_token and '$ref' in block_dict:
140 | LOGGER.trace(f'$ref Type refinement: {block_dict}')
141 | ref_id = block_dict['$ref']
142 | ref_block = self._get_reference_id_definition(ref_id)
143 | if ref_block:
144 | return_type = self._get_types(ref_block)
145 | else:
146 | token_type = type(type_token)
147 | type_caption = f'{type_token}:'
148 | type_caption = f'{type_caption:7}'
149 | LOGGER.trace(f' type_token: {type_token} token_type: {token_type} type_caption: {type_caption}')
150 | if token_type in [ list, dict ]:
151 | if token_type is list:
152 | LOGGER.trace(f'list Type refinement: {block_dict}')
153 | return_type = ""
154 | for type_entry in type_token:
155 | if isinstance(type_entry, str):
156 | return_type += f'{type_entry},'
157 | else:
158 | return_type += f'{self._get_types(type_entry)}|'
159 | else:
160 | LOGGER.trace(f'dict Type refinement: {block_dict}')
161 | return_type += f'{self._get_types(type_token)}|'
162 |
163 | elif token_type == str:
164 | if type_token == "boolean":
165 | LOGGER.trace(f'bool Type refinement: {block_dict}')
166 | return_type = f'{type_caption} True,False'
167 |
168 | elif type_token == "integer":
169 | LOGGER.trace(f'int Type refinement: {block_dict}')
170 | t_max = int(block_dict.get('maximum',-1))
171 | t_min = int(block_dict.get('minimum', -1))
172 | return_type = type_caption
173 | if t_max > 0 and t_min >= 0:
174 | return_type = f'{type_caption} ({t_min}..{t_max})'
175 | else:
176 | return_type = type_token
177 | # TODO: Handle 'array' type in block dict
178 | elif 'enum' in block_dict:
179 | LOGGER.trace(f'enums Type refinement: {block_dict}')
180 | enums = sorted(set(block_dict['enum']))
181 | return_type = f"{type_caption} enum [{','.join(enums)}]"
182 | # return_type = f"{type_caption} enum"
183 |
184 | else:
185 | LOGGER.trace(f'str Type refinement: {block_dict}')
186 | if 'additionalProperties' in block_dict:
187 | return_type = f"{type_caption} additionalProperties"
188 | elif 'items' in block_dict:
189 | return_type = f"{type_caption} items"
190 | elif 'description' in block_dict:
191 | return_type = f'{type_caption} {block_dict["description"]}'
192 | elif '$ref' in block_dict:
193 | return_type = f'{type_caption} {block_dict["$ref"]}'
194 |
195 | if return_type:
196 | if 'default' in block_dict: # and not self.default:
197 | self.default = str(block_dict['default'])
198 | else:
199 | LOGGER.trace(f'NO Type refinement: {block_dict}')
200 | return_type = f'{type_caption} {list(block_dict.keys())[0]}'
201 | else:
202 | # TODO: Expand here for type type(range|min|max|...)
203 | LOGGER.trace(f'unk Type refinement: {block_dict}')
204 | return_type = type_token
205 |
206 | if return_type.endswith(','):
207 | return_type = return_type[:-1]
208 | LOGGER.trace(f'_get_types() returns: {return_type}')
209 | return return_type
210 |
211 | def _get_parameter_value(self, p_dict: dict, key: str, default: str = None) -> str:
212 | token = p_dict.get(key, default)
213 | # LOGGER.debug(f'_get_parameter_value() key: {key:15} dict: {p_dict}')
214 | return token
215 |
216 | def _get_reference_id_definition(self, ref_id: str) -> str:
217 | LOGGER.trace(f'_get_reference_id_definition({ref_id})')
218 | ref_dict = self._reference_block.get(ref_id, None)
219 | if ref_dict:
220 | LOGGER.debug(f'Retrieved referenceId: {ref_id}')
221 | else:
222 | LOGGER.trace(f'No reference found for: {ref_id}')
223 | return ref_dict
224 |
225 |
226 | def _print_parameter_line(cls, caption: str, value: str, value_indent: int = 0):
227 | if value:
228 | cls._get_console_size()
229 | sep = ":"
230 | if len(caption.strip()) == 0:
231 | sep = " "
232 | label = f'{caption:13}{sep} '
233 | # max_len is largest size of value before screen overflow
234 | max_len = cls._max_cols - len(label)
235 | print(f'{label}',end='')
236 | value = str(value)
237 | while len(value) > max_len:
238 | idx = value.rfind(",", 0, max_len)
239 | if idx <= 0:
240 | idx = value.rfind(" ",0, max_len)
241 | if idx <= 0:
242 | max_len = len(value)
243 | else:
244 | print(f'{value[0:idx+1]}')
245 | value = value[idx+1:].strip()
246 | value = f'{" "*value_indent}{value}'
247 | print(f"{' '*len(label)}",end='')
248 | print(value)
249 |
250 | def _get_console_size(cls) -> Tuple[int, int]:
251 | """Retrieve console size in Rows and Columns"""
252 | cls._max_rows = int(os.getenv('LINES', -1))
253 | cls._max_cols = int(os.getenv('COLUMNS', -1))
254 | if cls._max_rows <= 0 or cls._max_cols <= 0:
255 | size = os.get_terminal_size()
256 | cls._max_rows = int(size.lines)
257 | cls._max_cols = int(size.columns)
258 | return cls._max_rows, cls._max_cols
259 |
260 | class KodiObj():
261 | CSV_CAPABLE_COMMANDS = {
262 | 'Addons.GetAddons': 'addons',
263 | 'AudioLibrary.GetAlbums': 'albums',
264 | 'AudioLibrary.GetArtists': 'artists',
265 | 'AudioLibrary.GetGenres': 'genres',
266 | 'AudioLibrary.GetRecentlyAddedAlbums': 'albums',
267 | 'AudioLibrary.GetRecentlyAddedSongs': 'songs',
268 | 'AudioLibrary.GetRecentlyPlayedAlbums': 'albums',
269 | 'AudioLibrary.GetRecentlyPlayedSongs': 'songs',
270 | 'VideoLibrary.GetRecentlyAddedEpisodes': 'episodes'
271 | }
272 |
273 | def __init__(self, host: str = "localhost", port: int = 8080, user: str = None, password: str = None, json_loc: str = "./json-defs"):
274 | LOGGER.debug("KodiObj created")
275 | self._host = host
276 | self._ip = self._get_ip(host)
277 | self._port = port
278 | self._userid = user
279 | self._password = password
280 | self._kodi_api_version = None
281 | self._namespaces = {}
282 | self._kodi_references = {}
283 | self._base_url = f'http://{host}:{port}/jsonrpc'
284 | self._error_json = {
285 | "error": {
286 | "code": -1,
287 | "data": {
288 | "method": "TBD"
289 | },
290 | "message": "Invalid params."
291 | }
292 | }
293 |
294 | LOGGER.debug(f' host: {host}, ip: {self._ip}, port: {port}')
295 | this_path = pathlib.Path(__file__).absolute().parent
296 | json_dict_loc = this_path / pathlib.Path(json_loc) / "methods.json"
297 | LOGGER.debug(f' Loading method definitionsL {json_dict_loc}')
298 | all_methods = dict(sorted(self._load_kodi_json_def(json_dict_loc).items()))
299 |
300 | last_ns = ""
301 | for entry, value in all_methods.items():
302 | token = entry.split('.')
303 | ns = token[0]
304 | method = token[1]
305 | if ns != last_ns:
306 | self._namespaces[ns] = {}
307 | last_ns = ns
308 | self._namespaces[ns][method] = value
309 | self._namespaces[ns][method]['csv'] = (entry in KodiObj.CSV_CAPABLE_COMMANDS.keys())
310 |
311 | json_dict_loc = this_path / pathlib.Path(json_loc) / "types.json"
312 | LOGGER.debug(f' Loading reference/types definitions: {json_dict_loc}')
313 | self._kodi_references = self._load_kodi_json_def(json_dict_loc)
314 |
315 | kodi_version_loc = this_path / pathlib.Path(json_loc) / "version.txt"
316 | if kodi_version_loc.exists():
317 | self._kodi_api_version = kodi_version_loc.read_text().replace('\n','')
318 | else:
319 | self._kodi_api_version = "Unknown"
320 | LOGGER.debug(f' Kodi RPC Version: {self._kodi_api_version}')
321 |
322 | def get_namespace_list(self) -> list:
323 | """Returns a list of the Kodi namespace objeccts"""
324 | return self._namespaces.keys()
325 |
326 | def get_namespace_method_list(self, namespace: str) -> list:
327 | """Returns a list of the methods for the requested namespace"""
328 | commands = []
329 | LOGGER.debug(f'retrieving methods for namespace: {namespace}')
330 | ns = self._namespaces.get(namespace, None)
331 | if ns:
332 | commands = ns.keys()
333 | return commands
334 |
335 | def check_command(self, namespace: str, method: str, parms: str = None) -> bool:
336 | """Validate namespace method combination, true if valid, false if not"""
337 | self._clear_response()
338 | full_namespace = namespace
339 | if method:
340 | full_namespace += f".{method}"
341 | LOGGER.debug(f'Check Command: {full_namespace}')
342 | if namespace not in self._kodi_references:
343 | if namespace not in self._namespaces.keys():
344 | self._set_response(-10, f"Invalid namespace \'{full_namespace}", False)
345 | LOGGER.error(f'Invalid namespace \'{full_namespace}')
346 | return False
347 | if method:
348 | if method not in self._namespaces[namespace].keys():
349 | self._set_response(-20, f'\'{method}\' is not valid method\' for namespace \'{namespace}\'', False)
350 | LOGGER.error(f'\'{method}\' is not valid method\' for namespace \'{namespace}\'')
351 | return False
352 | else:
353 | LOGGER.error(f'Must supply Method for namespace \'{namespace}\'')
354 | return False
355 |
356 | param_template = self._namespaces[namespace][method]
357 | if param_template['description'] == "NOT IMPLEMENTED.":
358 | self._set_response(-30,f'{full_namespace} has not been implemented',False)
359 | LOGGER.error(f'{full_namespace} has not been implemented')
360 | return False
361 |
362 | # TODO: Check for required parameters
363 | LOGGER.debug(f' {full_namespace} is valid.')
364 | return True
365 |
366 | def send_request(self, namespace: str, command: str, input_params: dict) -> bool:
367 | """Send Namesmpace.Method command to target host"""
368 | LOGGER.trace(f"send_request('{namespace}'),('{command}'),('{input_params}')")
369 | method = f'{namespace}.{command}'
370 | LOGGER.debug(f'Load Command Template : {method}')
371 | param_template = self._namespaces[namespace][command]
372 | parm_list = param_template['params']
373 | LOGGER.debug(f' template: {param_template}')
374 | LOGGER.debug(f' parm_list: {parm_list}')
375 | req_parms = {}
376 | LOGGER.trace(' Parameter dictionary:')
377 | for parm_entry in parm_list:
378 | parm_name = parm_entry['name']
379 | parm_value = input_params.get(parm_name, None)
380 | if parm_value is None:
381 | parm_value = parm_entry.get('default', None)
382 | if parm_value is not None:
383 | if isinstance(parm_value, bool):
384 | LOGGER.trace('isBool')
385 | req_parms[parm_name] = "true" if parm_value else "false"
386 | elif isinstance(parm_value, int):
387 | req_parms[parm_name] = int(parm_value)
388 | else:
389 | req_parms[parm_name] = parm_value
390 | LOGGER.trace(f' Key : {parm_name:15} Value: {req_parms[parm_name]} {type(parm_value)}')
391 | else:
392 | LOGGER.trace(f' Key : {parm_name:15} Value: {parm_value} BYPASS')
393 |
394 | LOGGER.trace('')
395 | return self._call_kodi(method, req_parms)
396 |
397 | # === Help functions ==========================================================
398 | def help(self, input_string: str = None):
399 | """Provide help context for the namespace or namespace.method"""
400 | namesp = None
401 | method = None
402 | ref_id = None
403 | if input_string:
404 | if input_string in self._kodi_references:
405 | ref_id = input_string
406 | else:
407 | if "." in input_string:
408 | tokens = input_string.split(".")
409 | namesp = tokens[0]
410 | method = tokens[1]
411 | else:
412 | namesp = input_string
413 |
414 | LOGGER.debug(f'Help - {input_string}')
415 | LOGGER.debug(f' Namesapce : {namesp}')
416 | LOGGER.debug(f' Method : {method}')
417 | LOGGER.debug(f' RefID : {ref_id}')
418 |
419 | if ref_id:
420 | self._help_reference(ref_id)
421 | return
422 |
423 | if not namesp or (namesp == "help" or namesp == "Help"):
424 | # General help
425 | self._help_namespaces()
426 | return
427 |
428 | if namesp not in self._namespaces.keys():
429 | print(f'Unknown namespace \'{namesp}\'. Try help namespaces')
430 | return
431 |
432 | if not method or method == "None":
433 | self._help_namespace(namesp)
434 | return
435 |
436 | if method not in self.get_namespace_method_list(namesp):
437 | self._help_namespace(namesp)
438 | # print(f'Unknown command [{method}] in namespace {namesp}')
439 | return
440 |
441 | self._help_namespace_method(namesp, method)
442 |
443 | def _help_sep_line(self) -> str:
444 | return f"{'—'*HelpParameter()._max_cols}"
445 |
446 |
447 | def _help_namespaces(self):
448 | LOGGER.trace('_help_namespaces()')
449 | print("\nKodi namespaces:\n")
450 | print(" Namespace Methods")
451 | print(f" {'—'*15} {'—'*70}")
452 | for ns in self.get_namespace_list():
453 | methods = ""
454 | for method in self.get_namespace_method_list(ns):
455 | if len(methods) == 0:
456 | methods = method
457 | elif len(methods) + len(method) > 70:
458 | print(f' {ns:15} {methods},')
459 | ns = ""
460 | methods = method
461 | else:
462 | methods = f"{methods}, {method}"
463 | print(f' {ns:15} {methods}\n')
464 |
465 | def _help_namespace(self, ns: str):
466 | LOGGER.trace(f'_help_namespace({ns})')
467 | ns_commands = self.get_namespace_method_list(ns)
468 |
469 | print(f'\n{ns} Namespace:\n')
470 | print(' Method Description')
471 | print(f" {'—'*35} {'—'*50}")
472 | for token in ns_commands:
473 | def_block = self._namespaces[ns][token]
474 | # def_block = json.loads(self._namespaces[ns][token])
475 | description = def_block['description']
476 | method = f'{ns}.{token}'
477 | print(f' {method:35} {description}')
478 |
479 | def _help_namespace_method(self, ns: str, method: str):
480 | # help_json = json.loads(self._namespaces[ns][method])
481 | LOGGER.trace(f'_help_namespace_method({ns}, {method})')
482 | help_json = self._namespaces[ns][method]
483 | print()
484 | param_list = help_json.get('params', [])
485 | p_names = self._get_parameter_names(param_list)
486 | print(self._help_sep_line())
487 | HelpParameter()._print_parameter_line("Signature", f'{ns}.{method}({p_names})',)
488 | # print(f'Signature : {ns}.{method}({p_names})')
489 | description = help_json['description']
490 | if help_json.get('csv'):
491 | description = f'{description} (csv)'
492 | HelpParameter()._print_parameter_line("Description", description,)
493 | print(self._help_sep_line())
494 |
495 | if len(param_list) > 0:
496 | for param_item in param_list:
497 | hp = HelpParameter(param_item, self._kodi_references)
498 | hp.populate()
499 | hp.print_parameter_definition()
500 |
501 | LOGGER.info(f'\nRaw Json Definition:\n{json.dumps(help_json,indent=2)}')
502 |
503 | def _help_reference(self, ref_id: str):
504 | LOGGER.trace(f'__help_reference({ref_id})')
505 | help_json = self._kodi_references.get(ref_id)
506 | print(self._help_sep_line())
507 | print(f'Reference: {ref_id}')
508 | print(f'{json.dumps(help_json,indent=2)}')
509 | print('')
510 |
511 | # === Private Class Fuctions ==============================================================
512 | def _get_ip(self, host_name: str) -> str:
513 | ip = None
514 | try:
515 | ip = socket.gethostbyname(host_name)
516 | except socket.gaierror as sge:
517 | LOGGER.trace(f'{host_name} cannot be resolved: {repr(sge)}')
518 | return ip
519 |
520 | def _http_client_print(self,*args):
521 | self._requests_log.debug(" ".join(args))
522 |
523 | def _load_kodi_json_def(self, file_name: pathlib.Path) -> dict:
524 | """Load kodi namespace definition from configuration json file"""
525 | # this_path = os.path.dirname(os.path.abspath(__file__))
526 | # json_file = f'{this_path}{os.sep}kodi_namespaces.json'
527 | if not file_name.exists():
528 | raise FileNotFoundError(file_name)
529 | with open(file_name,"r") as json_fh:
530 | json_data = json.load(json_fh)
531 | return json_data
532 |
533 | def _clear_response(self):
534 | self.response_text = None
535 | self.response_status_code = None
536 | self.request_success = False
537 |
538 | def _set_response(self, code: int, text: str, success: bool = False):
539 | self.response_status_code = code
540 | self.response_text = text
541 | self.request_success = success
542 | LOGGER.debug(' Response -')
543 | LOGGER.debug(f' status_code: {code}')
544 | LOGGER.debug(f' resp_test : {text}')
545 | LOGGER.debug(f' success : {success}')
546 |
547 | def _call_kodi(self, method: str, params: dict = {}) -> bool:
548 | self._clear_response()
549 | MAX_RETRY = 2
550 | payload = {"jsonrpc": "2.0", "id": 1, "method": f"{method}", "params": params }
551 | headers = {"Content-type": "application/json"}
552 | LOGGER.trace(f'Prep call to {self._host}')
553 | LOGGER.trace(f" URL : {self._base_url}")
554 | LOGGER.trace(f" Method : {method}")
555 | LOGGER.trace(f" Payload: {payload}")
556 |
557 | retry = 0
558 | success = False # default for 1st loop cycle
559 | while not success and retry < MAX_RETRY:
560 | try:
561 | LOGGER.trace(f'Making call to {self._base_url} for {method}')
562 | resp = requests.post(self._base_url,
563 | auth=(self._userid, self._password),
564 | data=json.dumps(payload),
565 | headers=headers, timeout=(5,3)) # connect, read
566 |
567 | if resp.status_code == 200:
568 | resp_json = json.loads(resp.text)
569 | if 'error' in resp_json.keys():
570 | self._set_response(resp_json['error']['code'], resp.text, True)
571 | else:
572 | self._set_response(0, resp.text, True)
573 | success = True
574 | else:
575 | retry = MAX_RETRY
576 | resp.raise_for_status()
577 | except requests.RequestException as re:
578 | LOGGER.debug(repr(re))
579 | retry = MAX_RETRY + 1
580 | self._error_json['error']['code'] = -20
581 | self._error_json['error']['data']['method'] = method
582 | self._error_json['error']['message'] = repr(re)
583 | self._set_response(-20, json.dumps(self._error_json))
584 | except Exception as ce:
585 | LOGGER.debug(repr(ce))
586 | retry += 1
587 | if not hasattr(ce, "message"):
588 | self._error_json['error']['code'] = -30
589 | self._error_json['error']['code']['data']['method'] = method
590 | self._error_json['error']['message'] = repr(ce)
591 | self._set_response(-30, json.dumps(self._error_json))
592 | time.sleep(2)
593 |
594 | if not success:
595 | LOGGER.trace(self._error_json)
596 | return success
597 |
598 | def _get_parameter_names(self, json_param_list: list, identify_optional: bool = True) -> list:
599 | name_list = []
600 | for p_entry in json_param_list:
601 | parameter_name = p_entry['name']
602 | if p_entry.get('required', False) or not identify_optional:
603 | name_list.extend([parameter_name])
604 | else:
605 | name_list.extend([f"[{parameter_name}]"])
606 | return ', '.join(name_list)
607 |
608 |
609 | # === Parsing (parameter) routines =========================================================
610 | def _get_types(self, param: dict) -> str:
611 | param_type = param.get('type', "String")
612 | return_types = type(param_type)
613 | LOGGER.debug(f'_get_types for: {param}')
614 | if isinstance(param_type, str):
615 | return_types = param_type
616 | elif isinstance(param_type, list):
617 | types_list = []
618 | return_types = ""
619 | for token_type in param_type:
620 | if "type" in token_type:
621 | types_list.extend([token_type['type']])
622 | elif "$ref" in token_type:
623 | ref_types = self._get_reference_types(token_type)
624 | types_list.extend(ref_types.split('|'))
625 |
626 | return_types += '|'.join(list(set(types_list)))
627 |
628 | LOGGER.debug(f'_get_types returns: {return_types}')
629 | return return_types
630 |
631 | def _get_reference_types(self, param: dict) -> str:
632 | types = None
633 | ref_dict = self._get_reference_id_definition(param)
634 | LOGGER.debug(f'_get_reference_types for: {param}')
635 | if not ref_dict:
636 | types = param.get('$ref')
637 | else:
638 | r_types = ref_dict['type']
639 | ret_types = []
640 | if isinstance(r_types, str):
641 | ret_types.extend([r_types])
642 | elif isinstance(r_types, list):
643 | for r_type in ref_dict['type']:
644 | val = r_type.get('type')
645 | if not val:
646 | val = r_type.get('$ref')
647 | ret_types.extend([val])
648 | types = '|'.join(ret_types)
649 |
650 | return types
651 |
--------------------------------------------------------------------------------
/kodi_cli/json-defs/types.json:
--------------------------------------------------------------------------------
1 | {
2 | "Optional.Boolean": {
3 | "type": [ "null", "boolean" ],
4 | "default": null
5 | },
6 | "Optional.String": {
7 | "type": [ "null", "string" ],
8 | "default": null
9 | },
10 | "Optional.Integer": {
11 | "type": [ "null", "integer" ],
12 | "default": null
13 | },
14 | "Optional.Number": {
15 | "type": [ "null", "number" ],
16 | "default": null
17 | },
18 | "Array.String": {
19 | "type": "array",
20 | "items": { "type": "string", "minLength": 1 }
21 | },
22 | "Array.Integer": {
23 | "type": "array",
24 | "items": { "type": "integer" }
25 | },
26 | "Global.Time": {
27 | "type": "object",
28 | "description": "A duration.",
29 | "properties": {
30 | "hours": { "type": "integer", "required": true, "minimum": 0 },
31 | "minutes": { "type": "integer", "required": true, "minimum": 0, "maximum": 59 },
32 | "seconds": { "type": "integer", "required": true, "minimum": 0, "maximum": 59 },
33 | "milliseconds": { "type": "integer", "required": true, "minimum": 0, "maximum": 999 }
34 | },
35 | "additionalProperties": false
36 | },
37 | "Global.Weekday": {
38 | "type": "string",
39 | "enum": [ "monday", "tuesday", "wednesday", "thursday",
40 | "friday", "saturday", "sunday" ]
41 | },
42 | "Global.IncrementDecrement": {
43 | "type": "string",
44 | "enum": [ "increment", "decrement" ]
45 | },
46 | "Global.Toggle": {
47 | "type": [
48 | { "type": "boolean", "required": true },
49 | { "type": "string", "enum": [ "toggle" ], "required": true }
50 | ]
51 | },
52 | "Global.String.NotEmpty": {
53 | "type": "string",
54 | "minLength": 1
55 | },
56 | "Configuration.Notifications": {
57 | "type": "object",
58 | "properties": {
59 | "Player": { "type": "boolean", "required": true },
60 | "Playlist": { "type": "boolean", "required": true },
61 | "GUI": { "type": "boolean", "required": true },
62 | "System": { "type": "boolean", "required": true },
63 | "VideoLibrary": { "type": "boolean", "required": true },
64 | "AudioLibrary": { "type": "boolean", "required": true },
65 | "Application": { "type": "boolean", "required": true },
66 | "Input": { "type": "boolean", "required": true },
67 | "PVR": { "type": "boolean", "required": true },
68 | "Other": { "type": "boolean", "required": true }
69 | },
70 | "additionalProperties": false
71 | },
72 | "Configuration": {
73 | "type": "object", "required": true,
74 | "properties": {
75 | "notifications": { "$ref": "Configuration.Notifications", "required": true }
76 | }
77 | },
78 | "Files.Media": {
79 | "type": "string",
80 | "enum": [ "video", "music", "pictures", "files", "programs" ]
81 | },
82 | "List.Amount": {
83 | "type": "integer",
84 | "default": -1,
85 | "minimum": 0
86 | },
87 | "List.Limits": {
88 | "type": "object",
89 | "properties": {
90 | "start": { "type": "integer", "minimum": 0, "default": 0, "description": "Index of the first item to return" },
91 | "end": { "$ref": "List.Amount", "description": "Index of the last item to return" }
92 | },
93 | "additionalProperties": false
94 | },
95 | "List.LimitsReturned": {
96 | "type": "object",
97 | "properties": {
98 | "start": { "type": "integer", "minimum": 0, "default": 0 },
99 | "end": { "$ref": "List.Amount" },
100 | "total": { "type": "integer", "minimum": 0, "required": true }
101 | },
102 | "additionalProperties": false
103 | },
104 | "List.Sort": {
105 | "type": "object",
106 | "properties": {
107 | "method": { "type": "string", "default": "none",
108 | "enum": [ "none", "label", "date", "size", "file", "path", "drivetype", "title", "track", "time", "artist",
109 | "album", "albumtype", "genre", "country", "year", "rating", "userrating", "votes", "top250", "programcount",
110 | "playlist", "episode", "season", "totalepisodes", "watchedepisodes", "tvshowstatus", "tvshowtitle",
111 | "sorttitle", "productioncode", "mpaa", "studio", "dateadded", "lastplayed", "playcount", "listeners",
112 | "bitrate", "random", "totaldiscs", "originaldate", "bpm" ]
113 | },
114 | "order": { "type": "string", "default": "ascending", "enum": [ "ascending", "descending" ] },
115 | "ignorearticle": { "type": "boolean", "default": false },
116 | "useartistsortname": { "type": "boolean", "default": false }
117 | }
118 | },
119 | "Library.Id": {
120 | "type": "integer",
121 | "default": -1,
122 | "minimum": 1
123 | },
124 | "PVR.Channel.Type": {
125 | "type": "string",
126 | "enum": [ "tv", "radio" ]
127 | },
128 | "Playlist.Id": {
129 | "type": "integer",
130 | "minimum": 0,
131 | "maximum": 2,
132 | "default": -1
133 | },
134 | "Playlist.Type": {
135 | "type": "string",
136 | "enum": [ "unknown", "video", "audio", "picture", "mixed" ]
137 | },
138 | "Playlist.Property.Name": {
139 | "type": "string",
140 | "enum": [ "type", "size" ]
141 | },
142 | "Playlist.Property.Value": {
143 | "type": "object",
144 | "properties": {
145 | "type": { "$ref": "Playlist.Type" },
146 | "size": { "type": "integer", "minimum": 0 }
147 | }
148 | },
149 | "Playlist.Position": {
150 | "type": "integer",
151 | "minimum": 0,
152 | "default": -1
153 | },
154 | "Playlist.Item": {
155 | "type": [
156 | { "type": "object", "properties": { "file": { "type": "string", "description": "Path to a file (not a directory) to be added to the playlist", "required": true } }, "additionalProperties": false },
157 | { "type": "object", "properties": { "directory": { "type": "string", "required": true }, "recursive": { "type": "boolean", "default": false }, "media": { "$ref": "Files.Media", "default": "files" } }, "additionalProperties": false },
158 | { "type": "object", "properties": { "movieid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
159 | { "type": "object", "properties": { "episodeid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
160 | { "type": "object", "properties": { "musicvideoid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
161 | { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
162 | { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
163 | { "type": "object", "properties": { "songid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
164 | { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Identification of a genre from the AudioLibrary" } }, "additionalProperties": false }
165 | ]
166 | },
167 | "Player.Id": {
168 | "type": "integer",
169 | "minimum": 0,
170 | "maximum": 2,
171 | "default": -1
172 | },
173 | "Player.Type": {
174 | "type": "string",
175 | "enum": [ "video", "audio", "picture" ]
176 | },
177 | "Player.Position.Percentage": {
178 | "type": "number",
179 | "minimum": 0.0,
180 | "maximum": 100.0
181 | },
182 | "Player.Position.Time": {
183 | "type": "object",
184 | "description": "A position in duration.",
185 | "additionalProperties": false,
186 | "properties": {
187 | "hours": { "type": "integer", "minimum": 0, "default": 0 },
188 | "minutes": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0 },
189 | "seconds": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0 },
190 | "milliseconds": { "type": "integer", "minimum": 0, "maximum": 999, "default": 0 }
191 | }
192 | },
193 | "Player.Speed": {
194 | "type": "object",
195 | "required": true,
196 | "properties": {
197 | "speed": { "type": "integer" }
198 | }
199 | },
200 | "Player.ViewMode": {
201 | "type": "string",
202 | "enum": [ "normal", "zoom", "stretch4x3", "widezoom", "stretch16x9", "original",
203 | "stretch16x9nonlin", "zoom120width", "zoom110width" ]
204 | },
205 | "Player.CustomViewMode": {
206 | "type": "object",
207 | "required": true,
208 | "properties": {
209 | "zoom": { "type": [
210 | { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
211 | { "$ref": "Optional.Number", "minimum":0.5, "maximum": 2.0, "description": "Zoom where 1.0 means 100%", "required": true } ] },
212 | "pixelratio": { "type": [
213 | { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
214 | { "$ref": "Optional.Number", "minimum":0.5, "maximum": 2.0, "description": "Pixel aspect ratio where 1.0 means square pixel", "required": true } ] },
215 | "verticalshift": { "type": [
216 | { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
217 | { "$ref": "Optional.Number", "minimum": -2.0, "maximum": 2.0, "description": "Vertical shift 1.0 means shift to bottom", "required": true } ] },
218 | "nonlinearstretch": { "type": [
219 | { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
220 | { "$ref": "Optional.Boolean", "description": "Flag to enable nonlinear stretch", "required": true } ] }
221 | }
222 | },
223 | "Player.Repeat": {
224 | "type": "string",
225 | "enum": [ "off", "one", "all" ]
226 | },
227 | "Player.Audio.Stream": {
228 | "type": "object",
229 | "properties": {
230 | "index": { "type": "integer", "minimum": 0, "required": true },
231 | "name": { "type": "string", "required": true },
232 | "language": { "type": "string", "required": true },
233 | "codec": { "type": "string", "required": true },
234 | "bitrate": { "type": "integer", "required": true },
235 | "channels": { "type": "integer", "required": true },
236 | "isdefault": { "type": "boolean", "required": true },
237 | "isoriginal": { "type": "boolean", "required": true },
238 | "isimpaired": { "type": "boolean", "required": true },
239 | "samplerate": { "type": "integer", "required": true }
240 | }
241 | },
242 | "Player.Video.Stream": {
243 | "type": "object",
244 | "properties": {
245 | "index": { "type": "integer", "minimum": 0, "required": true },
246 | "name": { "type": "string", "required": true },
247 | "language": { "type": "string", "required": true },
248 | "codec": { "type": "string", "required": true },
249 | "width": { "type": "integer", "required": true },
250 | "height": { "type": "integer", "required": true }
251 | }
252 | },
253 | "Player.Subtitle": {
254 | "type": "object",
255 | "properties": {
256 | "index": { "type": "integer", "minimum": 0, "required": true },
257 | "name": { "type": "string", "required": true },
258 | "language": { "type": "string", "required": true },
259 | "isdefault": { "type": "boolean", "required": true },
260 | "isforced": { "type": "boolean", "required": true },
261 | "isimpaired": { "type": "boolean", "required": true }
262 | }
263 | },
264 | "Player.Property.Name": {
265 | "type": "string",
266 | "enum": [ "type", "partymode", "speed", "time", "percentage",
267 | "totaltime", "playlistid", "position", "repeat", "shuffled",
268 | "canseek", "canchangespeed", "canmove", "canzoom", "canrotate",
269 | "canshuffle", "canrepeat", "currentaudiostream", "audiostreams",
270 | "subtitleenabled", "currentsubtitle", "subtitles", "live",
271 | "currentvideostream", "videostreams", "cachepercentage" ]
272 | },
273 | "Player.Property.Value": {
274 | "type": "object",
275 | "properties": {
276 | "type": { "$ref": "Player.Type" },
277 | "partymode": { "type": "boolean" },
278 | "speed": { "type": "integer" },
279 | "time": { "$ref": "Global.Time" },
280 | "percentage": { "$ref": "Player.Position.Percentage" },
281 | "totaltime": { "$ref": "Global.Time" },
282 | "playlistid": { "$ref": "Playlist.Id" },
283 | "position": { "$ref": "Playlist.Position" },
284 | "repeat": { "$ref": "Player.Repeat" },
285 | "shuffled": { "type": "boolean" },
286 | "canseek": { "type": "boolean" },
287 | "canchangespeed": { "type": "boolean" },
288 | "canmove": { "type": "boolean" },
289 | "canzoom": { "type": "boolean" },
290 | "canrotate": { "type": "boolean" },
291 | "canshuffle": { "type": "boolean" },
292 | "canrepeat": { "type": "boolean" },
293 | "currentaudiostream": { "$ref": "Player.Audio.Stream" },
294 | "audiostreams": { "type": "array", "items": { "$ref": "Player.Audio.Stream" } },
295 | "currentvideostream": { "$ref": "Player.Video.Stream" },
296 | "videostreams": { "type": "array", "items": { "$ref": "Player.Video.Stream" } },
297 | "subtitleenabled": { "type": "boolean" },
298 | "currentsubtitle": { "$ref": "Player.Subtitle" },
299 | "subtitles": { "type": "array", "items": { "$ref": "Player.Subtitle" } },
300 | "live": { "type": "boolean" },
301 | "cachepercentage": { "$ref": "Player.Position.Percentage" }
302 | }
303 | },
304 | "Notifications.Item.Type": {
305 | "type": "string",
306 | "enum": [ "unknown", "movie", "episode", "musicvideo", "song", "picture", "channel" ]
307 | },
308 | "Notifications.Item": {
309 | "type": [
310 | { "type": "object", "description": "An unknown item does not have any additional information.",
311 | "properties": {
312 | "type": { "$ref": "Notifications.Item.Type", "required": true }
313 | }
314 | },
315 | { "type": "object", "description": "An item known to the database has an identification.",
316 | "properties": {
317 | "type": { "$ref": "Notifications.Item.Type", "required": true },
318 | "id": { "$ref": "Library.Id", "required": true }
319 | }
320 | },
321 | { "type": "object", "description": "A movie item has a title and may have a release year.",
322 | "properties": {
323 | "type": { "$ref": "Notifications.Item.Type", "required": true },
324 | "title": { "type": "string", "required": true },
325 | "year": { "type": "integer" }
326 | }
327 | },
328 | { "type": "object", "description": "A tv episode has a title and may have an episode number, season number and the title of the show it belongs to.",
329 | "properties": {
330 | "type": { "$ref": "Notifications.Item.Type", "required": true },
331 | "title": { "type": "string", "required": true },
332 | "episode": { "type": "integer" },
333 | "season": { "type": "integer" },
334 | "showtitle": { "type": "string" }
335 | }
336 | },
337 | { "type": "object", "description": "A music video has a title and may have an album and an artist.",
338 | "properties": {
339 | "type": { "$ref": "Notifications.Item.Type", "required": true },
340 | "title": { "type": "string", "required": true },
341 | "album": { "type": "string" },
342 | "artist": { "type": "string" }
343 | }
344 | },
345 | { "type": "object", "description": "A song has a title and may have an album, an artist and a track number.",
346 | "properties": {
347 | "type": { "$ref": "Notifications.Item.Type", "required": true },
348 | "title": { "type": "string", "required": true },
349 | "album": { "type": "string" },
350 | "artist": { "type": "string" },
351 | "track": { "type": "integer" }
352 | }
353 | },
354 | { "type": "object", "description": "A picture has a file path.",
355 | "properties": {
356 | "type": { "$ref": "Notifications.Item.Type", "required": true },
357 | "file": { "type": "string", "required": true }
358 | }
359 | },
360 | { "type": "object", "description": "A PVR channel is either a radio or tv channel and has a title.",
361 | "properties": {
362 | "type": { "$ref": "Notifications.Item.Type", "required": true },
363 | "id": { "$ref": "Library.Id", "required": true },
364 | "title": { "type": "string", "required": true },
365 | "channeltype": { "$ref": "PVR.Channel.Type", "required": true }
366 | }
367 | }
368 | ]
369 | },
370 | "Player.Notifications.Player": {
371 | "type": "object",
372 | "properties": {
373 | "playerid": { "$ref": "Player.Id", "required": true },
374 | "speed": { "type": "integer" }
375 | }
376 | },
377 | "Player.Notifications.Player.Seek": {
378 | "extends": "Player.Notifications.Player",
379 | "properties": {
380 | "time": { "$ref": "Global.Time" },
381 | "seekoffset": { "$ref": "Global.Time" }
382 | }
383 | },
384 | "Player.Notifications.Data": {
385 | "type": "object",
386 | "properties": {
387 | "item": { "$ref": "Notifications.Item", "required": true },
388 | "player": { "$ref": "Player.Notifications.Player", "required": true }
389 | }
390 | },
391 | "Item.Fields.Base": {
392 | "type": "array",
393 | "uniqueItems": true,
394 | "items": { "type": "string" }
395 | },
396 | "Item.Details.Base": {
397 | "type": "object",
398 | "properties": {
399 | "label": { "type": "string", "required": true }
400 | }
401 | },
402 | "Item.CustomProperties": {
403 | "type": "object",
404 | "additionalProperties": { "$ref": "Global.String.NotEmpty" }
405 | },
406 | "Media.Details.Base": {
407 | "extends": "Item.Details.Base",
408 | "properties": {
409 | "fanart": { "type": "string" },
410 | "thumbnail": { "type": "string" }
411 | }
412 | },
413 | "Media.Artwork": {
414 | "type": "object",
415 | "properties": {
416 | "thumb": { "$ref": "Global.String.NotEmpty" },
417 | "poster": { "$ref": "Global.String.NotEmpty" },
418 | "banner": { "$ref": "Global.String.NotEmpty" },
419 | "fanart": { "$ref": "Global.String.NotEmpty" }
420 | },
421 | "additionalProperties": { "$ref": "Global.String.NotEmpty" }
422 | },
423 | "Media.Artwork.Set": {
424 | "type": "object",
425 | "properties": {
426 | "thumb": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
427 | "poster": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
428 | "banner": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
429 | "fanart": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" }
430 | },
431 | "additionalProperties": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ] }
432 | },
433 | "Video.Rating": {
434 | "type": "object",
435 | "properties": {
436 | "rating": { "type": "number", "required": true },
437 | "votes": { "type": "integer" },
438 | "default": { "type": "boolean" }
439 | }
440 | },
441 | "Video.Ratings": {
442 | "type": "object",
443 | "additionalProperties": { "$ref": "Video.Rating" }
444 | },
445 | "Video.Ratings.Set": {
446 | "type": "object",
447 | "additionalProperties": { "type": [ "null", { "$ref": "Video.Rating", "required": true } ] }
448 | },
449 | "Media.UniqueID": {
450 | "type": "object",
451 | "additionalProperties": { "$ref": "Global.String.NotEmpty" }
452 | },
453 | "Media.UniqueID.Set": {
454 | "type": "object",
455 | "additionalProperties": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ] }
456 | },
457 | "Library.Fields.Source": {
458 | "extends": "Item.Fields.Base",
459 | "items": { "type": "string", "enum": [ "file", "paths" ] }
460 | },
461 | "Library.Details.Source": {
462 | "extends": "Item.Details.Base",
463 | "properties": {
464 | "sourceid": { "$ref": "Library.Id", "required": true },
465 | "file": { "type": "string", "description": "The url encoded multipath string combining all paths of the source ", "required": true },
466 | "paths": { "$ref": "Array.String", "description": "The individual paths of the media source" }
467 | }
468 | },
469 | "Library.Fields.Genre": {
470 | "extends": "Item.Fields.Base",
471 | "items": { "type": "string", "enum": [ "title", "thumbnail", "sourceid" ] }
472 | },
473 | "Library.Details.Genre": {
474 | "extends": "Item.Details.Base",
475 | "properties": {
476 | "genreid": { "$ref": "Library.Id", "required": true },
477 | "title": { "type": "string" },
478 | "thumbnail": { "type": "string" },
479 | "sourceid": { "$ref": "Array.Integer", "description": "The ids of sources with songs of the genre" }
480 | }
481 | },
482 | "Library.Fields.Tag": {
483 | "extends": "Item.Fields.Base",
484 | "items": { "type": "string", "enum": [ "title" ] }
485 | },
486 | "Library.Details.Tag": {
487 | "extends": "Item.Details.Base",
488 | "properties": {
489 | "tagid": { "$ref": "Library.Id", "required": true },
490 | "title": { "type": "string" }
491 | }
492 | },
493 | "Audio.Fields.Role": {
494 | "extends": "Item.Fields.Base",
495 | "items": { "type": "string", "enum": [ "title" ] }
496 | },
497 | "Audio.Details.Role": {
498 | "extends": "Item.Details.Base",
499 | "properties": {
500 | "roleid": { "$ref": "Library.Id", "required": true },
501 | "title": { "type": "string" }
502 | }
503 | },
504 | "Audio.Fields.Artist": {
505 | "extends": "Item.Fields.Base",
506 | "items": { "type": "string",
507 | "description": "Requesting the (song)genreid/genre, roleid/role or sourceid fields will result in increased response times",
508 | "enum": [ "instrument", "style", "mood", "born", "formed",
509 | "description", "genre", "died", "disbanded",
510 | "yearsactive", "musicbrainzartistid", "fanart",
511 | "thumbnail", "compilationartist", "dateadded",
512 | "roles", "songgenres", "isalbumartist",
513 | "sortname", "type", "gender", "disambiguation", "art", "sourceid",
514 | "datemodified", "datenew" ]
515 | }
516 | },
517 | "Audio.Fields.Album": {
518 | "extends": "Item.Fields.Base",
519 | "items": { "type": "string",
520 | "description": "Requesting the songgenres, artistid and/or sourceid fields will result in increased response times",
521 | "enum": [ "title", "description", "artist", "genre",
522 | "theme", "mood", "style", "type", "albumlabel",
523 | "rating", "votes", "userrating","year", "musicbrainzalbumid",
524 | "musicbrainzalbumartistid", "fanart", "thumbnail",
525 | "playcount", "artistid", "displayartist",
526 | "compilation", "releasetype", "dateadded",
527 | "sortartist", "musicbrainzreleasegroupid", "songgenres", "art",
528 | "lastplayed", "sourceid","isboxset", "totaldiscs",
529 | "releasedate", "originaldate", "albumstatus", "datemodified", "datenew",
530 | "albumduration"]
531 | }
532 | },
533 | "Audio.Fields.Song": {
534 | "extends": "Item.Fields.Base",
535 | "items": { "type": "string",
536 | "description": "Requesting the genreid, artistid, albumartistid and/or sourceid fields will result in increased response times",
537 | "enum": [ "title", "artist", "albumartist", "genre", "year",
538 | "rating", "album", "track", "duration", "comment",
539 | "lyrics", "musicbrainztrackid", "musicbrainzartistid",
540 | "musicbrainzalbumid", "musicbrainzalbumartistid",
541 | "playcount", "fanart", "thumbnail", "file", "albumid",
542 | "lastplayed", "disc", "genreid", "artistid", "displayartist",
543 | "albumartistid", "albumreleasetype", "dateadded",
544 | "votes", "userrating", "mood", "contributors",
545 | "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
546 | "sortartist", "art", "sourceid", "disctitle", "releasedate", "originaldate",
547 | "bpm", "samplerate", "bitrate", "channels", "datemodified", "datenew" ]
548 | }
549 | },
550 | "Audio.Album.ReleaseType": {
551 | "type": "string",
552 | "enum": [ "album", "single" ],
553 | "default": "album"
554 | },
555 | "Audio.Contributors": {
556 | "type": "array",
557 | "items": { "type": "object",
558 | "description": "The artist and the role they contribute to a song",
559 | "properties": {
560 | "name": { "type": "string", "required": true },
561 | "role": { "type": "string", "required": true },
562 | "roleid": { "$ref": "Library.Id", "required": true },
563 | "artistid": { "$ref": "Library.Id", "required": true }
564 | },
565 | "additionalProperties": false
566 | }
567 | },
568 | "Audio.Artist.Roles": {
569 | "type": "array",
570 | "items": { "type": "object",
571 | "description": "The various roles contributed by an artist to one or more songs",
572 | "properties": {
573 | "roleid": { "$ref": "Library.Id", "required": true },
574 | "role": { "type": "string", "required": true }
575 | },
576 | "additionalProperties": false
577 | }
578 | },
579 | "Audio.Details.Genres": {
580 | "type": "array",
581 | "items": { "type": "object",
582 | "properties": {
583 | "genreid": { "$ref": "Library.Id", "required": true },
584 | "title": { "type": "string" }
585 | }
586 | }
587 | },
588 | "Audio.Details.Base": {
589 | "extends": "Media.Details.Base",
590 | "properties": {
591 | "genre": { "$ref": "Array.String" },
592 | "dateadded": { "type": "string" },
593 | "art": { "$ref": "Media.Artwork" }
594 | }
595 | },
596 | "Audio.Details.Media": {
597 | "extends": "Audio.Details.Base",
598 | "properties": {
599 | "title": { "type": "string" },
600 | "artist": { "$ref": "Array.String" },
601 | "year": { "type": "integer" },
602 | "rating": { "type": "number" },
603 | "musicbrainzalbumartistid": { "$ref": "Array.String" },
604 | "artistid": { "$ref": "Array.Integer" },
605 | "displayartist": { "type" : "string" },
606 | "votes": { "type": "integer" },
607 | "userrating": { "type": "integer" },
608 | "sortartist": { "type" : "string" },
609 | "releasedate": { "type" : "string" },
610 | "originaldate": { "type" : "string" }
611 | }
612 | },
613 | "Audio.Details.Artist": {
614 | "extends": "Audio.Details.Base",
615 | "properties": {
616 | "artistid": { "$ref": "Library.Id", "required": true },
617 | "artist": { "type": "string", "required": true },
618 | "instrument": { "$ref": "Array.String" },
619 | "style": { "$ref": "Array.String" },
620 | "mood": { "$ref": "Array.String" },
621 | "born": { "type": "string" },
622 | "formed": { "type": "string" },
623 | "description": { "type": "string" },
624 | "died": { "type": "string" },
625 | "disbanded": { "type": "string" },
626 | "yearsactive": { "$ref": "Array.String" },
627 | "compilationartist": { "type": "boolean" },
628 | "musicbrainzartistid": { "$ref": "Array.String" },
629 | "roles": {"$ref": "Audio.Artist.Roles"},
630 | "songgenres": {"$ref": "Audio.Details.Genres"},
631 | "isalbumartist": { "type": "boolean" },
632 | "sortname": { "type": "string" },
633 | "type": { "type": "string" },
634 | "gender": { "type": "string" },
635 | "disambiguation": { "type": "string" },
636 | "sourceid": { "$ref": "Array.Integer" }
637 | }
638 | },
639 | "Audio.Details.Album": {
640 | "extends": "Audio.Details.Media",
641 | "properties": {
642 | "albumid": { "$ref": "Library.Id", "required": true },
643 | "description": { "type": "string" },
644 | "theme": { "$ref": "Array.String" },
645 | "mood": { "$ref": "Array.String" },
646 | "style": { "$ref": "Array.String" },
647 | "type": { "type": "string" },
648 | "albumlabel": { "type": "string" },
649 | "playcount": { "type": "integer" },
650 | "compilation": { "type": "boolean" },
651 | "releasetype": { "$ref": "Audio.Album.ReleaseType" },
652 | "musicbrainzreleasegroupid": { "type": "string" },
653 | "musicbrainzalbumid": { "type": "string" },
654 | "songgenres": {"$ref": "Audio.Details.Genres"},
655 | "lastplayed": { "type": "string" },
656 | "sourceid": { "$ref": "Array.Integer" },
657 | "isboxset" : { "type": "boolean" },
658 | "totaldiscs": { "type": "integer" },
659 | "albumstatus": { "type": "string" },
660 | "albumduration": { "type": "integer" }
661 | }
662 | },
663 | "Audio.Details.Song": {
664 | "extends": "Audio.Details.Media",
665 | "properties": {
666 | "songid": { "$ref": "Library.Id", "required": true },
667 | "file": { "type": "string" },
668 | "albumartist": { "$ref": "Array.String" },
669 | "album": { "type": "string" },
670 | "track": { "type": "integer" },
671 | "duration": { "type": "integer" },
672 | "comment": { "type": "string" },
673 | "lyrics": { "type": "string" },
674 | "playcount": { "type": "integer" },
675 | "musicbrainztrackid": { "type": "string" },
676 | "musicbrainzartistid": { "$ref": "Array.String" },
677 | "albumid": { "$ref": "Library.Id" },
678 | "lastplayed": { "type": "string" },
679 | "disc": { "type": "integer" },
680 | "albumartistid": { "$ref": "Array.Integer" },
681 | "albumreleasetype": { "$ref": "Audio.Album.ReleaseType" },
682 | "mood": { "type": "string"},
683 | "contributors": { "$ref": "Audio.Contributors" },
684 | "displaycomposer": { "type": "string"},
685 | "displayconductor": { "type": "string"},
686 | "displayorchestra": { "type": "string"},
687 | "displaylyricist": { "type": "string"},
688 | "genreid": { "$ref": "Array.Integer"},
689 | "sourceid": { "$ref": "Array.Integer" },
690 | "disctitle": { "type": "string" },
691 | "bpm": { "type": "Integer" },
692 | "samplerate": { "type": "Integer" },
693 | "bitrate": { "type": "Integer"},
694 | "channels": { "type": "Integer"}
695 | }
696 | },
697 | "Audio.Property.Name": {
698 | "type": "string",
699 | "enum": [ "missingartistid", "librarylastupdated", "librarylastcleaned", "artistlinksupdated",
700 | "songslastadded", "albumslastadded", "artistslastadded", "genreslastadded",
701 | "songsmodified", "albumsmodified", "artistsmodified"]
702 | },
703 | "Audio.Property.Value": {
704 | "type": "object",
705 | "properties": {
706 | "missingartistid": { "$ref": "Library.Id" },
707 | "librarylastupdated": { "type": "string" },
708 | "librarylastcleaned": { "type": "string" },
709 | "artistlinksupdated": { "type": "string" },
710 | "songslastadded": { "type": "string" },
711 | "albumslastadded": { "type": "string" },
712 | "artistslastadded": { "type": "string" },
713 | "genreslastadded": { "type": "string" },
714 | "songsmodified": { "type": "string" },
715 | "albumsmodified": { "type": "string" },
716 | "artistsmodified": { "type": "string" }
717 | }
718 | },
719 | "Video.Fields.Movie": {
720 | "extends": "Item.Fields.Base",
721 | "items": { "type": "string",
722 | "description": "Requesting the cast, ratings, showlink, streamdetails, uniqueid and/or tag field will result in increased response times",
723 | "enum": [ "title", "genre", "year", "rating", "director", "trailer",
724 | "tagline", "plot", "plotoutline", "originaltitle", "lastplayed",
725 | "playcount", "writer", "studio", "mpaa", "cast", "country",
726 | "imdbnumber", "runtime", "set", "showlink", "streamdetails",
727 | "top250", "votes", "fanart", "thumbnail", "file", "sorttitle",
728 | "resume", "setid", "dateadded", "tag", "art", "userrating",
729 | "ratings", "premiered", "uniqueid" ]
730 | }
731 | },
732 | "Video.Fields.MovieSet": {
733 | "extends": "Item.Fields.Base",
734 | "items": { "type": "string",
735 | "enum": [ "title", "playcount", "fanart", "thumbnail", "art", "plot" ]
736 | }
737 | },
738 | "Video.Fields.TVShow": {
739 | "extends": "Item.Fields.Base",
740 | "items": { "type": "string",
741 | "description": "Requesting the cast, ratings, uniqueid and/or tag field will result in increased response times",
742 | "enum": [ "title", "genre", "year", "rating", "plot",
743 | "studio", "mpaa", "cast", "playcount", "episode",
744 | "imdbnumber", "premiered", "votes", "lastplayed",
745 | "fanart", "thumbnail", "file", "originaltitle",
746 | "sorttitle", "episodeguide", "season", "watchedepisodes",
747 | "dateadded", "tag", "art", "userrating", "ratings",
748 | "runtime", "uniqueid" ]
749 | }
750 | },
751 | "Video.Fields.Season": {
752 | "extends": "Item.Fields.Base",
753 | "items": { "type": "string",
754 | "enum": [ "season", "showtitle", "playcount", "episode", "fanart", "thumbnail", "tvshowid",
755 | "watchedepisodes", "art", "userrating", "title" ]
756 | }
757 | },
758 | "Video.Fields.Episode": {
759 | "extends": "Item.Fields.Base",
760 | "items": { "type": "string",
761 | "description": "Requesting the cast, ratings, streamdetails, uniqueid and/or tag field will result in increased response times",
762 | "enum": [ "title", "plot", "votes", "rating", "writer",
763 | "firstaired", "playcount", "runtime", "director",
764 | "productioncode", "season", "episode", "originaltitle",
765 | "showtitle", "cast", "streamdetails", "lastplayed", "fanart",
766 | "thumbnail", "file", "resume", "tvshowid", "dateadded",
767 | "uniqueid", "art", "specialsortseason", "specialsortepisode", "userrating",
768 | "seasonid", "ratings" ]
769 | }
770 | },
771 | "Video.Fields.MusicVideo": {
772 | "extends": "Item.Fields.Base",
773 | "items": { "type": "string",
774 | "description": "Requesting the streamdetails and/or tag field will result in increased response times",
775 | "enum": [ "title", "playcount", "runtime", "director",
776 | "studio", "year", "plot", "album", "artist",
777 | "genre", "track", "streamdetails", "lastplayed",
778 | "fanart", "thumbnail", "file", "resume", "dateadded",
779 | "tag", "art", "rating", "userrating", "premiered" ]
780 | }
781 | },
782 | "Video.Cast": {
783 | "type": "array",
784 | "items": { "type": "object",
785 | "properties": {
786 | "name": { "type": "string", "required": true },
787 | "role": { "type": "string", "required": true },
788 | "order": { "type": "integer", "required": true },
789 | "thumbnail": { "type": "string" }
790 | },
791 | "additionalProperties": false
792 | }
793 | },
794 | "Video.Streams": {
795 | "type": "object",
796 | "properties": {
797 | "audio": { "type": "array", "minItems": 1,
798 | "items": { "type": "object",
799 | "properties": {
800 | "codec": { "type": "string" },
801 | "language": { "type": "string" },
802 | "channels": { "type": "integer" }
803 | },
804 | "additionalProperties": false
805 | }
806 | },
807 | "video": { "type": "array", "minItems": 1,
808 | "items": { "type": "object",
809 | "properties": {
810 | "codec": { "type": "string" },
811 | "aspect": { "type": "number" },
812 | "width": { "type": "integer" },
813 | "height": { "type": "integer" },
814 | "duration": { "type": "integer" }
815 | },
816 | "additionalProperties": false
817 | }
818 | },
819 | "subtitle": { "type": "array", "minItems": 1,
820 | "items": { "type": "object",
821 | "properties": {
822 | "language": { "type": "string" }
823 | },
824 | "additionalProperties": false
825 | }
826 | }
827 | },
828 | "additionalProperties": false
829 | },
830 | "Video.Resume": {
831 | "type": "object",
832 | "properties": {
833 | "position": { "type": "number", "minimum": 0.0 },
834 | "total": { "type": "number", "minimum": 0.0 }
835 | },
836 | "additionalProperties": false
837 | },
838 | "Video.Details.Base": {
839 | "extends": "Media.Details.Base",
840 | "properties": {
841 | "playcount": { "type": "integer" },
842 | "art": { "$ref": "Media.Artwork" }
843 | }
844 | },
845 | "Video.Details.Media": {
846 | "extends": "Video.Details.Base",
847 | "properties": {
848 | "title": { "type": "string" }
849 | }
850 | },
851 | "Video.Details.Item": {
852 | "extends": "Video.Details.Media",
853 | "properties": {
854 | "file": { "type": "string" },
855 | "plot": { "type": "string" },
856 | "lastplayed": { "type": "string" },
857 | "dateadded": { "type": "string" }
858 | }
859 | },
860 | "Video.Details.File": {
861 | "extends": "Video.Details.Item",
862 | "properties": {
863 | "runtime": { "type": "integer", "description": "Runtime in seconds" },
864 | "director": { "$ref": "Array.String" },
865 | "streamdetails": { "$ref": "Video.Streams" },
866 | "resume": { "$ref": "Video.Resume" }
867 | }
868 | },
869 | "Video.Details.Movie": {
870 | "extends": "Video.Details.File",
871 | "properties": {
872 | "movieid": { "$ref": "Library.Id", "required": true },
873 | "genre": { "$ref": "Array.String" },
874 | "year": { "type": "integer" },
875 | "rating": { "type": "number" },
876 | "trailer": { "type": "string" },
877 | "tagline": { "type": "string" },
878 | "plotoutline": { "type": "string" },
879 | "originaltitle": { "type": "string" },
880 | "sorttitle": { "type": "string" },
881 | "writer": { "$ref": "Array.String" },
882 | "studio": { "$ref": "Array.String" },
883 | "mpaa": { "type": "string" },
884 | "cast": { "$ref": "Video.Cast" },
885 | "country": { "$ref": "Array.String" },
886 | "imdbnumber": { "type": "string" },
887 | "set": { "type": "string" },
888 | "showlink": { "$ref": "Array.String" },
889 | "top250": { "type": "integer" },
890 | "votes": { "type": "string" },
891 | "setid": { "$ref": "Library.Id" },
892 | "tag": { "$ref": "Array.String" },
893 | "userrating": { "type": "integer" },
894 | "ratings": { "type": "Video.Ratings" },
895 | "premiered": { "type": "string" },
896 | "uniqueid": { "$ref": "Media.UniqueID" }
897 | }
898 | },
899 | "Video.Details.MovieSet": {
900 | "extends": "Video.Details.Media",
901 | "properties": {
902 | "setid": { "$ref": "Library.Id", "required": true },
903 | "plot": { "type": "string" }
904 | }
905 | },
906 | "Video.Details.MovieSet.Extended": {
907 | "extends": "Video.Details.MovieSet",
908 | "properties": {
909 | "limits": { "$ref": "List.LimitsReturned", "required": true },
910 | "movies": { "type": "array",
911 | "items": { "$ref": "Video.Details.Movie" }
912 | }
913 | }
914 | },
915 | "Video.Details.TVShow": {
916 | "extends": "Video.Details.Item",
917 | "properties": {
918 | "tvshowid": { "$ref": "Library.Id", "required": true },
919 | "genre": { "$ref": "Array.String" },
920 | "year": { "type": "integer" },
921 | "rating": { "type": "number" },
922 | "originaltitle": { "type": "string" },
923 | "sorttitle": { "type": "string" },
924 | "studio": { "$ref": "Array.String" },
925 | "mpaa": { "type": "string" },
926 | "cast": { "$ref": "Video.Cast" },
927 | "episode": { "type": "integer" },
928 | "watchedepisodes": { "type": "integer" },
929 | "imdbnumber": { "type": "string" },
930 | "premiered": { "type": "string" },
931 | "votes": { "type": "string" },
932 | "episodeguide": { "type": "string" },
933 | "season": { "type": "integer" },
934 | "tag": { "$ref": "Array.String" },
935 | "userrating": { "type": "integer" },
936 | "ratings": { "type": "Video.Ratings" },
937 | "runtime": { "type": "integer", "description": "Runtime in seconds" },
938 | "status": { "type": "string", "description": "Returns 'returning series', 'in production', 'planned', 'cancelled' or 'ended'" },
939 | "uniqueid": { "$ref": "Media.UniqueID" }
940 | }
941 | },
942 | "Video.Details.Season": {
943 | "extends": "Video.Details.Base",
944 | "properties": {
945 | "seasonid": { "$ref": "Library.Id", "required": true },
946 | "season": { "type": "integer", "required": true },
947 | "showtitle": { "type": "string" },
948 | "episode": { "type": "integer" },
949 | "watchedepisodes": { "type": "integer" },
950 | "tvshowid": { "$ref": "Library.Id" },
951 | "userrating": { "type": "integer" },
952 | "title": { "type": "string" }
953 | }
954 | },
955 | "Video.Details.Episode": {
956 | "extends": "Video.Details.File",
957 | "properties": {
958 | "episodeid": { "$ref": "Library.Id", "required": true },
959 | "votes": { "type": "string" },
960 | "rating": { "type": "number" },
961 | "writer": { "$ref": "Array.String" },
962 | "firstaired": { "type": "string" },
963 | "productioncode": { "type": "string" },
964 | "season": { "type": "integer" },
965 | "episode": { "type": "integer" },
966 | "uniqueid": { "$ref": "Media.UniqueID" },
967 | "originaltitle": { "type": "string" },
968 | "showtitle": { "type": "string" },
969 | "cast": { "$ref": "Video.Cast" },
970 | "tvshowid": { "$ref": "Library.Id" },
971 | "specialsortseason": { "type": "integer" },
972 | "specialsortepisode": { "type": "integer" },
973 | "userrating": { "type": "integer" },
974 | "seasonid": { "$ref": "Library.Id" },
975 | "ratings": { "type": "Video.Ratings" }
976 | }
977 | },
978 | "Video.Details.MusicVideo": {
979 | "extends": "Video.Details.File",
980 | "properties": {
981 | "musicvideoid": { "$ref": "Library.Id", "required": true },
982 | "studio": { "$ref": "Array.String" },
983 | "year": { "type": "integer" },
984 | "album": { "type": "string" },
985 | "artist": { "$ref": "Array.String" },
986 | "genre": { "$ref": "Array.String" },
987 | "track": { "type": "integer" },
988 | "tag": { "$ref": "Array.String" },
989 | "rating": { "type": "number" },
990 | "userrating": { "type": "integer" },
991 | "premiered": { "type": "string" }
992 | }
993 | },
994 | "PVR.Property.Name": {
995 | "type": "string",
996 | "enum": [ "available", "recording", "scanning" ]
997 | },
998 | "PVR.Property.Value": {
999 | "type": "object",
1000 | "properties": {
1001 | "available": { "type": "boolean" },
1002 | "recording": { "type": "boolean" },
1003 | "scanning": { "type": "boolean" }
1004 | }
1005 | },
1006 | "PVR.ChannelGroup.Id": {
1007 | "type": [
1008 | { "$ref": "Library.Id", "required": true },
1009 | { "type": "string", "enum": [ "alltv", "allradio" ], "required": true }
1010 | ]
1011 | },
1012 | "PVR.Fields.Broadcast": {
1013 | "extends": "Item.Fields.Base",
1014 | "items": { "type": "string",
1015 | "enum": [ "title", "plot", "plotoutline", "starttime",
1016 | "endtime", "runtime", "progress", "progresspercentage",
1017 | "genre", "episodename", "episodenum", "episodepart",
1018 | "firstaired", "hastimer", "isactive", "parentalrating",
1019 | "wasactive", "thumbnail", "rating", "originaltitle", "cast",
1020 | "director", "writer", "year", "imdbnumber", "hastimerrule",
1021 | "hasrecording", "recording", "isseries", "isplayable", "clientid",
1022 | "hasreminder" ]
1023 | }
1024 | },
1025 | "PVR.Details.Broadcast": {
1026 | "extends": "Item.Details.Base",
1027 | "properties": {
1028 | "broadcastid": { "$ref": "Library.Id", "required": true },
1029 | "title": { "type": "string" },
1030 | "plot": { "type": "string" },
1031 | "plotoutline": { "type": "string" },
1032 | "starttime": { "type": "string" },
1033 | "endtime": { "type": "string" },
1034 | "runtime": { "type": "integer" },
1035 | "progress": { "type": "integer" },
1036 | "progresspercentage": { "type": "number" },
1037 | "genre": { "type": "string" },
1038 | "episodename": { "type": "string" },
1039 | "episodenum": { "type": "integer" },
1040 | "episodepart": { "type": "integer" },
1041 | "firstaired": { "type": "string" },
1042 | "hastimer": { "type": "boolean" },
1043 | "isactive": { "type": "boolean" },
1044 | "parentalrating": { "type": "integer" },
1045 | "wasactive": { "type": "boolean" },
1046 | "thumbnail": { "type": "string" },
1047 | "rating": { "type": "integer" },
1048 | "originaltitle": { "type": "string" },
1049 | "cast": { "type": "string" },
1050 | "director": { "type": "string" },
1051 | "writer": { "type": "string" },
1052 | "year": { "type": "integer" },
1053 | "imdbnumber": { "type": "integer" },
1054 | "hastimerrule": { "type": "boolean" },
1055 | "hasrecording": { "type": "boolean" },
1056 | "recording": { "type": "string" },
1057 | "isseries": { "type": "boolean" },
1058 | "isplayable": { "type": "boolean", "description": "Deprecated - Use GetBroadcastIsPlayable instead" },
1059 | "clientid": { "$ref": "Library.Id" },
1060 | "hasreminder": { "type": "boolean" }
1061 | }
1062 | },
1063 | "PVR.Fields.Channel": {
1064 | "extends": "Item.Fields.Base",
1065 | "items": { "type": "string",
1066 | "enum": [ "thumbnail", "channeltype", "hidden", "locked", "channel", "lastplayed",
1067 | "broadcastnow", "broadcastnext", "uniqueid", "icon", "channelnumber",
1068 | "subchannelnumber", "isrecording", "hasarchive", "clientid" ]
1069 | }
1070 | },
1071 | "PVR.Details.Channel": {
1072 | "extends": "Item.Details.Base",
1073 | "properties": {
1074 | "channelid": { "$ref": "Library.Id", "required": true },
1075 | "channel": { "type": "string" },
1076 | "channeltype": { "$ref": "PVR.Channel.Type" },
1077 | "hidden": { "type": "boolean" },
1078 | "locked": { "type": "boolean" },
1079 | "thumbnail": { "type": "string" },
1080 | "lastplayed": { "type": "string" },
1081 | "broadcastnow": { "$ref": "PVR.Details.Broadcast" },
1082 | "broadcastnext": { "$ref": "PVR.Details.Broadcast" },
1083 | "uniqueid": { "type": "integer", "required": true },
1084 | "icon": { "type": "string" },
1085 | "channelnumber": { "type": "integer" },
1086 | "subchannelnumber": { "type": "integer" },
1087 | "isrecording": { "type": "boolean" },
1088 | "hasarchive": { "type": "boolean" },
1089 | "clientid": { "$ref": "Library.Id" }
1090 | }
1091 | },
1092 | "PVR.Details.ChannelGroup": {
1093 | "extends": "Item.Details.Base",
1094 | "properties": {
1095 | "channelgroupid": { "$ref": "Library.Id", "required": true },
1096 | "channeltype": { "$ref": "PVR.Channel.Type", "required": true }
1097 | }
1098 | },
1099 | "PVR.Details.ChannelGroup.Extended": {
1100 | "extends": "PVR.Details.ChannelGroup",
1101 | "properties": {
1102 | "limits": { "$ref": "List.LimitsReturned", "required": true },
1103 | "channels": { "type": "array",
1104 | "items": { "$ref": "PVR.Details.Channel" }
1105 | }
1106 | }
1107 | },
1108 | "PVR.Fields.Client": {
1109 | "extends": "Item.Fields.Base",
1110 | "items": { "type": "string",
1111 | "enum": [ "addonid", "supportstv", "supportsradio", "supportsepg",
1112 | "supportsrecordings", "supportstimers", "supportschannelgroups",
1113 | "supportschannelscan" ]
1114 | }
1115 | },
1116 | "PVR.Details.Client": {
1117 | "extends": "Item.Details.Base",
1118 | "properties": {
1119 | "clientid": { "$ref": "Library.Id", "required": true },
1120 | "addonid": { "type": "string" },
1121 | "supportstv": { "type": "boolean" },
1122 | "supportsradio": { "type": "boolean" },
1123 | "supportsepg": { "type": "boolean" },
1124 | "supportsrecordings": { "type": "boolean" },
1125 | "supportstimers": { "type": "boolean" },
1126 | "supportschannelgroups": { "type": "boolean" },
1127 | "supportschannelscan": { "type": "boolean" }
1128 | }
1129 | },
1130 | "PVR.TimerState": {
1131 | "type": "string",
1132 | "enum": [ "unknown", "new", "scheduled", "recording", "completed",
1133 | "aborted", "cancelled", "conflict_ok", "conflict_notok",
1134 | "error", "disabled" ]
1135 | },
1136 | "PVR.Fields.Timer": {
1137 | "extends": "Item.Fields.Base",
1138 | "items": { "type": "string",
1139 | "enum": [ "title", "summary", "channelid", "isradio", "istimerrule", "ismanual",
1140 | "starttime", "endtime", "runtime", "lifetime", "firstday",
1141 | "weekdays", "priority", "startmargin", "endmargin", "state",
1142 | "file", "directory", "preventduplicateepisodes", "startanytime",
1143 | "endanytime", "epgsearchstring", "fulltextepgsearch", "recordinggroup",
1144 | "maxrecordings", "epguid", "isreadonly", "isreminder", "clientid", "broadcastid" ]
1145 | }
1146 | },
1147 | "PVR.Details.Timer": {
1148 | "extends": "Item.Details.Base",
1149 | "properties": {
1150 | "timerid": { "$ref": "Library.Id", "required": true },
1151 | "title": { "type": "string" },
1152 | "summary": { "type": "string" },
1153 | "channelid": { "$ref": "Library.Id" },
1154 | "isradio": { "type": "boolean" },
1155 | "istimerrule": { "type": "boolean" },
1156 | "ismanual": { "type": "boolean" },
1157 | "starttime": { "type": "string" },
1158 | "endtime": { "type": "string" },
1159 | "runtime": { "type": "integer" },
1160 | "lifetime": { "type": "integer" },
1161 | "firstday": { "type": "string" },
1162 | "weekdays": { "type": "array",
1163 | "items": { "$ref": "Global.Weekday" },
1164 | "uniqueItems": true
1165 | },
1166 | "priority": { "type": "integer" },
1167 | "startmargin": { "type": "integer" },
1168 | "endmargin": { "type": "integer" },
1169 | "state": { "$ref": "PVR.TimerState" },
1170 | "file": { "type": "string" },
1171 | "directory": { "type": "string" },
1172 | "preventduplicateepisodes": { "type": "integer" },
1173 | "startanytime": { "type": "boolean" },
1174 | "endanytime": { "type": "boolean" },
1175 | "epgsearchstring": { "type": "string" },
1176 | "fulltextepgsearch": { "type": "boolean" },
1177 | "recordinggroup": { "type": "integer" },
1178 | "maxrecordings": { "type": "integer" },
1179 | "epguid": { "type": "integer" },
1180 | "isreadonly": { "type": "boolean" },
1181 | "isreminder": { "type": "boolean" },
1182 | "clientid": { "$ref": "Library.Id" },
1183 | "broadcastid": { "$ref": "Library.Id" }
1184 | }
1185 | },
1186 | "PVR.Fields.Recording": {
1187 | "extends": "Item.Fields.Base",
1188 | "items": { "type": "string",
1189 | "enum": [ "title", "plot", "plotoutline", "genre", "playcount",
1190 | "resume", "channel", "starttime","endtime", "runtime",
1191 | "lifetime", "icon", "art", "streamurl", "file",
1192 | "directory", "radio", "isdeleted", "epgeventid", "channeluid",
1193 | "season", "episode", "showtitle", "clientid" ]
1194 | }
1195 | },
1196 | "PVR.Details.Recording": {
1197 | "extends": "Item.Details.Base",
1198 | "properties": {
1199 | "recordingid": { "$ref": "Library.Id", "required": true },
1200 | "title": { "type": "string" },
1201 | "plot": { "type": "string" },
1202 | "plotoutline": { "type": "string" },
1203 | "genre": { "type": "string" },
1204 | "playcount": { "type": "integer" },
1205 | "resume": { "$ref": "Video.Resume" },
1206 | "channel": { "type": "string" },
1207 | "starttime": { "type": "string" },
1208 | "endtime": { "type": "string" },
1209 | "runtime": { "type": "integer" },
1210 | "lifetime": { "type": "integer" },
1211 | "icon": { "type": "string" },
1212 | "art": { "$ref": "Media.Artwork" },
1213 | "streamurl": { "type": "string" },
1214 | "file": { "type": "string" },
1215 | "directory": { "type": "string" },
1216 | "radio": { "type": "boolean" },
1217 | "isdeleted": { "type": "boolean" },
1218 | "epgeventid": { "type": "integer" },
1219 | "channeluid": { "type": "integer" },
1220 | "season": { "type": "integer" },
1221 | "episode": { "type": "integer" },
1222 | "showtitle": { "type": "string" },
1223 | "clientid": { "$ref": "Library.Id" }
1224 | }
1225 | },
1226 | "Textures.Details.Size": {
1227 | "type": "object",
1228 | "properties": {
1229 | "size": { "type": "integer", "description": "Size of the texture (1 == largest)" },
1230 | "width": { "type": "integer", "description": "Width of texture" },
1231 | "height": { "type": "integer", "description": "Height of texture" },
1232 | "usecount": { "type": "integer", "description": "Number of uses" },
1233 | "lastused": { "type": "string", "description": "Date of last use" }
1234 | }
1235 | },
1236 | "Textures.Fields.Texture": {
1237 | "extends": "Item.Fields.Base",
1238 | "items": { "type": "string",
1239 | "enum": [ "url", "cachedurl", "lasthashcheck", "imagehash", "sizes" ]
1240 | }
1241 | },
1242 | "Textures.Details.Texture": {
1243 | "type": "object",
1244 | "properties": {
1245 | "textureid": { "$ref": "Library.Id", "required": "true" },
1246 | "url": { "type": "string", "description": "Original source URL" },
1247 | "cachedurl": { "type": "string", "description": "Cached URL on disk" },
1248 | "lasthashcheck": { "type": "string", "description": "Last time source was checked for changes" },
1249 | "imagehash": { "type": "string", "description": "Hash of image" },
1250 | "sizes": { "type": "array", "items": { "$ref": "Textures.Details.Size" } }
1251 | }
1252 | },
1253 | "Profiles.Password": {
1254 | "type": "object",
1255 | "properties": {
1256 | "value": { "type": "string", "required": true, "description": "Password" },
1257 | "encryption": { "type": "string", "description": "Password Encryption", "default": "md5", "enum": [ "none", "md5" ] }
1258 | }
1259 | },
1260 | "Profiles.Fields.Profile": {
1261 | "extends": "Item.Fields.Base",
1262 | "items": { "type": "string", "enum": [ "thumbnail", "lockmode" ] }
1263 | },
1264 | "Profiles.Details.Profile": {
1265 | "extends": "Item.Details.Base",
1266 | "properties": {
1267 | "thumbnail": { "type": "string" },
1268 | "lockmode": { "type": "integer" }
1269 | }
1270 | },
1271 | "List.Filter.Rule": {
1272 | "type": "object",
1273 | "properties": {
1274 | "operator": { "$ref": "List.Filter.Operators", "required": true },
1275 | "value": {
1276 | "type": [
1277 | { "type": "string", "required": true },
1278 | { "type": "array", "items": { "type": "string" }, "required": true }
1279 | ], "required": true
1280 | }
1281 | }
1282 | },
1283 | "List.Filter.Rule.Movies": {
1284 | "extends": "List.Filter.Rule",
1285 | "properties": {
1286 | "field": { "$ref": "List.Filter.Fields.Movies", "required": true }
1287 | }
1288 | },
1289 | "List.Filter.Rule.TVShows": {
1290 | "extends": "List.Filter.Rule",
1291 | "properties": {
1292 | "field": { "$ref": "List.Filter.Fields.TVShows", "required": true }
1293 | }
1294 | },
1295 | "List.Filter.Rule.Episodes": {
1296 | "extends": "List.Filter.Rule",
1297 | "properties": {
1298 | "field": { "$ref": "List.Filter.Fields.Episodes", "required": true }
1299 | }
1300 | },
1301 | "List.Filter.Rule.MusicVideos": {
1302 | "extends": "List.Filter.Rule",
1303 | "properties": {
1304 | "field": { "$ref": "List.Filter.Fields.MusicVideos", "required": true }
1305 | }
1306 | },
1307 | "List.Filter.Rule.Artists": {
1308 | "extends": "List.Filter.Rule",
1309 | "properties": {
1310 | "field": { "$ref": "List.Filter.Fields.Artists", "required": true }
1311 | }
1312 | },
1313 | "List.Filter.Rule.Albums": {
1314 | "extends": "List.Filter.Rule",
1315 | "properties": {
1316 | "field": { "$ref": "List.Filter.Fields.Albums", "required": true }
1317 | }
1318 | },
1319 | "List.Filter.Rule.Songs": {
1320 | "extends": "List.Filter.Rule",
1321 | "properties": {
1322 | "field": { "$ref": "List.Filter.Fields.Songs", "required": true }
1323 | }
1324 | },
1325 | "List.Filter.Rule.Textures": {
1326 | "extends": "List.Filter.Rule",
1327 | "properties": {
1328 | "field": { "$ref": "List.Filter.Fields.Textures", "required": true }
1329 | }
1330 | },
1331 | "List.Filter.Movies": {
1332 | "type": [
1333 | { "type": "object",
1334 | "properties": {
1335 | "and": { "type": "array",
1336 | "items": { "$ref": "List.Filter.Movies" },
1337 | "minItems": 1, "required": true
1338 | }
1339 | }
1340 | },
1341 | { "type": "object",
1342 | "properties": {
1343 | "or": { "type": "array",
1344 | "items": { "$ref": "List.Filter.Movies" },
1345 | "minItems": 1, "required": true
1346 | }
1347 | }
1348 | },
1349 | { "$ref": "List.Filter.Rule.Movies" }
1350 | ]
1351 | },
1352 | "List.Filter.TVShows": {
1353 | "type": [
1354 | { "type": "object",
1355 | "properties": {
1356 | "and": { "type": "array",
1357 | "items": { "$ref": "List.Filter.TVShows" },
1358 | "minItems": 1, "required": true
1359 | }
1360 | }
1361 | },
1362 | { "type": "object",
1363 | "properties": {
1364 | "or": { "type": "array",
1365 | "items": { "$ref": "List.Filter.TVShows" },
1366 | "minItems": 1, "required": true
1367 | }
1368 | }
1369 | },
1370 | { "$ref": "List.Filter.Rule.TVShows" }
1371 | ]
1372 | },
1373 | "List.Filter.Episodes": {
1374 | "type": [
1375 | { "type": "object",
1376 | "properties": {
1377 | "and": { "type": "array",
1378 | "items": { "$ref": "List.Filter.Episodes" },
1379 | "minItems": 1, "required": true
1380 | }
1381 | }
1382 | },
1383 | { "type": "object",
1384 | "properties": {
1385 | "or": { "type": "array",
1386 | "items": { "$ref": "List.Filter.Episodes" },
1387 | "minItems": 1, "required": true
1388 | }
1389 | }
1390 | },
1391 | { "$ref": "List.Filter.Rule.Episodes" }
1392 | ]
1393 | },
1394 | "List.Filter.MusicVideos": {
1395 | "type": [
1396 | { "type": "object",
1397 | "properties": {
1398 | "and": { "type": "array",
1399 | "items": { "$ref": "List.Filter.MusicVideos" },
1400 | "minItems": 1, "required": true
1401 | }
1402 | }
1403 | },
1404 | { "type": "object",
1405 | "properties": {
1406 | "or": { "type": "array",
1407 | "items": { "$ref": "List.Filter.MusicVideos" },
1408 | "minItems": 1, "required": true
1409 | }
1410 | }
1411 | },
1412 | { "$ref": "List.Filter.Rule.MusicVideos" }
1413 | ]
1414 | },
1415 | "List.Filter.Artists": {
1416 | "type": [
1417 | { "type": "object",
1418 | "properties": {
1419 | "and": { "type": "array",
1420 | "items": { "$ref": "List.Filter.Artists" },
1421 | "minItems": 1, "required": true
1422 | }
1423 | }
1424 | },
1425 | { "type": "object",
1426 | "properties": {
1427 | "or": { "type": "array",
1428 | "items": { "$ref": "List.Filter.Artists" },
1429 | "minItems": 1, "required": true
1430 | }
1431 | }
1432 | },
1433 | { "$ref": "List.Filter.Rule.Artists" }
1434 | ]
1435 | },
1436 | "List.Filter.Albums": {
1437 | "type": [
1438 | { "type": "object",
1439 | "properties": {
1440 | "and": { "type": "array",
1441 | "items": { "$ref": "List.Filter.Albums" },
1442 | "minItems": 1, "required": true
1443 | }
1444 | }
1445 | },
1446 | { "type": "object",
1447 | "properties": {
1448 | "or": { "type": "array",
1449 | "items": { "$ref": "List.Filter.Albums" },
1450 | "minItems": 1, "required": true
1451 | }
1452 | }
1453 | },
1454 | { "$ref": "List.Filter.Rule.Albums" }
1455 | ]
1456 | },
1457 | "List.Filter.Songs": {
1458 | "type": [
1459 | { "type": "object",
1460 | "properties": {
1461 | "and": { "type": "array",
1462 | "items": { "$ref": "List.Filter.Songs" },
1463 | "minItems": 1, "required": true
1464 | }
1465 | }
1466 | },
1467 | { "type": "object",
1468 | "properties": {
1469 | "or": { "type": "array",
1470 | "items": { "$ref": "List.Filter.Songs" },
1471 | "minItems": 1, "required": true
1472 | }
1473 | }
1474 | },
1475 | { "$ref": "List.Filter.Rule.Songs" }
1476 | ]
1477 | },
1478 | "List.Filter.Textures": {
1479 | "type": [
1480 | { "type": "object",
1481 | "properties": {
1482 | "and": { "type": "array",
1483 | "items": { "$ref": "List.Filter.Textures" },
1484 | "minItems": 1, "required": true
1485 | }
1486 | }
1487 | },
1488 | { "type": "object",
1489 | "properties": {
1490 | "or": { "type": "array",
1491 | "items": { "$ref": "List.Filter.Textures" },
1492 | "minItems": 1, "required": true
1493 | }
1494 | }
1495 | },
1496 | { "$ref": "List.Filter.Rule.Textures" }
1497 | ]
1498 | },
1499 | "List.Item.Base": {
1500 | "extends": [ "Video.Details.File", "Audio.Details.Media" ],
1501 | "properties": {
1502 | "id": { "$ref": "Library.Id" },
1503 | "type": { "type": "string", "enum": [ "unknown", "movie", "episode", "musicvideo", "song", "picture", "channel" ] },
1504 | "albumartist": { "$ref": "Array.String" },
1505 | "album": { "type": "string" },
1506 | "track": { "type": "integer" },
1507 | "duration": { "type": "integer" },
1508 | "comment": { "type": "string" },
1509 | "lyrics": { "type": "string" },
1510 | "musicbrainztrackid": { "type": "string" },
1511 | "musicbrainzartistid": { "$ref": "Array.String" },
1512 | "trailer": { "type": "string" },
1513 | "tagline": { "type": "string" },
1514 | "plotoutline": { "type": "string" },
1515 | "originaltitle": { "type": "string" },
1516 | "writer": { "$ref": "Array.String" },
1517 | "studio": { "$ref": "Array.String" },
1518 | "mpaa": { "type": "string" },
1519 | "cast": { "$ref": "Video.Cast" },
1520 | "country": { "$ref": "Array.String" },
1521 | "imdbnumber": { "type": "string" },
1522 | "premiered": { "type": "string" },
1523 | "productioncode": { "type": "string" },
1524 | "set": { "type": "string" },
1525 | "showlink": { "$ref": "Array.String" },
1526 | "top250": { "type": "integer" },
1527 | "votes": { "type": "string" },
1528 | "firstaired": { "type": "string" },
1529 | "season": { "type": "integer" },
1530 | "episode": { "type": "integer" },
1531 | "showtitle": { "type": "string" },
1532 | "albumid": { "$ref": "Library.Id" },
1533 | "setid": { "$ref": "Library.Id" },
1534 | "tvshowid": { "$ref": "Library.Id" },
1535 | "watchedepisodes": { "type": "integer" },
1536 | "disc": { "type": "integer" },
1537 | "tag": { "$ref": "Array.String" },
1538 | "albumartistid": { "$ref": "Array.Integer" },
1539 | "uniqueid": { "$ref": "Media.UniqueID" },
1540 | "episodeguide": { "type": "string" },
1541 | "sorttitle": { "type": "string" },
1542 | "description": { "type": "string" },
1543 | "theme": { "$ref": "Array.String" },
1544 | "mood": { "$ref": "Array.String" },
1545 | "style": { "$ref": "Array.String" },
1546 | "albumlabel": { "type": "string" },
1547 | "specialsortseason": { "type": "integer" },
1548 | "specialsortepisode": { "type": "integer" },
1549 | "compilation": { "type": "boolean" },
1550 | "releasetype": { "$ref": "Audio.Album.ReleaseType" },
1551 | "albumreleasetype": { "$ref": "Audio.Album.ReleaseType" },
1552 | "contributors": { "$ref": "Audio.Contributors" },
1553 | "displaycomposer": { "type": "string"},
1554 | "displayconductor": { "type": "string"},
1555 | "displayorchestra": { "type": "string"},
1556 | "displaylyricist": { "type": "string"},
1557 | "mediapath": { "type": "string", "description": "Media source path that identifies the item"},
1558 | "dynpath": { "type": "string", "description": "An experimental property for debug purposes, often same as mediapath but when different gives the actual file playing that should also be in file property"},
1559 | "isboxset": { "type": "boolean" },
1560 | "totaldiscs": { "type": "integer" },
1561 | "disctitle": { "type": "string" },
1562 | "releasedate": { "type": "string" },
1563 | "originaldate": { "type": "string" },
1564 | "bpm": { "type": "integer" },
1565 | "bitrate": { "type": "integer" },
1566 | "samplerate": { "type": "integer" },
1567 | "channels": { "type": "integer"},
1568 | "albumstatus": { "type": "string" },
1569 | "customproperties": { "$ref": "Item.CustomProperties" }
1570 | }
1571 | },
1572 | "List.Fields.All": {
1573 | "extends": "Item.Fields.Base",
1574 | "items": { "type": "string",
1575 | "enum": [ "title", "artist", "albumartist", "genre", "year", "rating",
1576 | "album", "track", "duration", "comment", "lyrics", "musicbrainztrackid",
1577 | "musicbrainzartistid", "musicbrainzalbumid", "musicbrainzalbumartistid",
1578 | "playcount", "fanart", "director", "trailer", "tagline", "plot",
1579 | "plotoutline", "originaltitle", "lastplayed", "writer", "studio",
1580 | "mpaa", "cast", "country", "imdbnumber", "premiered", "productioncode",
1581 | "runtime", "set", "showlink", "streamdetails", "top250", "votes",
1582 | "firstaired", "season", "episode", "showtitle", "thumbnail", "file",
1583 | "resume", "artistid", "albumid", "tvshowid", "setid", "watchedepisodes",
1584 | "disc", "tag", "art", "genreid", "displayartist", "albumartistid",
1585 | "description", "theme", "mood", "style", "albumlabel", "sorttitle",
1586 | "episodeguide", "uniqueid", "dateadded", "channel", "channeltype", "hidden",
1587 | "locked", "channelnumber", "subchannelnumber", "starttime", "endtime",
1588 | "specialsortseason", "specialsortepisode", "compilation", "releasetype",
1589 | "albumreleasetype", "contributors", "displaycomposer", "displayconductor",
1590 | "displayorchestra", "displaylyricist", "userrating", "votes", "sortartist",
1591 | "musicbrainzreleasegroupid", "mediapath", "dynpath", "isboxset", "totaldiscs",
1592 | "disctitle", "releasedate", "originaldate", "bpm", "bitrate", "samplerate",
1593 | "channels", "albumstatus", "datemodified", "datenew", "customproperties",
1594 | "albumduration"]
1595 | }
1596 | },
1597 | "List.Item.All": {
1598 | "extends": "List.Item.Base",
1599 | "properties": {
1600 | "channel": { "type": "string" },
1601 | "channeltype": { "$ref": "PVR.Channel.Type" },
1602 | "hidden": { "type": "boolean" },
1603 | "locked": { "type": "boolean" },
1604 | "channelnumber": { "type": "integer" },
1605 | "subchannelnumber": { "type": "integer" },
1606 | "starttime": { "type": "string" },
1607 | "endtime": { "type": "string" }
1608 | }
1609 | },
1610 | "List.Fields.Files": {
1611 | "extends": "Item.Fields.Base",
1612 | "items": { "type": "string",
1613 | "enum": [ "title", "artist", "albumartist", "genre", "year", "rating",
1614 | "album", "track", "duration", "comment", "lyrics", "musicbrainztrackid",
1615 | "musicbrainzartistid", "musicbrainzalbumid", "musicbrainzalbumartistid",
1616 | "playcount", "fanart", "director", "trailer", "tagline", "plot",
1617 | "plotoutline", "originaltitle", "lastplayed", "writer", "studio",
1618 | "mpaa", "cast", "country", "imdbnumber", "premiered", "productioncode",
1619 | "runtime", "set", "showlink", "streamdetails", "top250", "votes",
1620 | "firstaired", "season", "episode", "showtitle", "thumbnail", "file",
1621 | "resume", "artistid", "albumid", "tvshowid", "setid", "watchedepisodes",
1622 | "disc", "tag", "art", "genreid", "displayartist", "albumartistid",
1623 | "description", "theme", "mood", "style", "albumlabel", "sorttitle",
1624 | "episodeguide", "uniqueid", "dateadded", "size", "lastmodified", "mimetype",
1625 | "specialsortseason", "specialsortepisode", "sortartist", "musicbrainzreleasegroupid",
1626 | "isboxset", "totaldiscs", "disctitle", "releasedate", "originaldate", "bpm",
1627 | "bitrate", "samplerate", "channels", "datemodified", "datenew", "customproperties",
1628 | "albumduration"]
1629 | }
1630 | },
1631 | "List.Item.File": {
1632 | "extends": "List.Item.Base",
1633 | "properties": {
1634 | "file": { "type": "string", "required": true },
1635 | "filetype": { "type": "string", "enum": [ "file", "directory" ], "required": true },
1636 | "size": { "type": "integer", "description": "Size of the file in bytes" },
1637 | "lastmodified": { "type": "string" },
1638 | "mimetype": { "type": "string" }
1639 | }
1640 | },
1641 | "List.Items.Sources": {
1642 | "type": "array",
1643 | "items": {
1644 | "extends": "Item.Details.Base",
1645 | "properties": {
1646 | "file": { "type": "string", "required": true }
1647 | }
1648 | }
1649 | },
1650 | "Addon.Content": {
1651 | "type": "string",
1652 | "enum": [ "unknown", "video", "audio", "image", "executable" ],
1653 | "default": "unknown"
1654 | },
1655 | "Addon.Fields": {
1656 | "extends": "Item.Fields.Base",
1657 | "items": { "type": "string",
1658 | "enum": [ "name", "version", "summary", "description", "path", "author", "thumbnail", "disclaimer", "fanart",
1659 | "dependencies", "broken", "extrainfo", "rating", "enabled", "installed", "deprecated" ]
1660 | }
1661 | },
1662 | "Addon.Details": {
1663 | "extends": "Item.Details.Base",
1664 | "properties": {
1665 | "addonid": { "type": "string", "required": true },
1666 | "type": { "$ref": "Addon.Types", "required": true },
1667 | "name": { "type": "string" },
1668 | "version": { "type": "string" },
1669 | "summary": { "type": "string" },
1670 | "description": { "type": "string" },
1671 | "path": { "type": "string" },
1672 | "author": { "type": "string" },
1673 | "thumbnail": { "type": "string" },
1674 | "disclaimer": { "type": "string" },
1675 | "fanart": { "type": "string" },
1676 | "dependencies": { "type": "array",
1677 | "items": { "type": "object",
1678 | "properties": {
1679 | "addonid": { "type": "string", "required": true },
1680 | "version": { "type": "string", "required": true },
1681 | "optional": { "type": "boolean", "required": true }
1682 | }
1683 | }
1684 | },
1685 | "broken": { "type": [ "boolean", "string" ] },
1686 | "extrainfo": { "type": "array",
1687 | "items": { "type": "object",
1688 | "properties": {
1689 | "key": { "type": "string", "required": true },
1690 | "value": { "type": "string", "required": true }
1691 | }
1692 | }
1693 | },
1694 | "rating": { "type": "integer" },
1695 | "enabled": { "type": "boolean" },
1696 | "installed": { "type": "boolean" },
1697 | "deprecated": { "type": [ "boolean", "string" ] }
1698 | }
1699 | },
1700 | "GUI.Stereoscopy.Mode": {
1701 | "type": "object",
1702 | "properties": {
1703 | "mode": { "type": "string", "required": true, "enum": [ "off", "split_vertical", "split_horizontal", "row_interleaved", "hardware_based", "anaglyph_cyan_red", "anaglyph_green_magenta", "anaglyph_yellow_blue", "monoscopic" ] },
1704 | "label": { "type": "string", "required": true }
1705 | }
1706 | },
1707 | "GUI.Property.Name": {
1708 | "type": "string",
1709 | "enum": [ "currentwindow", "currentcontrol", "skin", "fullscreen", "stereoscopicmode" ]
1710 | },
1711 | "GUI.Property.Value": {
1712 | "type": "object",
1713 | "properties": {
1714 | "currentwindow": { "type": "object",
1715 | "properties": {
1716 | "id": { "type": "integer", "required": true },
1717 | "label": { "type": "string", "required": true }
1718 | }
1719 | },
1720 | "currentcontrol": { "type": "object",
1721 | "properties": {
1722 | "label": { "type": "string", "required": true }
1723 | }
1724 | },
1725 | "skin": { "type": "object",
1726 | "properties": {
1727 | "id": { "type": "string", "required": true, "minLength": 1 },
1728 | "name": { "type": "string" }
1729 | }
1730 | },
1731 | "fullscreen": { "type": "boolean" },
1732 | "stereoscopicmode": { "$ref": "GUI.Stereoscopy.Mode" }
1733 | }
1734 | },
1735 | "System.Property.Name": {
1736 | "type": "string",
1737 | "enum": [ "canshutdown", "cansuspend", "canhibernate", "canreboot" ]
1738 | },
1739 | "System.Property.Value": {
1740 | "type": "object",
1741 | "properties": {
1742 | "canshutdown": { "type": "boolean" },
1743 | "cansuspend": { "type": "boolean" },
1744 | "canhibernate": { "type": "boolean" },
1745 | "canreboot": { "type": "boolean" }
1746 | }
1747 | },
1748 | "Application.Property.Name": {
1749 | "type": "string",
1750 | "enum": [ "volume", "muted", "name", "version", "volume", "sorttokens", "language" ]
1751 | },
1752 | "Application.Property.Value": {
1753 | "type": "object",
1754 | "properties": {
1755 | "volume": { "type": "integer", "minimum": 0, "maximum": 100 },
1756 | "muted": { "type": "boolean" },
1757 | "name": { "type": "string", "minLength": 1 },
1758 | "version": { "type": "object",
1759 | "properties": {
1760 | "major": { "type": "integer", "minimum": 0, "required": true },
1761 | "minor": { "type": "integer", "minimum": 0, "required": true },
1762 | "revision": { "type": [ "string", "integer" ] },
1763 | "tag": { "type": "string", "enum": [ "prealpha", "alpha", "beta", "releasecandidate", "stable" ], "required": true },
1764 | "tagversion": { "type": "string" }
1765 | }
1766 | },
1767 | "sorttokens": { "$ref": "Array.String", "description": "Articles ignored during sorting when ignorearticle is enabled." },
1768 | "language": { "type": "string", "minLength": 1, "description": "Current language code and region e.g. en_GB" }
1769 | }
1770 | },
1771 | "Favourite.Fields.Favourite": {
1772 | "extends": "Item.Fields.Base",
1773 | "items": { "type": "string",
1774 | "enum": [ "window", "windowparameter", "thumbnail", "path" ]
1775 | }
1776 | },
1777 | "Favourite.Type": {
1778 | "type": "string",
1779 | "enum": [ "media", "window", "script", "androidapp", "unknown" ]
1780 | },
1781 | "Favourite.Details.Favourite": {
1782 | "type": "object",
1783 | "properties": {
1784 | "title": { "type": "string", "required": true },
1785 | "type": { "$ref": "Favourite.Type", "required": true },
1786 | "path": { "type": "string" },
1787 | "window": { "type": "string" },
1788 | "windowparameter": { "type": "string" },
1789 | "thumbnail": { "type": "string" }
1790 | },
1791 | "additionalProperties": false
1792 | },
1793 | "Setting.Type": {
1794 | "type": "string",
1795 | "enum": [
1796 | "boolean", "integer", "number", "string", "action", "list",
1797 | "path", "addon", "date", "time"
1798 | ]
1799 | },
1800 | "Setting.Level": {
1801 | "type": "string",
1802 | "enum": [ "basic", "standard", "advanced", "expert" ]
1803 | },
1804 | "Setting.Value": {
1805 | "type": [
1806 | { "type": "boolean", "required": true },
1807 | { "type": "integer", "required": true },
1808 | { "type": "number", "required": true },
1809 | { "type": "string", "required": true }
1810 | ]
1811 | },
1812 | "Setting.Value.List": {
1813 | "type": "array",
1814 | "items": { "$ref": "Setting.Value" }
1815 | },
1816 | "Setting.Value.Extended": {
1817 | "type": [
1818 | { "type": "boolean", "required": true },
1819 | { "type": "integer", "required": true },
1820 | { "type": "number", "required": true },
1821 | { "type": "string", "required": true },
1822 | { "$ref": "Setting.Value.List", "required": true }
1823 | ]
1824 | },
1825 | "Setting.Details.ControlBase": {
1826 | "type": "object",
1827 | "properties": {
1828 | "type": { "type": "string", "required": true },
1829 | "format": { "type": "string", "required": true },
1830 | "delayed": { "type": "boolean", "required": true }
1831 | }
1832 | },
1833 | "Setting.Details.ControlCheckmark": {
1834 | "extends": "Setting.Details.ControlBase",
1835 | "properties": {
1836 | "type": { "type": "string", "required": true, "enum": [ "toggle" ] },
1837 | "format": { "type": "string", "required": true, "enum": [ "boolean" ] }
1838 | }
1839 | },
1840 | "Setting.Details.ControlSpinner": {
1841 | "extends": "Setting.Details.ControlBase",
1842 | "properties": {
1843 | "type": { "type": "string", "required": true, "enum": [ "spinner" ] },
1844 | "formatlabel": { "type": "string" },
1845 | "minimumlabel": { "type": "string" }
1846 | }
1847 | },
1848 | "Setting.Details.ControlHeading": {
1849 | "extends": "Setting.Details.ControlBase",
1850 | "properties": {
1851 | "heading": { "type": "string" }
1852 | }
1853 | },
1854 | "Setting.Details.ControlEdit": {
1855 | "extends": "Setting.Details.ControlHeading",
1856 | "properties": {
1857 | "type": { "type": "string", "required": true, "enum": [ "edit" ] },
1858 | "hidden": { "type": "boolean", "required": true },
1859 | "verifynewvalue": { "type": "boolean", "required": true }
1860 | }
1861 | },
1862 | "Setting.Details.ControlButton": {
1863 | "extends": "Setting.Details.ControlHeading",
1864 | "properties": {
1865 | "type": { "type": "string", "required": true, "enum": [ "button" ] }
1866 | }
1867 | },
1868 | "Setting.Details.ControlList": {
1869 | "extends": "Setting.Details.ControlHeading",
1870 | "properties": {
1871 | "type": { "type": "string", "required": true, "enum": [ "list" ] },
1872 | "multiselect": { "type": "boolean", "required": true }
1873 | }
1874 | },
1875 | "Setting.Details.ControlSlider": {
1876 | "extends": "Setting.Details.ControlHeading",
1877 | "properties": {
1878 | "type": { "type": "string", "required": true, "enum": [ "slider" ] },
1879 | "formatlabel": { "type": "string", "required": true },
1880 | "popup": { "type": "boolean", "required": true }
1881 | }
1882 | },
1883 | "Setting.Details.ControlRange": {
1884 | "extends": "Setting.Details.ControlBase",
1885 | "properties": {
1886 | "type": { "type": "string", "required": true, "enum": [ "range" ] },
1887 | "formatlabel": { "type": "string", "required": true },
1888 | "formatvalue": { "type": "string", "required": true }
1889 | }
1890 | },
1891 | "Setting.Details.ControlLabel": {
1892 | "extends": "Setting.Details.ControlBase",
1893 | "properties": {
1894 | "type": { "type": "string", "required": true, "enum": [ "label" ] },
1895 | "format": { "type": "string", "required": true, "enum": [ "string" ] }
1896 | }
1897 | },
1898 | "Setting.Details.Control": {
1899 | "type": [
1900 | { "$ref": "Setting.Details.ControlCheckmark", "required": true },
1901 | { "$ref": "Setting.Details.ControlSpinner", "required": true },
1902 | { "$ref": "Setting.Details.ControlEdit", "required": true },
1903 | { "$ref": "Setting.Details.ControlButton", "required": true },
1904 | { "$ref": "Setting.Details.ControlList", "required": true },
1905 | { "$ref": "Setting.Details.ControlSlider", "required": true },
1906 | { "$ref": "Setting.Details.ControlRange", "required": true },
1907 | { "$ref": "Setting.Details.ControlLabel", "required": true }
1908 | ]
1909 | },
1910 | "Setting.Details.Base": {
1911 | "type": "object",
1912 | "properties": {
1913 | "id": { "type": "string", "required": true, "minLength": 1 },
1914 | "label": { "type": "string", "required": true },
1915 | "help": { "type": "string" }
1916 | }
1917 | },
1918 | "Setting.Details.SettingBase": {
1919 | "extends": "Setting.Details.Base",
1920 | "properties": {
1921 | "type": { "$ref": "Setting.Type", "required": true },
1922 | "enabled": { "type": "boolean", "required": true },
1923 | "level": { "$ref": "Setting.Level", "required": true },
1924 | "parent": { "type": "string" },
1925 | "control": { "$ref": "Setting.Details.Control" }
1926 | },
1927 | "additionalProperties": false
1928 | },
1929 | "Setting.Details.SettingBool": {
1930 | "extends": "Setting.Details.SettingBase",
1931 | "properties": {
1932 | "value": { "type": "boolean", "required": true },
1933 | "default": { "type": "boolean", "required": true }
1934 | },
1935 | "additionalProperties": false
1936 | },
1937 | "Setting.Details.SettingInt": {
1938 | "extends": "Setting.Details.SettingBase",
1939 | "properties": {
1940 | "value": { "type": "integer", "required": true },
1941 | "default": { "type": "integer", "required": true },
1942 | "minimum": { "type": "integer" },
1943 | "step": { "type": "integer" },
1944 | "maximum": { "type": "integer" },
1945 | "options": { "type": "array",
1946 | "items": { "type": "object",
1947 | "properties": {
1948 | "label": { "type": "string", "required": true },
1949 | "value": { "type": "integer", "required": true }
1950 | }
1951 | }
1952 | }
1953 | },
1954 | "additionalProperties": false
1955 | },
1956 | "Setting.Details.SettingNumber": {
1957 | "extends": "Setting.Details.SettingBase",
1958 | "properties": {
1959 | "value": { "type": "number", "required": true },
1960 | "default": { "type": "number", "required": true },
1961 | "minimum": { "type": "number", "required": true },
1962 | "step": { "type": "number", "required": true },
1963 | "maximum": { "type": "number", "required": true }
1964 | },
1965 | "additionalProperties": false
1966 | },
1967 | "Setting.Details.SettingString": {
1968 | "extends": "Setting.Details.SettingBase",
1969 | "properties": {
1970 | "value": { "type": "string", "required": true },
1971 | "default": { "type": "string", "required": true },
1972 | "allowempty": { "type": "boolean", "required": true },
1973 | "options": { "type": "array",
1974 | "items": { "type": "object",
1975 | "properties": {
1976 | "label": { "type": "string", "required": true },
1977 | "value": { "type": "string", "required": true }
1978 | }
1979 | }
1980 | }
1981 | }
1982 | },
1983 | "Setting.Details.SettingAction": {
1984 | "extends": "Setting.Details.SettingBase",
1985 | "properties": {
1986 | "data": { "type": "string", "required": true }
1987 | },
1988 | "additionalProperties": false
1989 | },
1990 | "Setting.Details.SettingList": {
1991 | "extends": "Setting.Details.SettingBase",
1992 | "properties": {
1993 | "value": { "$ref": "Setting.Value.List", "required": true },
1994 | "default": { "$ref": "Setting.Value.List", "required": true },
1995 | "elementtype": { "$ref": "Setting.Type", "required": true },
1996 | "definition": { "$ref": "Setting.Details.Setting", "required": true },
1997 | "delimiter": { "type": "string", "required": true },
1998 | "minimumItems": { "type": "integer" },
1999 | "maximumItems": { "type": "integer" }
2000 | },
2001 | "additionalProperties": false
2002 | },
2003 | "Setting.Details.SettingPath": {
2004 | "extends": "Setting.Details.SettingString",
2005 | "properties": {
2006 | "writable": { "type": "boolean", "required": true },
2007 | "sources": { "type": "array", "items": { "type": "string" } }
2008 | },
2009 | "additionalProperties": false
2010 | },
2011 | "Setting.Details.SettingAddon": {
2012 | "extends": "Setting.Details.SettingString",
2013 | "properties": {
2014 | "addontype": { "$ref": "Addon.Types", "required": true }
2015 | },
2016 | "additionalProperties": false
2017 | },
2018 | "Setting.Details.SettingDate": {
2019 | "extends": "Setting.Details.SettingString",
2020 | "additionalProperties": false
2021 | },
2022 | "Setting.Details.SettingTime": {
2023 | "extends": "Setting.Details.SettingString",
2024 | "additionalProperties": false
2025 | },
2026 | "Setting.Details.Setting": {
2027 | "type": [
2028 | { "$ref": "Setting.Details.SettingBool", "required": true },
2029 | { "$ref": "Setting.Details.SettingInt", "required": true },
2030 | { "$ref": "Setting.Details.SettingNumber", "required": true },
2031 | { "$ref": "Setting.Details.SettingString", "required": true },
2032 | { "$ref": "Setting.Details.SettingAction", "required": true },
2033 | { "$ref": "Setting.Details.SettingList", "required": true },
2034 | { "$ref": "Setting.Details.SettingPath", "required": true },
2035 | { "$ref": "Setting.Details.SettingAddon", "required": true },
2036 | { "$ref": "Setting.Details.SettingDate", "required": true },
2037 | { "$ref": "Setting.Details.SettingTime", "required": true }
2038 | ]
2039 | },
2040 | "Setting.Details.Group": {
2041 | "type": "object",
2042 | "properties": {
2043 | "id": { "type": "string", "required": true, "minLength": 1 },
2044 | "settings": {
2045 | "type": "array",
2046 | "items": { "$ref": "Setting.Details.Setting" },
2047 | "minItems": 1,
2048 | "uniqueItems": true
2049 | }
2050 | },
2051 | "additionalProperties": false
2052 | },
2053 | "Setting.Details.Category": {
2054 | "extends": "Setting.Details.Base",
2055 | "properties": {
2056 | "groups": {
2057 | "type": "array",
2058 | "items": { "$ref": "Setting.Details.Group" },
2059 | "minItems": 1,
2060 | "uniqueItems": true
2061 | }
2062 | },
2063 | "additionalProperties": false
2064 | },
2065 | "Setting.Details.Section": {
2066 | "extends": "Setting.Details.Base",
2067 | "properties": {
2068 | "categories": {
2069 | "type": "array",
2070 | "items": { "$ref": "Setting.Details.Category" },
2071 | "minItems": 1,
2072 | "uniqueItems": true
2073 | }
2074 | },
2075 | "additionalProperties": false
2076 | }
2077 | }
2078 |
--------------------------------------------------------------------------------