├── 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 | --------------------------------------------------------------------------------