From 77637622125a187c5b9cbe72b78c8bd3b26f754a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 10 Jun 2024 09:19:47 +0000 Subject: [PATCH] Tool black: auto-format Python code --- build.py | 81 +- jellyfin_kodi/client.py | 76 +- jellyfin_kodi/connect.py | 186 +-- jellyfin_kodi/database/__init__.py | 240 ++-- jellyfin_kodi/database/jellyfin_db.py | 11 +- jellyfin_kodi/database/queries.py | 130 +- jellyfin_kodi/dialogs/context.py | 32 +- jellyfin_kodi/dialogs/loginmanual.py | 20 +- jellyfin_kodi/dialogs/serverconnect.py | 33 +- jellyfin_kodi/dialogs/servermanual.py | 38 +- jellyfin_kodi/dialogs/usersconnect.py | 30 +- jellyfin_kodi/downloader.py | 245 ++-- jellyfin_kodi/entrypoint/context.py | 125 +- jellyfin_kodi/entrypoint/default.py | 1156 +++++++++++------ jellyfin_kodi/entrypoint/service.py | 400 +++--- jellyfin_kodi/full_sync.py | 540 ++++---- jellyfin_kodi/helper/api.py | 293 +++-- jellyfin_kodi/helper/exceptions.py | 7 +- jellyfin_kodi/helper/lazylogger.py | 2 + jellyfin_kodi/helper/loghandler.py | 34 +- jellyfin_kodi/helper/playutils.py | 672 +++++----- jellyfin_kodi/helper/translate.py | 44 +- jellyfin_kodi/helper/utils.py | 256 ++-- jellyfin_kodi/helper/wrapper.py | 23 +- jellyfin_kodi/helper/xmls.py | 45 +- jellyfin_kodi/jellyfin/__init__.py | 20 +- jellyfin_kodi/jellyfin/api.py | 355 ++--- jellyfin_kodi/jellyfin/client.py | 15 +- jellyfin_kodi/jellyfin/configuration.py | 49 +- jellyfin_kodi/jellyfin/connection_manager.py | 167 +-- jellyfin_kodi/jellyfin/credentials.py | 56 +- jellyfin_kodi/jellyfin/http.py | 141 +- jellyfin_kodi/jellyfin/ws_client.py | 76 +- jellyfin_kodi/library.py | 528 +++++--- jellyfin_kodi/monitor.py | 274 ++-- jellyfin_kodi/objects/actions.py | 975 +++++++------- jellyfin_kodi/objects/kodi/artwork.py | 52 +- jellyfin_kodi/objects/kodi/kodi.py | 97 +- jellyfin_kodi/objects/kodi/movies.py | 28 +- jellyfin_kodi/objects/kodi/music.py | 46 +- jellyfin_kodi/objects/kodi/queries.py | 314 ++++- jellyfin_kodi/objects/kodi/queries_music.py | 61 +- jellyfin_kodi/objects/movies.py | 386 +++--- jellyfin_kodi/objects/music.py | 555 ++++---- jellyfin_kodi/objects/musicvideos.py | 243 ++-- jellyfin_kodi/objects/obj.py | 75 +- jellyfin_kodi/objects/tvshows.py | 718 +++++----- jellyfin_kodi/objects/utils.py | 10 +- jellyfin_kodi/player.py | 306 ++--- jellyfin_kodi/views.py | 898 +++++++------ service.py | 19 +- tests/test_clean_none_dict_values.py | 82 +- tests/test_imports.py | 1 + .../jellyfin_kodi/database/jellyfin_db.pyi | 2 - 54 files changed, 6545 insertions(+), 4723 deletions(-) diff --git a/build.py b/build.py index bf449524..148441f3 100755 --- a/build.py +++ b/build.py @@ -35,46 +35,48 @@ def create_addon_xml(config: dict, source: str, py_version: str) -> None: Create addon.xml from template file """ # Load template file - with open('{}/.build/template.xml'.format(source), 'r') as f: + with open("{}/.build/template.xml".format(source), "r") as f: tree = ET.parse(f) root = tree.getroot() # Populate dependencies in template - dependencies = config['dependencies'].get(py_version) + dependencies = config["dependencies"].get(py_version) for dep in dependencies: - ET.SubElement(root.find('requires'), 'import', attrib=dep) + ET.SubElement(root.find("requires"), "import", attrib=dep) # Populate version string - addon_version = config.get('version') - root.attrib['version'] = '{}+{}'.format(addon_version, py_version) + addon_version = config.get("version") + root.attrib["version"] = "{}+{}".format(addon_version, py_version) # Populate Changelog - date = datetime.today().strftime('%Y-%m-%d') - changelog = config.get('changelog') - for section in root.findall('extension'): - news = section.findall('news') + date = datetime.today().strftime("%Y-%m-%d") + changelog = config.get("changelog") + for section in root.findall("extension"): + news = section.findall("news") if news: - news[0].text = 'v{} ({}):\n{}'.format(addon_version, date, changelog) + news[0].text = "v{} ({}):\n{}".format(addon_version, date, changelog) # Format xml tree indent(root) # Write addon.xml - tree.write('{}/addon.xml'.format(source), encoding='utf-8', xml_declaration=True) + tree.write("{}/addon.xml".format(source), encoding="utf-8", xml_declaration=True) def zip_files(py_version: str, source: str, target: str, dev: bool) -> None: """ Create installable addon zip archive """ - archive_name = 'plugin.video.jellyfin+{}.zip'.format(py_version) + archive_name = "plugin.video.jellyfin+{}.zip".format(py_version) - with zipfile.ZipFile('{}/{}'.format(target, archive_name), 'w') as z: + with zipfile.ZipFile("{}/{}".format(target, archive_name), "w") as z: for root, dirs, files in os.walk(args.source): for filename in filter(file_filter, files): file_path = os.path.join(root, filename) if dev or folder_filter(file_path): - relative_path = os.path.join('plugin.video.jellyfin', os.path.relpath(file_path, source)) + relative_path = os.path.join( + "plugin.video.jellyfin", os.path.relpath(file_path, source) + ) z.write(file_path, relative_path) @@ -83,10 +85,12 @@ def file_filter(file_name: str) -> bool: True if file_name is meant to be included """ return ( - not (file_name.startswith('plugin.video.jellyfin') and file_name.endswith('.zip')) - and not file_name.endswith('.pyo') - and not file_name.endswith('.pyc') - and not file_name.endswith('.pyd') + not ( + file_name.startswith("plugin.video.jellyfin") and file_name.endswith(".zip") + ) + and not file_name.endswith(".pyo") + and not file_name.endswith(".pyc") + and not file_name.endswith(".pyd") ) @@ -95,13 +99,13 @@ def folder_filter(folder_name: str) -> bool: True if folder_name is meant to be included """ filters = [ - '.ci', - '.git', - '.github', - '.build', - '.mypy_cache', - '.pytest_cache', - '__pycache__', + ".ci", + ".git", + ".github", + ".build", + ".mypy_cache", + ".pytest_cache", + "__pycache__", ] for f in filters: if f in folder_name.split(os.path.sep): @@ -110,33 +114,22 @@ def folder_filter(folder_name: str) -> bool: return True +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Build flags:") + parser.add_argument("--version", type=str, choices=("py2", "py3"), default="py3") -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Build flags:') - parser.add_argument( - '--version', - type=str, - choices=('py2', 'py3'), - default='py3') + parser.add_argument("--source", type=Path, default=Path(__file__).absolute().parent) - parser.add_argument( - '--source', - type=Path, - default=Path(__file__).absolute().parent) + parser.add_argument("--target", type=Path, default=Path(__file__).absolute().parent) - parser.add_argument( - '--target', - type=Path, - default=Path(__file__).absolute().parent) - - parser.add_argument('--dev', dest='dev', action='store_true') + parser.add_argument("--dev", dest="dev", action="store_true") parser.set_defaults(dev=False) args = parser.parse_args() # Load config file - config_path = os.path.join(args.source, 'release.yaml') - with open(config_path, 'r') as fh: + config_path = os.path.join(args.source, "release.yaml") + with open(config_path, "r") as fh: release_config = yaml.safe_load(fh) create_addon_xml(release_config, args.source, args.version) diff --git a/jellyfin_kodi/client.py b/jellyfin_kodi/client.py index 3aac72cb..e38e52f7 100644 --- a/jellyfin_kodi/client.py +++ b/jellyfin_kodi/client.py @@ -18,73 +18,69 @@ LOG = LazyLogger(__name__) def get_addon_name(): - - ''' Used for logging. - ''' - return xbmcaddon.Addon(addon_id()).getAddonInfo('name').upper() + """Used for logging.""" + return xbmcaddon.Addon(addon_id()).getAddonInfo("name").upper() def get_version(): - return xbmcaddon.Addon(addon_id()).getAddonInfo('version') + return xbmcaddon.Addon(addon_id()).getAddonInfo("version") def get_platform(): - if xbmc.getCondVisibility('system.platform.osx'): + if xbmc.getCondVisibility("system.platform.osx"): return "OSX" - elif xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)'): + elif xbmc.getCondVisibility("System.HasAddon(service.coreelec.settings)"): return "CoreElec" - elif xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'): + elif xbmc.getCondVisibility("System.HasAddon(service.libreelec.settings)"): return "LibreElec" - elif xbmc.getCondVisibility('System.HasAddon(service.osmc.settings)'): + elif xbmc.getCondVisibility("System.HasAddon(service.osmc.settings)"): return "OSMC" - elif xbmc.getCondVisibility('system.platform.atv2'): + elif xbmc.getCondVisibility("system.platform.atv2"): return "ATV2" - elif xbmc.getCondVisibility('system.platform.ios'): + elif xbmc.getCondVisibility("system.platform.ios"): return "iOS" - elif xbmc.getCondVisibility('system.platform.windows'): + elif xbmc.getCondVisibility("system.platform.windows"): return "Windows" - elif xbmc.getCondVisibility('system.platform.android'): + elif xbmc.getCondVisibility("system.platform.android"): return "Linux/Android" - elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): + elif xbmc.getCondVisibility("system.platform.linux.raspberrypi"): return "Linux/RPi" - elif xbmc.getCondVisibility('system.platform.linux'): + elif xbmc.getCondVisibility("system.platform.linux"): return "Linux" else: return "Unknown" def get_device_name(): - - ''' Detect the device name. If deviceNameOpt, then - use the device name in the add-on settings. - Otherwise, fallback to the Kodi device name. - ''' - if not settings('deviceNameOpt.bool'): - device_name = xbmc.getInfoLabel('System.FriendlyName') + """Detect the device name. If deviceNameOpt, then + use the device name in the add-on settings. + Otherwise, fallback to the Kodi device name. + """ + if not settings("deviceNameOpt.bool"): + device_name = xbmc.getInfoLabel("System.FriendlyName") else: - device_name = settings('deviceName') - device_name = device_name.replace("\"", "_") + device_name = settings("deviceName") + device_name = device_name.replace('"', "_") device_name = device_name.replace("/", "_") return device_name def get_device_id(reset=False): + """Return the device_id if already loaded. + It will load from jellyfin_guid file. If it's a fresh + setup, it will generate a new GUID to uniquely + identify the setup for all users. - ''' Return the device_id if already loaded. - It will load from jellyfin_guid file. If it's a fresh - setup, it will generate a new GUID to uniquely - identify the setup for all users. - - window prop: jellyfin_deviceId - ''' - client_id = window('jellyfin_deviceId') + window prop: jellyfin_deviceId + """ + client_id = window("jellyfin_deviceId") if client_id: return client_id - directory = translate_path('special://profile/addon_data/plugin.video.jellyfin/') + directory = translate_path("special://profile/addon_data/plugin.video.jellyfin/") if not xbmcvfs.exists(directory): xbmcvfs.mkdir(directory) @@ -97,27 +93,27 @@ def get_device_id(reset=False): LOG.debug("Generating a new GUID.") client_id = str(create_id()) - file_guid = xbmcvfs.File(jellyfin_guid, 'w') + file_guid = xbmcvfs.File(jellyfin_guid, "w") file_guid.write(client_id) file_guid.close() LOG.debug("DeviceId loaded: %s", client_id) - window('jellyfin_deviceId', value=client_id) + window("jellyfin_deviceId", value=client_id) return client_id def reset_device_id(): - window('jellyfin_deviceId', clear=True) + window("jellyfin_deviceId", clear=True) get_device_id(True) dialog("ok", "{jellyfin}", translate(33033)) - xbmc.executebuiltin('RestartApp') + xbmc.executebuiltin("RestartApp") def get_info(): return { - 'DeviceName': get_device_name(), - 'Version': get_version(), - 'DeviceId': get_device_id() + "DeviceName": get_device_name(), + "Version": get_version(), + "DeviceId": get_device_id(), } diff --git a/jellyfin_kodi/connect.py b/jellyfin_kodi/connect.py index 7566ce90..8a3f2b1d 100644 --- a/jellyfin_kodi/connect.py +++ b/jellyfin_kodi/connect.py @@ -16,7 +16,7 @@ from .helper.exceptions import HTTPException ################################################################################################## LOG = LazyLogger(__name__) -XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo('path'), "default", "1080i") +XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo("path"), "default", "1080i") ################################################################################################## @@ -27,36 +27,37 @@ class Connect(object): self.info = client.get_info() def register(self, server_id=None, options={}): - - ''' Login into server. If server is None, then it will show the proper prompts to login, etc. - If a server id is specified then only a login dialog will be shown for that server. - ''' - LOG.info("--[ server/%s ]", server_id or 'default') + """Login into server. If server is None, then it will show the proper prompts to login, etc. + If a server id is specified then only a login dialog will be shown for that server. + """ + LOG.info("--[ server/%s ]", server_id or "default") credentials = dict(get_credentials()) - servers = credentials['Servers'] + servers = credentials["Servers"] - if server_id is None and credentials['Servers']: - credentials['Servers'] = [credentials['Servers'][0]] + if server_id is None and credentials["Servers"]: + credentials["Servers"] = [credentials["Servers"][0]] - elif credentials['Servers']: + elif credentials["Servers"]: - for server in credentials['Servers']: + for server in credentials["Servers"]: - if server['Id'] == server_id: - credentials['Servers'] = [server] + if server["Id"] == server_id: + credentials["Servers"] = [server] - server_select = server_id is None and not settings('SyncInstallRunDone.bool') - new_credentials = self.register_client(credentials, options, server_id, server_select) + server_select = server_id is None and not settings("SyncInstallRunDone.bool") + new_credentials = self.register_client( + credentials, options, server_id, server_select + ) for server in servers: - if server['Id'] == new_credentials['Servers'][0]['Id']: - server = new_credentials['Servers'][0] + if server["Id"] == new_credentials["Servers"][0]["Id"]: + server = new_credentials["Servers"][0] break else: - servers = new_credentials['Servers'] + servers = new_credentials["Servers"] - credentials['Servers'] = servers + credentials["Servers"] = servers save_credentials(credentials) try: @@ -65,36 +66,39 @@ class Connect(object): LOG.error(error) def get_ssl(self): - - ''' Returns boolean value. - True: verify connection. - ''' - return settings('sslverify.bool') + """Returns boolean value. + True: verify connection. + """ + return settings("sslverify.bool") def get_client(self, server_id=None): - - ''' Get Jellyfin client. - ''' + """Get Jellyfin client.""" client = Jellyfin(server_id) - client.config.app("Kodi", self.info['Version'], self.info['DeviceName'], self.info['DeviceId']) - client.config.data['http.user_agent'] = "Jellyfin-Kodi/%s" % self.info['Version'] - client.config.data['auth.ssl'] = self.get_ssl() + client.config.app( + "Kodi", self.info["Version"], self.info["DeviceName"], self.info["DeviceId"] + ) + client.config.data["http.user_agent"] = ( + "Jellyfin-Kodi/%s" % self.info["Version"] + ) + client.config.data["auth.ssl"] = self.get_ssl() return client - def register_client(self, credentials=None, options=None, server_id=None, server_selection=False): + def register_client( + self, credentials=None, options=None, server_id=None, server_selection=False + ): client = self.get_client(server_id) self.client = client self.connect_manager = client.auth if server_id is None: - client.config.data['app.default'] = True + client.config.data["app.default"] = True try: state = client.authenticate(credentials or {}, options or {}) - if state['State'] == CONNECTION_STATE['SignedIn']: + if state["State"] == CONNECTION_STATE["SignedIn"]: client.callback_ws = event if server_id is None: # Only assign for default server @@ -102,66 +106,77 @@ class Connect(object): client.callback = event self.get_user(client) - settings('serverName', client.config.data['auth.server-name']) - settings('server', client.config.data['auth.server']) + settings("serverName", client.config.data["auth.server-name"]) + settings("server", client.config.data["auth.server"]) - event('ServerOnline', {'ServerId': server_id}) - event('LoadServer', {'ServerId': server_id}) + event("ServerOnline", {"ServerId": server_id}) + event("LoadServer", {"ServerId": server_id}) - return state['Credentials'] + return state["Credentials"] - elif (server_selection or state['State'] == CONNECTION_STATE['ServerSelection'] or state['State'] == CONNECTION_STATE['Unavailable'] and not settings('SyncInstallRunDone.bool')): - state['Credentials']['Servers'] = [self.select_servers(state)] + elif ( + server_selection + or state["State"] == CONNECTION_STATE["ServerSelection"] + or state["State"] == CONNECTION_STATE["Unavailable"] + and not settings("SyncInstallRunDone.bool") + ): + state["Credentials"]["Servers"] = [self.select_servers(state)] - elif state['State'] == CONNECTION_STATE['ServerSignIn']: - if 'ExchangeToken' not in state['Servers'][0]: + elif state["State"] == CONNECTION_STATE["ServerSignIn"]: + if "ExchangeToken" not in state["Servers"][0]: self.login() - elif state['State'] == CONNECTION_STATE['Unavailable'] and state.get('Status_Code', 0) == 401: + elif ( + state["State"] == CONNECTION_STATE["Unavailable"] + and state.get("Status_Code", 0) == 401 + ): # If the saved credentials don't work, restart the addon to force the password dialog to open - window('jellyfin.restart', clear=True) + window("jellyfin.restart", clear=True) - elif state['State'] == CONNECTION_STATE['Unavailable']: - raise HTTPException('ServerUnreachable', {}) + elif state["State"] == CONNECTION_STATE["Unavailable"]: + raise HTTPException("ServerUnreachable", {}) - return self.register_client(state['Credentials'], options, server_id, False) + return self.register_client(state["Credentials"], options, server_id, False) except RuntimeError as error: LOG.exception(error) - xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id()) + xbmc.executebuiltin("Addon.OpenSettings(%s)" % addon_id()) - raise Exception('User sign in interrupted') + raise Exception("User sign in interrupted") except HTTPException as error: - if error.status == 'ServerUnreachable': - event('ServerUnreachable', {'ServerId': server_id}) + if error.status == "ServerUnreachable": + event("ServerUnreachable", {"ServerId": server_id}) return client.get_credentials() def get_user(self, client): - - ''' Save user info. - ''' + """Save user info.""" self.user = client.jellyfin.get_user() - settings('username', self.user['Name']) + settings("username", self.user["Name"]) - if 'PrimaryImageTag' in self.user: - server_address = client.auth.get_server_info(client.auth.server_id)['address'] - window('JellyfinUserImage', api.API(self.user, server_address).get_user_artwork(self.user['Id'])) + if "PrimaryImageTag" in self.user: + server_address = client.auth.get_server_info(client.auth.server_id)[ + "address" + ] + window( + "JellyfinUserImage", + api.API(self.user, server_address).get_user_artwork(self.user["Id"]), + ) def select_servers(self, state=None): - state = state or self.connect_manager.connect({'enableAutoLogin': False}) + state = state or self.connect_manager.connect({"enableAutoLogin": False}) user = {} dialog = ServerConnect("script-jellyfin-connect-server.xml", *XML_PATH) dialog.set_args( connect_manager=self.connect_manager, - username=user.get('DisplayName', ""), - user_image=user.get('ImageUrl'), - servers=self.connect_manager.get_available_servers() + username=user.get("DisplayName", ""), + user_image=user.get("ImageUrl"), + servers=self.connect_manager.get_available_servers(), ) dialog.doModal() @@ -182,9 +197,7 @@ class Connect(object): return self.select_servers() def setup_manual_server(self): - - ''' Setup manual servers - ''' + """Setup manual servers""" client = self.get_client() client.set_credentials(get_credentials()) manager = client.auth @@ -198,11 +211,9 @@ class Connect(object): save_credentials(credentials) def manual_server(self, manager=None): - - ''' Return server or raise error. - ''' + """Return server or raise error.""" dialog = ServerManual("script-jellyfin-connect-server-manual.xml", *XML_PATH) - dialog.set_args(**{'connect_manager': manager or self.connect_manager}) + dialog.set_args(**{"connect_manager": manager or self.connect_manager}) dialog.doModal() if dialog.is_connected(): @@ -213,7 +224,9 @@ class Connect(object): def login(self): users = self.connect_manager.get_public_users() - server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address'] + server = self.connect_manager.get_server_info(self.connect_manager.server_id)[ + "address" + ] if not users: try: @@ -222,14 +235,14 @@ class Connect(object): raise RuntimeError("No user selected") dialog = UsersConnect("script-jellyfin-connect-users.xml", *XML_PATH) - dialog.set_args(**{'server': server, 'users': users}) + dialog.set_args(**{"server": server, "users": users}) dialog.doModal() if dialog.is_user_selected(): user = dialog.get_user() - username = user['Name'] + username = user["Name"] - if user['HasPassword']: + if user["HasPassword"]: LOG.debug("User has password, present manual login") try: return self.login_manual(username) @@ -249,14 +262,12 @@ class Connect(object): return self.login() def setup_login_manual(self): - - ''' Setup manual login by itself for default server. - ''' + """Setup manual login by itself for default server.""" client = self.get_client() client.set_credentials(get_credentials()) manager = client.auth - username = settings('username') + username = settings("username") try: self.login_manual(user=username, manager=manager) except RuntimeError: @@ -266,11 +277,14 @@ class Connect(object): save_credentials(credentials) def login_manual(self, user=None, manager=None): - - ''' Return manual login user authenticated or raise error. - ''' + """Return manual login user authenticated or raise error.""" dialog = LoginManual("script-jellyfin-connect-login-manual.xml", *XML_PATH) - dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}}) + dialog.set_args( + **{ + "connect_manager": manager or self.connect_manager, + "username": user or {}, + } + ) dialog.doModal() if dialog.is_logged_in(): @@ -279,15 +293,13 @@ class Connect(object): raise RuntimeError("User is not authenticated") def remove_server(self, server_id): - - ''' Stop client and remove server. - ''' + """Stop client and remove server.""" Jellyfin(server_id).close() credentials = get_credentials() - for server in credentials['Servers']: - if server['Id'] == server_id: - credentials['Servers'].remove(server) + for server in credentials["Servers"]: + if server["Id"] == server_id: + credentials["Servers"].remove(server) break diff --git a/jellyfin_kodi/database/__init__.py b/jellyfin_kodi/database/__init__.py index e4871667..b92ff189 100644 --- a/jellyfin_kodi/database/__init__.py +++ b/jellyfin_kodi/database/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals + ################################################################################################# import datetime @@ -28,51 +29,56 @@ ADDON_DATA = translate_path("special://profile/addon_data/plugin.video.jellyfin/ class Database(object): + """This should be called like a context. + i.e. with Database('jellyfin') as db: + db.cursor + db.conn.commit() + """ - ''' This should be called like a context. - i.e. with Database('jellyfin') as db: - db.cursor - db.conn.commit() - ''' timeout = 120 discovered = False discovered_file = None def __init__(self, db_file=None, commit_close=True): - - ''' file: jellyfin, texture, music, video, :memory: or path to file - ''' + """file: jellyfin, texture, music, video, :memory: or path to file""" self.db_file = db_file or "video" self.commit_close = commit_close def __enter__(self): - - ''' Open the connection and return the Database class. - This is to allow for the cursor, conn and others to be accessible. - ''' + """Open the connection and return the Database class. + This is to allow for the cursor, conn and others to be accessible. + """ self.path = self._sql(self.db_file) self.conn = sqlite3.connect(self.path, timeout=self.timeout) self.cursor = self.conn.cursor() - if self.db_file in ('video', 'music', 'texture', 'jellyfin'): - self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi + if self.db_file in ("video", "music", "texture", "jellyfin"): + self.conn.execute( + "PRAGMA journal_mode=WAL" + ) # to avoid writing conflict with kodi LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn)) - if not window('jellyfin_db_check.bool') and self.db_file == 'jellyfin': + if not window("jellyfin_db_check.bool") and self.db_file == "jellyfin": - window('jellyfin_db_check.bool', True) + window("jellyfin_db_check.bool", True) jellyfin_tables(self.cursor) self.conn.commit() # Migration for #162 - if self.db_file == 'music': - query = self.conn.execute('SELECT * FROM path WHERE strPath LIKE "%/emby/%"') + if self.db_file == "music": + query = self.conn.execute( + 'SELECT * FROM path WHERE strPath LIKE "%/emby/%"' + ) contents = query.fetchall() if contents: for item in contents: - new_path = item[1].replace('/emby/', '/') - self.conn.execute('UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(new_path, item[0])) + new_path = item[1].replace("/emby/", "/") + self.conn.execute( + 'UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format( + new_path, item[0] + ) + ) return self @@ -97,68 +103,68 @@ class Database(object): return path def _discover_database(self, database): + """Use UpdateLibrary(video) to update the date modified + on the database file used by Kodi. + """ + if database == "video": - ''' Use UpdateLibrary(video) to update the date modified - on the database file used by Kodi. - ''' - if database == 'video': - - xbmc.executebuiltin('UpdateLibrary(video)') + xbmc.executebuiltin("UpdateLibrary(video)") xbmc.sleep(200) databases = translate_path("special://database/") - types = { - 'video': "MyVideos", - 'music': "MyMusic", - 'texture': "Textures" - } + types = {"video": "MyVideos", "music": "MyMusic", "texture": "Textures"} database = types[database] dirs, files = xbmcvfs.listdir(databases) - target = {'db_file': '', 'version': 0} + target = {"db_file": "", "version": 0} for db_file in reversed(files): - if (db_file.startswith(database) - and not db_file.endswith('-wal') - and not db_file.endswith('-shm') - and not db_file.endswith('db-journal')): + if ( + db_file.startswith(database) + and not db_file.endswith("-wal") + and not db_file.endswith("-shm") + and not db_file.endswith("db-journal") + ): - version_string = re.search('{}(.*).db'.format(database), db_file) + version_string = re.search("{}(.*).db".format(database), db_file) version = int(version_string.group(1)) - if version > target['version']: - target['db_file'] = db_file - target['version'] = version + if version > target["version"]: + target["db_file"] = db_file + target["version"] = version LOG.debug("Discovered database: %s", target) - self.discovered_file = target['db_file'] + self.discovered_file = target["db_file"] - return translate_path("special://database/%s" % target['db_file']) + return translate_path("special://database/%s" % target["db_file"]) def _sql(self, db_file): - - ''' Get the database path based on the file objects/obj_map.json - Compatible check, in the event multiple db version are supported with the same Kodi version. - Discover by file as a last resort. - ''' + """Get the database path based on the file objects/obj_map.json + Compatible check, in the event multiple db version are supported with the same Kodi version. + Discover by file as a last resort. + """ databases = obj.Objects().objects - if db_file not in ('video', 'music', 'texture') or databases.get('database_set%s' % db_file): + if db_file not in ("video", "music", "texture") or databases.get( + "database_set%s" % db_file + ): return self._get_database(databases[db_file], True) - discovered = self._discover_database(db_file) if not databases.get('database_set%s' % db_file) else None + discovered = ( + self._discover_database(db_file) + if not databases.get("database_set%s" % db_file) + else None + ) databases[db_file] = discovered self.discovered = True - databases['database_set%s' % db_file] = True + databases["database_set%s" % db_file] = True LOG.info("Database locked in: %s", databases[db_file]) return databases[db_file] def __exit__(self, exc_type, exc_val, exc_tb): - - ''' Close the connection and cursor. - ''' + """Close the connection and cursor.""" changes = self.conn.total_changes if exc_type is not None: # errors raised @@ -175,41 +181,43 @@ class Database(object): def jellyfin_tables(cursor): - - ''' Create the tables for the jellyfin database. - jellyfin, view, version - ''' + """Create the tables for the jellyfin database. + jellyfin, view, version + """ cursor.execute( """CREATE TABLE IF NOT EXISTS jellyfin( jellyfin_id TEXT UNIQUE, media_folder TEXT, jellyfin_type TEXT, media_type TEXT, kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, - checksum INTEGER, jellyfin_parent_id TEXT)""") + checksum INTEGER, jellyfin_parent_id TEXT)""" + ) cursor.execute( """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""") + view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""" + ) cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") columns = cursor.execute("SELECT * FROM jellyfin") - if 'jellyfin_parent_id' not in [description[0] for description in columns.description]: + if "jellyfin_parent_id" not in [ + description[0] for description in columns.description + ]: LOG.debug("Add missing column jellyfin_parent_id") cursor.execute("ALTER TABLE jellyfin ADD COLUMN jellyfin_parent_id 'TEXT'") def reset(): - - ''' Reset both the jellyfin database and the kodi database. - ''' + """Reset both the jellyfin database and the kodi database.""" from ..views import Views + views = Views() if not dialog("yesno", "{jellyfin}", translate(33074)): return - window('jellyfin_should_stop.bool', True) + window("jellyfin_should_stop.bool", True) count = 10 - while window('jellyfin_sync.bool'): + while window("jellyfin_sync.bool"): LOG.info("Sync is running...") count -= 1 @@ -239,12 +247,12 @@ def reset(): if xbmcvfs.exists(os.path.join(ADDON_DATA, "sync.json")): xbmcvfs.delete(os.path.join(ADDON_DATA, "sync.json")) - settings('enableMusic.bool', False) - settings('MinimumSetup', "") - settings('MusicRescan.bool', False) - settings('SyncInstallRunDone.bool', False) + settings("enableMusic.bool", False) + settings("MinimumSetup", "") + settings("MusicRescan.bool", False) + settings("SyncInstallRunDone.bool", False) dialog("ok", "{jellyfin}", translate(33088)) - xbmc.executebuiltin('RestartApp') + xbmc.executebuiltin("RestartApp") def reset_kodi(): @@ -256,18 +264,20 @@ def reset_kodi(): name = table[0] # These tables are populated by Kodi and we shouldn't wipe them - if name not in ['version', 'videoversiontype']: + if name not in ["version", "videoversiontype"]: videodb.cursor.execute("DELETE FROM " + name) - if settings('enableMusic.bool') or dialog("yesno", "{jellyfin}", translate(33162)): + if settings("enableMusic.bool") or dialog("yesno", "{jellyfin}", translate(33162)): - with Database('music') as musicdb: - musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + with Database("music") as musicdb: + musicdb.cursor.execute( + "SELECT tbl_name FROM sqlite_master WHERE type='table'" + ) for table in musicdb.cursor.fetchall(): name = table[0] - if name != 'version': + if name != "version": musicdb.cursor.execute("DELETE FROM " + name) LOG.info("[ reset kodi ]") @@ -275,13 +285,15 @@ def reset_kodi(): def reset_jellyfin(): - with Database('jellyfin') as jellyfindb: - jellyfindb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + with Database("jellyfin") as jellyfindb: + jellyfindb.cursor.execute( + "SELECT tbl_name FROM sqlite_master WHERE type='table'" + ) for table in jellyfindb.cursor.fetchall(): name = table[0] - if name not in ('version', 'view'): + if name not in ("version", "view"): jellyfindb.cursor.execute("DELETE FROM " + name) jellyfindb.cursor.execute("DROP table IF EXISTS jellyfin") @@ -292,10 +304,8 @@ def reset_jellyfin(): def reset_artwork(): - - ''' Remove all existing texture. - ''' - thumbnails = translate_path('special://thumbnails/') + """Remove all existing texture.""" + thumbnails = translate_path("special://thumbnails/") if xbmcvfs.exists(thumbnails): dirs, ignore = xbmcvfs.listdir(thumbnails) @@ -307,13 +317,13 @@ def reset_artwork(): LOG.debug("DELETE thumbnail %s", thumb) xbmcvfs.delete(os.path.join(thumbnails, directory, thumb)) - with Database('texture') as texdb: + with Database("texture") as texdb: texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") for table in texdb.cursor.fetchall(): name = table[0] - if name != 'version': + if name != "version": texdb.cursor.execute("DELETE FROM " + name) LOG.info("[ reset artwork ]") @@ -327,18 +337,18 @@ def get_sync(): xbmcvfs.mkdirs(ADDON_DATA) try: - with open(os.path.join(ADDON_DATA, 'sync.json'), 'rb') as infile: + with open(os.path.join(ADDON_DATA, "sync.json"), "rb") as infile: sync = json.load(infile) except Exception: sync = {} - sync['Libraries'] = sync.get('Libraries', []) - sync['RestorePoint'] = sync.get('RestorePoint', {}) - sync['Whitelist'] = list(set(sync.get('Whitelist', []))) - sync['SortedViews'] = sync.get('SortedViews', []) + sync["Libraries"] = sync.get("Libraries", []) + sync["RestorePoint"] = sync.get("RestorePoint", {}) + sync["Whitelist"] = list(set(sync.get("Whitelist", []))) + sync["SortedViews"] = sync.get("SortedViews", []) # Temporary cleanup from #494/#511, remove in a future version - sync['Libraries'] = [lib_id for lib_id in sync['Libraries'] if ',' not in lib_id] + sync["Libraries"] = [lib_id for lib_id in sync["Libraries"] if "," not in lib_id] return sync @@ -348,12 +358,12 @@ def save_sync(sync): if not xbmcvfs.exists(ADDON_DATA): xbmcvfs.mkdirs(ADDON_DATA) - sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + sync["Date"] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") - with open(os.path.join(ADDON_DATA, 'sync.json'), 'wb') as outfile: + with open(os.path.join(ADDON_DATA, "sync.json"), "wb") as outfile: data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False) if isinstance(data, text_type): - data = data.encode('utf-8') + data = data.encode("utf-8") outfile.write(data) @@ -365,30 +375,30 @@ def get_credentials(): xbmcvfs.mkdirs(ADDON_DATA) try: - with open(os.path.join(ADDON_DATA, 'data.json'), 'rb') as infile: + with open(os.path.join(ADDON_DATA, "data.json"), "rb") as infile: credentials = json.load(infile) except IOError: credentials = {} - credentials['Servers'] = credentials.get('Servers', []) + credentials["Servers"] = credentials.get("Servers", []) # Migration for #145 # TODO: CLEANUP for 1.0.0 release - for server in credentials['Servers']: + for server in credentials["Servers"]: # Functionality removed in #60 - if 'RemoteAddress' in server: - del server['RemoteAddress'] - if 'ManualAddress' in server: - server['address'] = server['ManualAddress'] - del server['ManualAddress'] + if "RemoteAddress" in server: + del server["RemoteAddress"] + if "ManualAddress" in server: + server["address"] = server["ManualAddress"] + del server["ManualAddress"] # If manual is present, local should always be here, but better to be safe - if 'LocalAddress' in server: - del server['LocalAddress'] - elif 'LocalAddress' in server: - server['address'] = server['LocalAddress'] - del server['LocalAddress'] - if 'LastConnectionMode' in server: - del server['LastConnectionMode'] + if "LocalAddress" in server: + del server["LocalAddress"] + elif "LocalAddress" in server: + server["address"] = server["LocalAddress"] + del server["LocalAddress"] + if "LastConnectionMode" in server: + del server["LastConnectionMode"] return credentials @@ -399,21 +409,21 @@ def save_credentials(credentials): if not xbmcvfs.exists(ADDON_DATA): xbmcvfs.mkdirs(ADDON_DATA) try: - with open(os.path.join(ADDON_DATA, 'data.json'), 'wb') as outfile: + with open(os.path.join(ADDON_DATA, "data.json"), "wb") as outfile: data = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False) if isinstance(data, text_type): - data = data.encode('utf-8') + data = data.encode("utf-8") outfile.write(data) except Exception: LOG.exception("Failed to save credentials:") def get_item(kodi_id, media): - - ''' Get jellyfin item based on kodi id and media. - ''' - with Database('jellyfin') as jellyfindb: - item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(kodi_id, media) + """Get jellyfin item based on kodi id and media.""" + with Database("jellyfin") as jellyfindb: + item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id( + kodi_id, media + ) if not item: LOG.debug("Not an jellyfin item") diff --git a/jellyfin_kodi/database/jellyfin_db.py b/jellyfin_kodi/database/jellyfin_db.py index 0cbe9dd6..5201e932 100644 --- a/jellyfin_kodi/database/jellyfin_db.py +++ b/jellyfin_kodi/database/jellyfin_db.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals + ################################################################################################# from . import queries as QU @@ -14,7 +15,7 @@ LOG = LazyLogger(__name__) ################################################################################################## -class JellyfinDatabase(): +class JellyfinDatabase: def __init__(self, cursor): self.cursor = cursor @@ -32,9 +33,7 @@ class JellyfinDatabase(): self.cursor.execute(QU.update_reference, args) def update_parent_id(self, *args): - - ''' Parent_id is the parent Kodi id. - ''' + """Parent_id is the parent Kodi id.""" self.cursor.execute(QU.update_parent, args) def get_item_id_by_parent_id(self, *args): @@ -160,8 +159,8 @@ class JellyfinDatabase(): return self.cursor.fetchone() def add_version(self, *args): - ''' + """ We only ever want one value here, so erase the existing contents first - ''' + """ self.cursor.execute(QU.delete_version) self.cursor.execute(QU.add_version, args) diff --git a/jellyfin_kodi/database/queries.py b/jellyfin_kodi/database/queries.py index 52d68846..41a59cda 100644 --- a/jellyfin_kodi/database/queries.py +++ b/jellyfin_kodi/database/queries.py @@ -94,16 +94,126 @@ INSERT OR REPLACE INTO jellyfin(jellyfin_id, kodi_id, kodi_fileid, kodi_pat media_type, parent_id, checksum, media_folder, jellyfin_parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] -add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None] -add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] -add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, None, None] -add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None] -add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", None, "{JellyfinParentId}"] -add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] -add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] -add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] -add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] +add_reference_movie_obj = [ + "{Id}", + "{MovieId}", + "{FileId}", + "{PathId}", + "Movie", + "movie", + None, + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] +add_reference_boxset_obj = [ + "{Id}", + "{SetId}", + None, + None, + "BoxSet", + "set", + None, + "{Checksum}", + None, + None, +] +add_reference_tvshow_obj = [ + "{Id}", + "{ShowId}", + None, + "{PathId}", + "Series", + "tvshow", + None, + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] +add_reference_season_obj = [ + "{Id}", + "{SeasonId}", + None, + None, + "Season", + "season", + "{ShowId}", + None, + None, + None, +] +add_reference_pool_obj = [ + "{SeriesId}", + "{ShowId}", + None, + "{PathId}", + "Series", + "tvshow", + None, + "{Checksum}", + "{LibraryId}", + None, +] +add_reference_episode_obj = [ + "{Id}", + "{EpisodeId}", + "{FileId}", + "{PathId}", + "Episode", + "episode", + "{SeasonId}", + "{Checksum}", + None, + "{JellyfinParentId}", +] +add_reference_mvideo_obj = [ + "{Id}", + "{MvideoId}", + "{FileId}", + "{PathId}", + "MusicVideo", + "musicvideo", + None, + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] +add_reference_artist_obj = [ + "{Id}", + "{ArtistId}", + None, + None, + "{ArtistType}", + "artist", + None, + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] +add_reference_album_obj = [ + "{Id}", + "{AlbumId}", + None, + None, + "MusicAlbum", + "album", + None, + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] +add_reference_song_obj = [ + "{Id}", + "{SongId}", + None, + "{PathId}", + "Audio", + "song", + "{AlbumId}", + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] add_view = """ INSERT OR REPLACE INTO view(view_id, view_name, media_type) VALUES (?, ?, ?) diff --git a/jellyfin_kodi/dialogs/context.py b/jellyfin_kodi/dialogs/context.py index 6d48a503..8e8eb5db 100644 --- a/jellyfin_kodi/dialogs/context.py +++ b/jellyfin_kodi/dialogs/context.py @@ -47,8 +47,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog): def onInit(self): - if window('JellyfinUserImage'): - self.getControl(USER_IMAGE).setImage(window('JellyfinUserImage')) + if window("JellyfinUserImage"): + self.getControl(USER_IMAGE).setImage(window("JellyfinUserImage")) LOG.info("options: %s", self._options) self.list_ = self.getControl(LIST) @@ -63,21 +63,35 @@ class ContextMenu(xbmcgui.WindowXMLDialog): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): self.close() - if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST: + if ( + action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) + and self.getFocusId() == LIST + ): option = self.list_.getSelectedItem() self.selected_option = ensure_text(option.getLabel()) - LOG.info('option selected: %s', self.selected_option) + LOG.info("option selected: %s", self.selected_option) self.close() def _add_editcontrol(self, x, y, height, width, password=0): - media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlImage(0, 0, 0, 0, - filename=os.path.join(media, "white.png"), - aspectRatio=0, - colorDiffuse="ff111111") + media = os.path.join( + xbmcaddon.Addon(addon_id()).getAddonInfo("path"), + "resources", + "skins", + "default", + "media", + ) + control = xbmcgui.ControlImage( + 0, + 0, + 0, + 0, + filename=os.path.join(media, "white.png"), + aspectRatio=0, + colorDiffuse="ff111111", + ) control.setPosition(x, y) control.setHeight(height) control.setWidth(width) diff --git a/jellyfin_kodi/dialogs/loginmanual.py b/jellyfin_kodi/dialogs/loginmanual.py index 1d70bec2..c63dd519 100644 --- a/jellyfin_kodi/dialogs/loginmanual.py +++ b/jellyfin_kodi/dialogs/loginmanual.py @@ -18,7 +18,7 @@ SIGN_IN = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 -ERROR = {'Invalid': 1, 'Empty': 2} +ERROR = {"Invalid": 1, "Empty": 2} ################################################################################################## @@ -76,7 +76,7 @@ class LoginManual(xbmcgui.WindowXMLDialog): if not user: # Display error - self._error(ERROR['Empty'], translate('empty_user')) + self._error(ERROR["Empty"], translate("empty_user")) LOG.error("Username cannot be null") elif self._login(user, password): @@ -88,7 +88,7 @@ class LoginManual(xbmcgui.WindowXMLDialog): def onAction(self, action): - if self.error == ERROR['Empty'] and self.user_field.getText(): + if self.error == ERROR["Empty"] and self.user_field.getText(): self._disable_error() if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): @@ -102,12 +102,12 @@ class LoginManual(xbmcgui.WindowXMLDialog): textColor="FF00A4DC", disabledColor="FF888888", focusTexture="-", - noFocusTexture="-" + noFocusTexture="-", ) # TODO: Kodi 17 compat removal cleanup if kodi_version() < 18: - kwargs['isPassword'] = password + kwargs["isPassword"] = password control = xbmcgui.ControlEdit(0, 0, 0, 0, **kwargs) @@ -126,11 +126,13 @@ class LoginManual(xbmcgui.WindowXMLDialog): def _login(self, username, password): - server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address'] + server = self.connect_manager.get_server_info(self.connect_manager.server_id)[ + "address" + ] result = self.connect_manager.login(server, username, password) if not result: - self._error(ERROR['Invalid'], translate('invalid_auth')) + self._error(ERROR["Invalid"], translate("invalid_auth")) return False else: self._user = result @@ -140,9 +142,9 @@ class LoginManual(xbmcgui.WindowXMLDialog): self.error = state self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('true') + self.error_toggle.setVisibleCondition("true") def _disable_error(self): self.error = None - self.error_toggle.setVisibleCondition('false') + self.error_toggle.setVisibleCondition("false") diff --git a/jellyfin_kodi/dialogs/serverconnect.py b/jellyfin_kodi/dialogs/serverconnect.py index 73d72058..adeffbe5 100644 --- a/jellyfin_kodi/dialogs/serverconnect.py +++ b/jellyfin_kodi/dialogs/serverconnect.py @@ -64,8 +64,10 @@ class ServerConnect(xbmcgui.WindowXMLDialog): self.list_ = self.getControl(LIST) for server in self.servers: - server_type = "wifi" if server.get('ExchangeToken') else "network" - self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) + server_type = "wifi" if server.get("ExchangeToken") else "network" + self.list_.addItem( + self._add_listitem(server["Name"], server["Id"], server_type) + ) if self.user_image is not None: self.getControl(USER_IMAGE).setImage(self.user_image) @@ -77,8 +79,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog): def _add_listitem(cls, label, server_id, server_type): item = xbmcgui.ListItem(label) - item.setProperty('id', server_id) - item.setProperty('server_type', server_type) + item.setProperty("id", server_id) + item.setProperty("server_type", server_type) return item @@ -87,14 +89,17 @@ class ServerConnect(xbmcgui.WindowXMLDialog): if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): self.close() - if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST: + if ( + action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) + and self.getFocusId() == LIST + ): server = self.list_.getSelectedItem() - selected_id = server.getProperty('id') - LOG.info('Server Id selected: %s', selected_id) + selected_id = server.getProperty("id") + LOG.info("Server Id selected: %s", selected_id) if self._connect_server(selected_id): - self.message_box.setVisibleCondition('false') + self.message_box.setVisibleCondition("false") self.close() def onClick(self, control): @@ -109,19 +114,19 @@ class ServerConnect(xbmcgui.WindowXMLDialog): def _connect_server(self, server_id): server = self.connect_manager.get_server_info(server_id) - self.message.setLabel("%s %s..." % (translate(30610), server['Name'])) + self.message.setLabel("%s %s..." % (translate(30610), server["Name"])) - self.message_box.setVisibleCondition('true') - self.busy.setVisibleCondition('true') + self.message_box.setVisibleCondition("true") + self.busy.setVisibleCondition("true") result = self.connect_manager.connect_to_server(server) - if result['State'] == CONNECTION_STATE['Unavailable']: - self.busy.setVisibleCondition('false') + if result["State"] == CONNECTION_STATE["Unavailable"]: + self.busy.setVisibleCondition("false") self.message.setLabel(translate(30609)) return False else: xbmc.sleep(1000) - self._selected_server = result['Servers'][0] + self._selected_server = result["Servers"][0] return True diff --git a/jellyfin_kodi/dialogs/servermanual.py b/jellyfin_kodi/dialogs/servermanual.py index 551c840e..8f4b44bb 100644 --- a/jellyfin_kodi/dialogs/servermanual.py +++ b/jellyfin_kodi/dialogs/servermanual.py @@ -22,10 +22,7 @@ CONNECT = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 -ERROR = { - 'Invalid': 1, - 'Empty': 2 -} +ERROR = {"Invalid": 1, "Empty": 2} # https://stackoverflow.com/a/17871737/1035647 _IPV6_PATTERN = r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" @@ -77,7 +74,7 @@ class ServerManual(xbmcgui.WindowXMLDialog): if not server: # Display error - self._error(ERROR['Empty'], translate('empty_server')) + self._error(ERROR["Empty"], translate("empty_server")) LOG.error("Server cannot be null") elif self._connect_to_server(server): @@ -89,7 +86,7 @@ class ServerManual(xbmcgui.WindowXMLDialog): def onAction(self, action): - if self.error == ERROR['Empty'] and self.host_field.getText(): + if self.error == ERROR["Empty"] and self.host_field.getText(): self._disable_error() if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): @@ -97,13 +94,18 @@ class ServerManual(xbmcgui.WindowXMLDialog): def _add_editcontrol(self, x, y, height, width): - control = xbmcgui.ControlEdit(0, 0, 0, 0, - label="", - font="font13", - textColor="FF00A4DC", - disabledColor="FF888888", - focusTexture="-", - noFocusTexture="-") + control = xbmcgui.ControlEdit( + 0, + 0, + 0, + 0, + label="", + font="font13", + textColor="FF00A4DC", + disabledColor="FF888888", + focusTexture="-", + noFocusTexture="-", + ) control.setPosition(x, y) control.setHeight(height) control.setWidth(width) @@ -118,25 +120,25 @@ class ServerManual(xbmcgui.WindowXMLDialog): self._message("%s %s..." % (translate(30610), server)) result = self.connect_manager.connect_to_address(server) - if result['State'] == CONNECTION_STATE['Unavailable']: + if result["State"] == CONNECTION_STATE["Unavailable"]: self._message(translate(30609)) return False else: - self._server = result['Servers'][0] + self._server = result["Servers"][0] return True def _message(self, message): self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('true') + self.error_toggle.setVisibleCondition("true") def _error(self, state, message): self.error = state self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('true') + self.error_toggle.setVisibleCondition("true") def _disable_error(self): self.error = None - self.error_toggle.setVisibleCondition('false') + self.error_toggle.setVisibleCondition("false") diff --git a/jellyfin_kodi/dialogs/usersconnect.py b/jellyfin_kodi/dialogs/usersconnect.py index 8c1428ef..1abb8949 100644 --- a/jellyfin_kodi/dialogs/usersconnect.py +++ b/jellyfin_kodi/dialogs/usersconnect.py @@ -52,17 +52,20 @@ class UsersConnect(xbmcgui.WindowXMLDialog): self.list_ = self.getControl(LIST) for user in self.users: - user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user - else self._get_user_artwork(user['Id'], 'Primary')) - self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) + user_image = ( + "items/logindefault.png" + if "PrimaryImageTag" not in user + else self._get_user_artwork(user["Id"], "Primary") + ) + self.list_.addItem(self._add_listitem(user["Name"], user["Id"], user_image)) self.setFocus(self.list_) def _add_listitem(self, label, user_id, user_image): item = xbmcgui.ListItem(label) - item.setProperty('id', user_id) - item.setArt({'icon': user_image}) + item.setProperty("id", user_id) + item.setArt({"icon": user_image}) return item @@ -71,14 +74,17 @@ class UsersConnect(xbmcgui.WindowXMLDialog): if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): self.close() - if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST: + if ( + action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) + and self.getFocusId() == LIST + ): user = self.list_.getSelectedItem() - selected_id = user.getProperty('id') - LOG.info('User Id selected: %s', selected_id) + selected_id = user.getProperty("id") + LOG.info("User Id selected: %s", selected_id) for user in self.users: - if user['Id'] == selected_id: + if user["Id"] == selected_id: self._user = user break @@ -95,4 +101,8 @@ class UsersConnect(xbmcgui.WindowXMLDialog): def _get_user_artwork(self, user_id, item_type): # Load user information set by UserClient - return "%s/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type) + return "%s/Users/%s/Images/%s?Format=original" % ( + self.server, + user_id, + item_type, + ) diff --git a/jellyfin_kodi/downloader.py b/jellyfin_kodi/downloader.py index f9fb03ca..2992af6e 100644 --- a/jellyfin_kodi/downloader.py +++ b/jellyfin_kodi/downloader.py @@ -25,7 +25,7 @@ LOG = LazyLogger(__name__) def get_jellyfinserver_url(handler): - if handler.startswith('/'): + if handler.startswith("/"): handler = handler[1:] LOG.info("handler starts with /: %s", handler) @@ -38,47 +38,55 @@ def _http(action, url, request=None, server_id=None): if request is None: request = {} - request.update({'url': url, 'type': action}) + request.update({"url": url, "type": action}) return Jellyfin(server_id).http.request(request) def _get(handler, params=None, server_id=None): - return _http("GET", get_jellyfinserver_url(handler), {'params': params}, server_id) + return _http("GET", get_jellyfinserver_url(handler), {"params": params}, server_id) def _post(handler, json=None, params=None, server_id=None): - return _http("POST", get_jellyfinserver_url(handler), {'params': params, 'json': json}, server_id) + return _http( + "POST", + get_jellyfinserver_url(handler), + {"params": params, "json": json}, + server_id, + ) def _delete(handler, params=None, server_id=None): - return _http("DELETE", get_jellyfinserver_url(handler), {'params': params}, server_id) + return _http( + "DELETE", get_jellyfinserver_url(handler), {"params": params}, server_id + ) def validate_view(library_id, item_id): - - ''' This confirms a single item from the library matches the view it belongs to. - Used to detect grouped libraries. - ''' + """This confirms a single item from the library matches the view it belongs to. + Used to detect grouped libraries. + """ try: - result = _get("Users/{UserId}/Items", { - 'ParentId': library_id, - 'Recursive': True, - 'Ids': item_id - }) + result = _get( + "Users/{UserId}/Items", + {"ParentId": library_id, "Recursive": True, "Ids": item_id}, + ) except Exception as error: LOG.exception(error) return False - return bool(len(result['Items'])) + return bool(len(result["Items"])) def get_single_item(parent_id, media): - return _get("Users/{UserId}/Items", { - 'ParentId': parent_id, - 'Recursive': True, - 'Limit': 1, - 'IncludeItemTypes': media - }) + return _get( + "Users/{UserId}/Items", + { + "ParentId": parent_id, + "Recursive": True, + "Limit": 1, + "IncludeItemTypes": media, + }, + ) def get_movies_by_boxset(boxset_id): @@ -90,13 +98,13 @@ def get_movies_by_boxset(boxset_id): def get_episode_by_show(show_id): query = { - 'url': "Shows/%s/Episodes" % show_id, - 'params': { - 'EnableUserData': True, - 'EnableImages': True, - 'UserId': "{UserId}", - 'Fields': api.info() - } + "url": "Shows/%s/Episodes" % show_id, + "params": { + "EnableUserData": True, + "EnableImages": True, + "UserId": "{UserId}", + "Fields": api.info(), + }, } for items in _get_items(query): yield items @@ -105,14 +113,14 @@ def get_episode_by_show(show_id): def get_episode_by_season(show_id, season_id): query = { - 'url': "Shows/%s/Episodes" % show_id, - 'params': { - 'SeasonId': season_id, - 'EnableUserData': True, - 'EnableImages': True, - 'UserId': "{UserId}", - 'Fields': api.info() - } + "url": "Shows/%s/Episodes" % show_id, + "params": { + "SeasonId": season_id, + "EnableUserData": True, + "EnableImages": True, + "UserId": "{UserId}", + "Fields": api.info(), + }, } for items in _get_items(query): yield items @@ -123,41 +131,41 @@ def get_item_count(parent_id, item_type=None, params=None): url = "Users/{UserId}/Items" query_params = { - 'ParentId': parent_id, - 'IncludeItemTypes': item_type, - 'EnableTotalRecordCount': True, - 'LocationTypes': "FileSystem,Remote,Offline", - 'Recursive': True, - 'Limit': 1 + "ParentId": parent_id, + "IncludeItemTypes": item_type, + "EnableTotalRecordCount": True, + "LocationTypes": "FileSystem,Remote,Offline", + "Recursive": True, + "Limit": 1, } if params: - query_params['params'].update(params) + query_params["params"].update(params) result = _get(url, query_params) - return result.get('TotalRecordCount', 1) + return result.get("TotalRecordCount", 1) def get_items(parent_id, item_type=None, basic=False, params=None): query = { - 'url': "Users/{UserId}/Items", - 'params': { - 'ParentId': parent_id, - 'IncludeItemTypes': item_type, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': api.basic_info() if basic else api.info(), - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True - } + "url": "Users/{UserId}/Items", + "params": { + "ParentId": parent_id, + "IncludeItemTypes": item_type, + "SortBy": "SortName", + "SortOrder": "Ascending", + "Fields": api.basic_info() if basic else api.info(), + "CollapseBoxSetItems": False, + "IsVirtualUnaired": False, + "EnableTotalRecordCount": False, + "LocationTypes": "FileSystem,Remote,Offline", + "IsMissing": False, + "Recursive": True, + }, } if params: - query['params'].update(params) + query["params"].update(params) for items in _get_items(query): yield items @@ -166,20 +174,20 @@ def get_items(parent_id, item_type=None, basic=False, params=None): def get_artists(parent_id=None): query = { - 'url': 'Artists', - 'params': { - 'UserId': "{UserId}", - 'ParentId': parent_id, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': api.music_info(), - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True - } + "url": "Artists", + "params": { + "UserId": "{UserId}", + "ParentId": parent_id, + "SortBy": "SortName", + "SortOrder": "Ascending", + "Fields": api.music_info(), + "CollapseBoxSetItems": False, + "IsVirtualUnaired": False, + "EnableTotalRecordCount": False, + "LocationTypes": "FileSystem,Remote,Offline", + "IsMissing": False, + "Recursive": True, + }, } for items in _get_items(query): @@ -188,48 +196,49 @@ def get_artists(parent_id=None): @stop def _get_items(query, server_id=None): - - ''' query = { - 'url': string, - 'params': dict -- opt, include StartIndex to resume - } - ''' - items = { - 'Items': [], - 'TotalRecordCount': 0, - 'RestorePoint': {} + """query = { + 'url': string, + 'params': dict -- opt, include StartIndex to resume } + """ + items = {"Items": [], "TotalRecordCount": 0, "RestorePoint": {}} - limit = min(int(settings('limitIndex') or 50), 50) - dthreads = int(settings('limitThreads') or 3) + limit = min(int(settings("limitIndex") or 50), 50) + dthreads = int(settings("limitThreads") or 3) - url = query['url'] - query.setdefault('params', {}) - params = query['params'] + url = query["url"] + query.setdefault("params", {}) + params = query["params"] try: test_params = dict(params) - test_params['Limit'] = 1 - test_params['EnableTotalRecordCount'] = True + test_params["Limit"] = 1 + test_params["EnableTotalRecordCount"] = True - items['TotalRecordCount'] = _get(url, test_params, server_id=server_id)['TotalRecordCount'] + items["TotalRecordCount"] = _get(url, test_params, server_id=server_id)[ + "TotalRecordCount" + ] except Exception as error: - LOG.exception("Failed to retrieve the server response %s: %s params:%s", url, error, params) + LOG.exception( + "Failed to retrieve the server response %s: %s params:%s", + url, + error, + params, + ) else: - params.setdefault('StartIndex', 0) + params.setdefault("StartIndex", 0) def get_query_params(params, start, count): params_copy = dict(params) - params_copy['StartIndex'] = start - params_copy['Limit'] = count + params_copy["StartIndex"] = start + params_copy["Limit"] = count return params_copy query_params = [ get_query_params(params, offset, limit) - for offset - in range(params['StartIndex'], items['TotalRecordCount'], limit) + for offset in range(params["StartIndex"], items["TotalRecordCount"], limit) ] # multiprocessing.dummy.Pool completes all requests in multiple threads but has to @@ -257,27 +266,29 @@ def _get_items(query, server_id=None): # process complete jobs for job in concurrent.futures.as_completed(jobs): # get the result - result = job.result() or {'Items': []} - query['params'] = jobs[job] + result = job.result() or {"Items": []} + query["params"] = jobs[job] # free job memory del jobs[job] del job # Mitigates #216 till the server validates the date provided is valid - if result['Items'][0].get('ProductionYear'): + if result["Items"][0].get("ProductionYear"): try: - date(result['Items'][0]['ProductionYear'], 1, 1) + date(result["Items"][0]["ProductionYear"], 1, 1) except ValueError: - LOG.info('#216 mitigation triggered. Setting ProductionYear to None') - result['Items'][0]['ProductionYear'] = None + LOG.info( + "#216 mitigation triggered. Setting ProductionYear to None" + ) + result["Items"][0]["ProductionYear"] = None - items['Items'].extend(result['Items']) + items["Items"].extend(result["Items"]) # Using items to return data and communicate a restore point back to the callee is # a violation of the SRP. TODO: Separate responsibilities. - items['RestorePoint'] = query + items["RestorePoint"] = query yield items - del items['Items'][:] + del items["Items"][:] # release the semaphore again thread_buffer.release() @@ -307,25 +318,25 @@ class GetItemWorker(threading.Thread): return request = { - 'type': "GET", - 'handler': "Users/{UserId}/Items", - 'params': { - 'Ids': ','.join(str(x) for x in item_ids), - 'Fields': api.info() - } + "type": "GET", + "handler": "Users/{UserId}/Items", + "params": { + "Ids": ",".join(str(x) for x in item_ids), + "Fields": api.info(), + }, } try: result = self.server.http.request(request, s) - for item in result['Items']: + for item in result["Items"]: - if item['Type'] in self.output: - self.output[item['Type']].put(item) + if item["Type"] in self.output: + self.output[item["Type"]].put(item) except HTTPException as error: LOG.error("--[ http status: %s ]", error.status) - if error.status == 'ServerUnreachable': + if error.status == "ServerUnreachable": self.is_done = True break @@ -335,5 +346,5 @@ class GetItemWorker(threading.Thread): self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break diff --git a/jellyfin_kodi/entrypoint/context.py b/jellyfin_kodi/entrypoint/context.py index dca7e9f9..e5d383b8 100644 --- a/jellyfin_kodi/entrypoint/context.py +++ b/jellyfin_kodi/entrypoint/context.py @@ -17,14 +17,18 @@ from ..jellyfin import Jellyfin ################################################################################################# LOG = LazyLogger(__name__) -XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i") +XML_PATH = ( + xbmcaddon.Addon("plugin.video.jellyfin").getAddonInfo("path"), + "default", + "1080i", +) OPTIONS = { - 'Refresh': translate(30410), - 'Delete': translate(30409), - 'Addon': translate(30408), - 'AddFav': translate(30405), - 'RemoveFav': translate(30406), - 'Transcode': translate(30412) + "Refresh": translate(30410), + "Delete": translate(30409), + "Addon": translate(30408), + "AddFav": translate(30405), + "RemoveFav": translate(30406), + "Transcode": translate(30412), } ################################################################################################# @@ -39,31 +43,33 @@ class Context(object): try: self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None self.media = self.get_media_type() - self.server_id = sys.listitem.getProperty('jellyfinserver') or None + self.server_id = sys.listitem.getProperty("jellyfinserver") or None self.api_client = Jellyfin(self.server_id).get_client().jellyfin - item_id = sys.listitem.getProperty('jellyfinid') + item_id = sys.listitem.getProperty("jellyfinid") except AttributeError: self.server_id = None - if xbmc.getInfoLabel('ListItem.Property(jellyfinid)'): - item_id = xbmc.getInfoLabel('ListItem.Property(jellyfinid)') + if xbmc.getInfoLabel("ListItem.Property(jellyfinid)"): + item_id = xbmc.getInfoLabel("ListItem.Property(jellyfinid)") else: - self.kodi_id = xbmc.getInfoLabel('ListItem.DBID') - self.media = xbmc.getInfoLabel('ListItem.DBTYPE') + self.kodi_id = xbmc.getInfoLabel("ListItem.DBID") + self.media = xbmc.getInfoLabel("ListItem.DBTYPE") item_id = None - addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json") - with open(addon_data, 'rb') as infile: + addon_data = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/data.json" + ) + with open(addon_data, "rb") as infile: data = json.load(infile) try: - server_data = data['Servers'][0] - self.api_client.config.data['auth.server'] = server_data.get('address') - self.api_client.config.data['auth.server-name'] = server_data.get('Name') - self.api_client.config.data['auth.user_id'] = server_data.get('UserId') - self.api_client.config.data['auth.token'] = server_data.get('AccessToken') + server_data = data["Servers"][0] + self.api_client.config.data["auth.server"] = server_data.get("address") + self.api_client.config.data["auth.server-name"] = server_data.get("Name") + self.api_client.config.data["auth.user_id"] = server_data.get("UserId") + self.api_client.config.data["auth.token"] = server_data.get("AccessToken") except Exception as e: - LOG.warning('Addon appears to not be configured yet: {}'.format(e)) + LOG.warning("Addon appears to not be configured yet: {}".format(e)) if self.server_id or item_id: self.item = self.api_client.get_item(item_id) @@ -81,26 +87,28 @@ class Context(object): elif self.select_menu(): self.action_menu() - if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], OPTIONS['RemoveFav']): + if self._selected_option in ( + OPTIONS["Delete"], + OPTIONS["AddFav"], + OPTIONS["RemoveFav"], + ): xbmc.sleep(500) - xbmc.executebuiltin('Container.Refresh') + xbmc.executebuiltin("Container.Refresh") def get_media_type(self): - - ''' Get media type based on sys.listitem. If unfilled, base on visible window. - ''' + """Get media type based on sys.listitem. If unfilled, base on visible window.""" media = sys.listitem.getVideoInfoTag().getMediaType() if not media: - if xbmc.getCondVisibility('Container.Content(albums)'): + if xbmc.getCondVisibility("Container.Content(albums)"): media = "album" - elif xbmc.getCondVisibility('Container.Content(artists)'): + elif xbmc.getCondVisibility("Container.Content(artists)"): media = "artist" - elif xbmc.getCondVisibility('Container.Content(songs)'): + elif xbmc.getCondVisibility("Container.Content(songs)"): media = "song" - elif xbmc.getCondVisibility('Container.Content(pictures)'): + elif xbmc.getCondVisibility("Container.Content(pictures)"): media = "picture" else: LOG.info("media is unknown") @@ -108,40 +116,37 @@ class Context(object): return media def get_item_id(self): - - ''' Get synced item from jellyfindb. - ''' + """Get synced item from jellyfindb.""" item = database.get_item(self.kodi_id, self.media) if not item: return return { - 'Id': item[0], - 'UserData': json.loads(item[4]) if item[4] else {}, - 'Type': item[3] + "Id": item[0], + "UserData": json.loads(item[4]) if item[4] else {}, + "Type": item[3], } def select_menu(self): - - ''' Display the select dialog. - Favorites, Refresh, Delete (opt), Settings. - ''' + """Display the select dialog. + Favorites, Refresh, Delete (opt), Settings. + """ options = [] - if self.item['Type'] != 'Season': + if self.item["Type"] != "Season": - if self.item['UserData'].get('IsFavorite'): - options.append(OPTIONS['RemoveFav']) + if self.item["UserData"].get("IsFavorite"): + options.append(OPTIONS["RemoveFav"]) else: - options.append(OPTIONS['AddFav']) + options.append(OPTIONS["AddFav"]) - options.append(OPTIONS['Refresh']) + options.append(OPTIONS["Refresh"]) - if settings('enableContextDelete.bool'): - options.append(OPTIONS['Delete']) + if settings("enableContextDelete.bool"): + options.append(OPTIONS["Delete"]) - options.append(OPTIONS['Addon']) + options.append(OPTIONS["Addon"]) context_menu = context.ContextMenu("script-jellyfin-context.xml", *XML_PATH) context_menu.set_options(options) @@ -156,24 +161,26 @@ class Context(object): selected = self._selected_option - if selected == OPTIONS['Refresh']: - self.api_client.refresh_item(self.item['Id']) + if selected == OPTIONS["Refresh"]: + self.api_client.refresh_item(self.item["Id"]) - elif selected == OPTIONS['AddFav']: - self.api_client.favorite(self.item['Id'], True) + elif selected == OPTIONS["AddFav"]: + self.api_client.favorite(self.item["Id"], True) - elif selected == OPTIONS['RemoveFav']: - self.api_client.favorite(self.item['Id'], False) + elif selected == OPTIONS["RemoveFav"]: + self.api_client.favorite(self.item["Id"], False) - elif selected == OPTIONS['Addon']: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.jellyfin)') + elif selected == OPTIONS["Addon"]: + xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)") - elif selected == OPTIONS['Delete']: + elif selected == OPTIONS["Delete"]: self.delete_item() def delete_item(self): - if settings('skipContextMenu.bool') or dialog("yesno", "{jellyfin}", translate(33015)): - self.api_client.delete_item(self.item['Id']) + if settings("skipContextMenu.bool") or dialog( + "yesno", "{jellyfin}", translate(33015) + ): + self.api_client.delete_item(self.item["Id"]) def transcode(self): filename = xbmc.getInfoLabel("ListItem.Filenameandpath") diff --git a/jellyfin_kodi/entrypoint/default.py b/jellyfin_kodi/entrypoint/default.py index d8f6c049..9b4185f2 100644 --- a/jellyfin_kodi/entrypoint/default.py +++ b/jellyfin_kodi/entrypoint/default.py @@ -14,7 +14,16 @@ from kodi_six import xbmc, xbmcvfs, xbmcgui, xbmcplugin, xbmcaddon from .. import client from ..database import reset, get_sync, Database, jellyfin_db, get_credentials from ..objects import Objects, Actions -from ..helper import translate, event, settings, window, dialog, api, JSONRPC, LazyLogger +from ..helper import ( + translate, + event, + settings, + window, + dialog, + api, + JSONRPC, + LazyLogger, +) from ..helper.utils import JsonDebugPrinter, translate_path, kodi_version from ..jellyfin import Jellyfin @@ -35,10 +44,9 @@ except IndexError: class Events(object): def __init__(self): - - ''' Parse the parameters. Reroute to our service.py - where user is fully identified already. - ''' + """Parse the parameters. Reroute to our service.py + where user is fully identified already. + """ base_url = ADDON_BASE_URL path = QUERY_STRING @@ -47,142 +55,193 @@ class Events(object): except Exception: params = {} - mode = params.get('mode') - server = params.get('server') + mode = params.get("mode") + server = params.get("server") - if server == 'None': + if server == "None": server = None jellyfin_client = Jellyfin(server).get_client() api_client = jellyfin_client.jellyfin - addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json") + addon_data = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/data.json" + ) try: - with open(addon_data, 'rb') as infile: + with open(addon_data, "rb") as infile: data = json.load(infile) - server_data = data['Servers'][0] - api_client.config.data['auth.server'] = server_data.get('address') - api_client.config.data['auth.server-name'] = server_data.get('Name') - api_client.config.data['auth.user_id'] = server_data.get('UserId') - api_client.config.data['auth.token'] = server_data.get('AccessToken') + server_data = data["Servers"][0] + api_client.config.data["auth.server"] = server_data.get("address") + api_client.config.data["auth.server-name"] = server_data.get("Name") + api_client.config.data["auth.user_id"] = server_data.get("UserId") + api_client.config.data["auth.token"] = server_data.get("AccessToken") except Exception as e: - LOG.warning('Addon appears to not be configured yet: {}'.format(e)) + LOG.warning("Addon appears to not be configured yet: {}".format(e)) LOG.info("path: %s params: %s", path, JsonDebugPrinter(params)) - if '/extrafanart' in base_url: + if "/extrafanart" in base_url: jellyfin_path = path[1:] - jellyfin_id = params.get('id') + jellyfin_id = params.get("id") get_fanart(jellyfin_id, jellyfin_path, server, api_client) - elif '/Extras' in base_url or '/VideoFiles' in base_url: + elif "/Extras" in base_url or "/VideoFiles" in base_url: jellyfin_path = path[1:] - jellyfin_id = params.get('id') + jellyfin_id = params.get("id") get_video_extras(jellyfin_id, jellyfin_path, server, api_client) - elif mode == 'play': + elif mode == "play": - item = api_client.get_item(params['id']) + item = api_client.get_item(params["id"]) item["resumePlayback"] = sys.argv[3].split(":")[1] == "true" - Actions(server, api_client).play(item, params.get('dbid'), params.get('transcode') == 'true', playlist=params.get('playlist') == 'true') + Actions(server, api_client).play( + item, + params.get("dbid"), + params.get("transcode") == "true", + playlist=params.get("playlist") == "true", + ) - elif mode == 'playlist': - api_client.post_session(api_client.config.data['app.session'], "Playing", { - 'PlayCommand': "PlayNow", - 'ItemIds': params['id'], - 'StartPositionTicks': 0 - }) - elif mode == 'deviceid': + elif mode == "playlist": + api_client.post_session( + api_client.config.data["app.session"], + "Playing", + { + "PlayCommand": "PlayNow", + "ItemIds": params["id"], + "StartPositionTicks": 0, + }, + ) + elif mode == "deviceid": client.reset_device_id() - elif mode == 'reset': + elif mode == "reset": reset() - elif mode == 'delete': + elif mode == "delete": delete_item() - elif mode == 'refreshboxsets': - event('SyncLibrary', {'Id': "Boxsets:Refresh"}) - elif mode == 'nextepisodes': - get_next_episodes(params['id'], params['limit']) - elif mode == 'browse': - browse(params.get('type'), params.get('id'), params.get('folder'), server, api_client) - elif mode == 'synclib': - event('SyncLibrary', {'Id': params.get('id')}) - elif mode == 'updatelib': - event('SyncLibrary', {'Id': params.get('id'), 'Update': True}) - elif mode == 'repairlib': - event('RepairLibrary', {'Id': params.get('id')}) - elif mode == 'removelib': - event('RemoveLibrary', {'Id': params.get('id')}) - elif mode == 'repairlibs': - event('RepairLibrarySelection') - elif mode == 'updatelibs': - event('SyncLibrarySelection') - elif mode == 'removelibs': - event('RemoveLibrarySelection') - elif mode == 'addlibs': - event('AddLibrarySelection') - elif mode == 'addserver': - event('AddServer') - elif mode == 'login': - event('ServerConnect', {'Id': server}) - elif mode == 'removeserver': - event('RemoveServer', {'Id': server}) - elif mode == 'settings': - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.jellyfin)') - elif mode == 'adduser': + elif mode == "refreshboxsets": + event("SyncLibrary", {"Id": "Boxsets:Refresh"}) + elif mode == "nextepisodes": + get_next_episodes(params["id"], params["limit"]) + elif mode == "browse": + browse( + params.get("type"), + params.get("id"), + params.get("folder"), + server, + api_client, + ) + elif mode == "synclib": + event("SyncLibrary", {"Id": params.get("id")}) + elif mode == "updatelib": + event("SyncLibrary", {"Id": params.get("id"), "Update": True}) + elif mode == "repairlib": + event("RepairLibrary", {"Id": params.get("id")}) + elif mode == "removelib": + event("RemoveLibrary", {"Id": params.get("id")}) + elif mode == "repairlibs": + event("RepairLibrarySelection") + elif mode == "updatelibs": + event("SyncLibrarySelection") + elif mode == "removelibs": + event("RemoveLibrarySelection") + elif mode == "addlibs": + event("AddLibrarySelection") + elif mode == "addserver": + event("AddServer") + elif mode == "login": + event("ServerConnect", {"Id": server}) + elif mode == "removeserver": + event("RemoveServer", {"Id": server}) + elif mode == "settings": + xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)") + elif mode == "adduser": add_user(api_client) - elif mode == 'updatepassword': - event('UpdatePassword') - elif mode == 'thememedia': + elif mode == "updatepassword": + event("UpdatePassword") + elif mode == "thememedia": get_themes(api_client) - elif mode == 'managelibs': + elif mode == "managelibs": manage_libraries() - elif mode == 'backup': + elif mode == "backup": backup() - elif mode == 'restartservice': - window('jellyfin.restart.bool', True) - elif mode is None and not params and base_url != 'plugin://plugin.video.jellyfin/': + elif mode == "restartservice": + window("jellyfin.restart.bool", True) + elif ( + mode is None + and not params + and base_url != "plugin://plugin.video.jellyfin/" + ): # Used when selecting "Browse" from a context menu, see #548 - item_id = base_url.strip('/').split('/')[-1] - browse('', item_id, None, server, api_client) + item_id = base_url.strip("/").split("/")[-1] + browse("", item_id, None, server, api_client) else: listing() def listing(): - - ''' Display all jellyfin nodes and dynamic entries when appropriate. - ''' - total = int(window('Jellyfin.nodes.total') or 0) + """Display all jellyfin nodes and dynamic entries when appropriate.""" + total = int(window("Jellyfin.nodes.total") or 0) sync = get_sync() - whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] - servers = get_credentials()['Servers'][1:] + whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]] + servers = get_credentials()["Servers"][1:] for i in range(total): window_prop = "Jellyfin.nodes.%s" % i - path = window('%s.index' % window_prop) + path = window("%s.index" % window_prop) if not path: - path = window('%s.content' % window_prop) or window('%s.path' % window_prop) + path = window("%s.content" % window_prop) or window("%s.path" % window_prop) - label = window('%s.title' % window_prop) - node = window('%s.type' % window_prop) - artwork = window('%s.artwork' % window_prop) - view_id = window('%s.id' % window_prop) + label = window("%s.title" % window_prop) + node = window("%s.type" % window_prop) + artwork = window("%s.artwork" % window_prop) + view_id = window("%s.id" % window_prop) context = [] - if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed') and view_id not in whitelist: + if ( + view_id + and node in ("movies", "tvshows", "musicvideos", "music", "mixed") + and view_id not in whitelist + ): label = "%s %s" % (label, translate(33166)) - context.append((translate(33123), "RunPlugin(plugin://plugin.video.jellyfin/?mode=synclib&id=%s)" % view_id)) + context.append( + ( + translate(33123), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=synclib&id=%s)" + % view_id, + ) + ) - if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music') and view_id in whitelist: + if ( + view_id + and node in ("movies", "tvshows", "musicvideos", "music") + and view_id in whitelist + ): - context.append((translate(33136), "RunPlugin(plugin://plugin.video.jellyfin/?mode=updatelib&id=%s)" % view_id)) - context.append((translate(33132), "RunPlugin(plugin://plugin.video.jellyfin/?mode=repairlib&id=%s)" % view_id)) - context.append((translate(33133), "RunPlugin(plugin://plugin.video.jellyfin/?mode=removelib&id=%s)" % view_id)) + context.append( + ( + translate(33136), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=updatelib&id=%s)" + % view_id, + ) + ) + context.append( + ( + translate(33132), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=repairlib&id=%s)" + % view_id, + ) + ) + context.append( + ( + translate(33133), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=removelib&id=%s)" + % view_id, + ) + ) LOG.debug("--[ listing/%s/%s ] %s", node, label, path) @@ -192,33 +251,52 @@ def listing(): for server in servers: context = [] - if server.get('ManualAddress'): - context.append((translate(33141), "RunPlugin(plugin://plugin.video.jellyfin/?mode=removeserver&server=%s)" % server['Id'])) + if server.get("ManualAddress"): + context.append( + ( + translate(33141), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=removeserver&server=%s)" + % server["Id"], + ) + ) - if 'AccessToken' not in server: - directory("%s (%s)" % (server['Name'], translate(30539)), "plugin://plugin.video.jellyfin/?mode=login&server=%s" % server['Id'], False, context=context) + if "AccessToken" not in server: + directory( + "%s (%s)" % (server["Name"], translate(30539)), + "plugin://plugin.video.jellyfin/?mode=login&server=%s" % server["Id"], + False, + context=context, + ) else: - directory(server['Name'], "plugin://plugin.video.jellyfin/?mode=browse&server=%s" % server['Id'], context=context) + directory( + server["Name"], + "plugin://plugin.video.jellyfin/?mode=browse&server=%s" % server["Id"], + context=context, + ) directory(translate(33194), "plugin://plugin.video.jellyfin/?mode=managelibs", True) directory(translate(33134), "plugin://plugin.video.jellyfin/?mode=addserver", False) directory(translate(33054), "plugin://plugin.video.jellyfin/?mode=adduser", False) directory(translate(5), "plugin://plugin.video.jellyfin/?mode=settings", False) - directory(translate(33161), "plugin://plugin.video.jellyfin/?mode=updatepassword", False) + directory( + translate(33161), "plugin://plugin.video.jellyfin/?mode=updatepassword", False + ) directory(translate(33058), "plugin://plugin.video.jellyfin/?mode=reset", False) - directory(translate(33180), "plugin://plugin.video.jellyfin/?mode=restartservice", False) + directory( + translate(33180), "plugin://plugin.video.jellyfin/?mode=restartservice", False + ) - if settings('backupPath'): - directory(translate(33092), "plugin://plugin.video.jellyfin/?mode=backup", False) + if settings("backupPath"): + directory( + translate(33092), "plugin://plugin.video.jellyfin/?mode=backup", False + ) - xbmcplugin.setContent(PROCESS_HANDLE, 'files') + xbmcplugin.setContent(PROCESS_HANDLE, "files") xbmcplugin.endOfDirectory(PROCESS_HANDLE) def directory(label, path, folder=True, artwork=None, fanart=None, context=None): - - ''' Add directory listitem. context should be a list of tuples [(label, action)*] - ''' + """Add directory listitem. context should be a list of tuples [(label, action)*]""" li = dir_listitem(label, path, artwork, fanart) if context: @@ -230,44 +308,56 @@ def directory(label, path, folder=True, artwork=None, fanart=None, context=None) def dir_listitem(label, path, artwork=None, fanart=None): - - ''' Gets the icon paths for default node listings - ''' + """Gets the icon paths for default node listings""" li = xbmcgui.ListItem(label, path=path) - li.setArt({ - "thumb": artwork or "special://home/addons/plugin.video.jellyfin/resources/icon.png", - "fanart": fanart or "special://home/addons/plugin.video.jellyfin/resources/fanart.png", - "landscape": artwork or fanart or "special://home/addons/plugin.video.jellyfin/resources/fanart.png", - }) + li.setArt( + { + "thumb": artwork + or "special://home/addons/plugin.video.jellyfin/resources/icon.png", + "fanart": fanart + or "special://home/addons/plugin.video.jellyfin/resources/fanart.png", + "landscape": artwork + or fanart + or "special://home/addons/plugin.video.jellyfin/resources/fanart.png", + } + ) return li def manage_libraries(): - directory(translate(33098), "plugin://plugin.video.jellyfin/?mode=refreshboxsets", False) + directory( + translate(33098), "plugin://plugin.video.jellyfin/?mode=refreshboxsets", False + ) directory(translate(33154), "plugin://plugin.video.jellyfin/?mode=addlibs", False) - directory(translate(33139), "plugin://plugin.video.jellyfin/?mode=updatelibs", False) - directory(translate(33140), "plugin://plugin.video.jellyfin/?mode=repairlibs", False) - directory(translate(33184), "plugin://plugin.video.jellyfin/?mode=removelibs", False) - directory(translate(33060), "plugin://plugin.video.jellyfin/?mode=thememedia", False) + directory( + translate(33139), "plugin://plugin.video.jellyfin/?mode=updatelibs", False + ) + directory( + translate(33140), "plugin://plugin.video.jellyfin/?mode=repairlibs", False + ) + directory( + translate(33184), "plugin://plugin.video.jellyfin/?mode=removelibs", False + ) + directory( + translate(33060), "plugin://plugin.video.jellyfin/?mode=thememedia", False + ) - xbmcplugin.setContent(PROCESS_HANDLE, 'files') + xbmcplugin.setContent(PROCESS_HANDLE, "files") xbmcplugin.endOfDirectory(PROCESS_HANDLE) def browse(media, view_id=None, folder=None, server_id=None, api_client=None): - - ''' Browse content dynamically. - ''' + """Browse content dynamically.""" LOG.info("--[ v:%s/%s ] %s", view_id, media, folder) - if not window('jellyfin_online.bool') and server_id is None: + if not window("jellyfin_online.bool") and server_id is None: monitor = xbmc.Monitor() for _i in range(300): - if window('jellyfin_online.bool'): + if window("jellyfin_online.bool"): break elif monitor.waitForAbort(0.1): return @@ -278,140 +368,359 @@ def browse(media, view_id=None, folder=None, server_id=None, api_client=None): folder = folder.lower() if folder else None - if folder is None and media in ('homevideos', 'movies', 'books', 'audiobooks'): + if folder is None and media in ("homevideos", "movies", "books", "audiobooks"): return browse_subfolders(media, view_id, server_id) - if folder and folder == 'firstletter': + if folder and folder == "firstletter": return browse_letters(media, view_id, server_id) if view_id: view = api_client.get_item(view_id) - xbmcplugin.setPluginCategory(PROCESS_HANDLE, view['Name']) + xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"]) content_type = "files" - if media in ('tvshows', 'seasons', 'episodes', 'movies', 'musicvideos', 'songs', 'albums'): + if media in ( + "tvshows", + "seasons", + "episodes", + "movies", + "musicvideos", + "songs", + "albums", + ): content_type = media - elif media in ('homevideos', 'photos'): + elif media in ("homevideos", "photos"): content_type = "images" - elif media in ('books', 'audiobooks'): + elif media in ("books", "audiobooks"): content_type = "videos" - elif media == 'music': + elif media == "music": content_type = "artists" - if folder == 'recentlyadded': + if folder == "recentlyadded": listing = api_client.get_recently_added(None, view_id, None) - elif folder == 'genres': + elif folder == "genres": listing = api_client.get_genres(view_id) - elif media == 'livetv': + elif media == "livetv": listing = api_client.get_channels() - elif folder == 'unwatched': - listing = get_filtered_section(view_id, None, None, None, None, None, ['IsUnplayed'], None, server_id, api_client) - elif folder == 'favorite': - listing = get_filtered_section(view_id, None, None, None, None, None, ['IsFavorite'], None, server_id, api_client) - elif folder == 'inprogress': - listing = get_filtered_section(view_id, None, None, None, None, None, ['IsResumable'], None, server_id, api_client) - elif folder == 'boxsets': - listing = get_filtered_section(view_id, get_media_type('boxsets'), None, True, None, None, None, None, server_id, api_client) - elif folder == 'random': - listing = get_filtered_section(view_id, get_media_type(content_type), 25, True, "Random", None, None, None, server_id, api_client) - elif (folder or "").startswith('firstletter-'): - listing = get_filtered_section(view_id, get_media_type(content_type), None, None, None, None, None, {'NameStartsWith': folder.split('-')[1]}, server_id, api_client) - elif (folder or "").startswith('genres-'): - listing = get_filtered_section(view_id, get_media_type(content_type), None, None, None, None, None, {'GenreIds': folder.split('-')[1]}, server_id, api_client) - elif folder == 'favepisodes': - listing = get_filtered_section(None, get_media_type(content_type), 25, None, None, None, ['IsFavorite'], None, server_id, api_client) - elif folder and media == 'playlists': - listing = get_filtered_section(folder, get_media_type(content_type), None, False, 'None', None, None, None, server_id, api_client) - elif media == 'homevideos': - listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, False, None, None, None, None, server_id, api_client) - elif media in ['movies', 'episodes']: - listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, True, None, None, None, None, server_id, api_client) - elif media in ('boxset', 'library'): - listing = get_filtered_section(folder or view_id, None, None, True, None, None, None, None, server_id, api_client) - elif media == 'boxsets': - listing = get_filtered_section(folder or view_id, None, None, False, None, None, ['Boxsets'], None, server_id, api_client) - elif media == 'tvshows': - listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, True, None, None, None, None, server_id, api_client) - elif media == 'seasons': + elif folder == "unwatched": + listing = get_filtered_section( + view_id, + None, + None, + None, + None, + None, + ["IsUnplayed"], + None, + server_id, + api_client, + ) + elif folder == "favorite": + listing = get_filtered_section( + view_id, + None, + None, + None, + None, + None, + ["IsFavorite"], + None, + server_id, + api_client, + ) + elif folder == "inprogress": + listing = get_filtered_section( + view_id, + None, + None, + None, + None, + None, + ["IsResumable"], + None, + server_id, + api_client, + ) + elif folder == "boxsets": + listing = get_filtered_section( + view_id, + get_media_type("boxsets"), + None, + True, + None, + None, + None, + None, + server_id, + api_client, + ) + elif folder == "random": + listing = get_filtered_section( + view_id, + get_media_type(content_type), + 25, + True, + "Random", + None, + None, + None, + server_id, + api_client, + ) + elif (folder or "").startswith("firstletter-"): + listing = get_filtered_section( + view_id, + get_media_type(content_type), + None, + None, + None, + None, + None, + {"NameStartsWith": folder.split("-")[1]}, + server_id, + api_client, + ) + elif (folder or "").startswith("genres-"): + listing = get_filtered_section( + view_id, + get_media_type(content_type), + None, + None, + None, + None, + None, + {"GenreIds": folder.split("-")[1]}, + server_id, + api_client, + ) + elif folder == "favepisodes": + listing = get_filtered_section( + None, + get_media_type(content_type), + 25, + None, + None, + None, + ["IsFavorite"], + None, + server_id, + api_client, + ) + elif folder and media == "playlists": + listing = get_filtered_section( + folder, + get_media_type(content_type), + None, + False, + "None", + None, + None, + None, + server_id, + api_client, + ) + elif media == "homevideos": + listing = get_filtered_section( + folder or view_id, + get_media_type(content_type), + None, + False, + None, + None, + None, + None, + server_id, + api_client, + ) + elif media in ["movies", "episodes"]: + listing = get_filtered_section( + folder or view_id, + get_media_type(content_type), + None, + True, + None, + None, + None, + None, + server_id, + api_client, + ) + elif media in ("boxset", "library"): + listing = get_filtered_section( + folder or view_id, + None, + None, + True, + None, + None, + None, + None, + server_id, + api_client, + ) + elif media == "boxsets": + listing = get_filtered_section( + folder or view_id, + None, + None, + False, + None, + None, + ["Boxsets"], + None, + server_id, + api_client, + ) + elif media == "tvshows": + listing = get_filtered_section( + folder or view_id, + get_media_type(content_type), + None, + True, + None, + None, + None, + None, + server_id, + api_client, + ) + elif media == "seasons": listing = api_client.get_seasons(folder) - elif media != 'files': - listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, False, None, None, None, None, server_id, api_client) + elif media != "files": + listing = get_filtered_section( + folder or view_id, + get_media_type(content_type), + None, + False, + None, + None, + None, + None, + server_id, + api_client, + ) else: - listing = get_filtered_section(folder or view_id, None, None, False, None, None, None, None, server_id, api_client) + listing = get_filtered_section( + folder or view_id, + None, + None, + False, + None, + None, + None, + None, + server_id, + api_client, + ) if listing: actions = Actions(server_id, api_client) list_li = [] - listing = listing if type(listing) == list else listing.get('Items', []) + listing = listing if type(listing) == list else listing.get("Items", []) for item in listing: li = xbmcgui.ListItem() - li.setProperty('jellyfinid', item['Id']) - li.setProperty('jellyfinserver', server_id) + li.setProperty("jellyfinid", item["Id"]) + li.setProperty("jellyfinserver", server_id) actions.set_listitem(item, li) - if item.get('IsFolder'): + if item.get("IsFolder"): params = { - 'id': view_id or item['Id'], - 'mode': "browse", - 'type': get_folder_type(item, media) or media, - 'folder': item['Id'], - 'server': server_id + "id": view_id or item["Id"], + "mode": "browse", + "type": get_folder_type(item, media) or media, + "folder": item["Id"], + "server": server_id, } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) context = [] - if item['Type'] in ('Series', 'Season', 'Playlist'): - context.append(("Play", "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))) + if item["Type"] in ("Series", "Season", "Playlist"): + context.append( + ( + "Play", + "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ) - if item['UserData']['Played']: - context.append((translate(16104), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) + if item["UserData"]["Played"]: + context.append( + ( + translate(16104), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ) else: - context.append((translate(16103), "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) + context.append( + ( + translate(16103), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ) li.addContextMenuItems(context) list_li.append((path, li, True)) - elif item['Type'] == 'Genre': + elif item["Type"] == "Genre": params = { - 'id': view_id or item['Id'], - 'mode': "browse", - 'type': get_folder_type(item, media) or media, - 'folder': 'genres-%s' % item['Id'], - 'server': server_id + "id": view_id or item["Id"], + "mode": "browse", + "type": get_folder_type(item, media) or media, + "folder": "genres-%s" % item["Id"], + "server": server_id, } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) list_li.append((path, li, True)) else: - if item['Type'] not in ('Photo', 'PhotoAlbum'): - params = { - 'id': item['Id'], - 'mode': "play", - 'server': server_id - } - path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) - li.setProperty('path', path) - context = [(translate(13412), "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))] + if item["Type"] not in ("Photo", "PhotoAlbum"): + params = {"id": item["Id"], "mode": "play", "server": server_id} + path = "%s?%s" % ( + "plugin://plugin.video.jellyfin/", + urlencode(params), + ) + li.setProperty("path", path) + context = [ + ( + translate(13412), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ] - if item['UserData']['Played']: - context.append((translate(16104), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) + if item["UserData"]["Played"]: + context.append( + ( + translate(16104), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ) else: - context.append((translate(16103), "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) + context.append( + ( + translate(16103), + "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" + % (item["Id"], server_id), + ) + ) li.addContextMenuItems(context) - list_li.append((li.getProperty('path'), li, False)) + list_li.append((li.getProperty("path"), li, False)) xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li)) - if content_type == 'images': + if content_type == "images": xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_TITLE) xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RATING) @@ -422,99 +731,94 @@ def browse(media, view_id=None, folder=None, server_id=None, api_client=None): def browse_subfolders(media, view_id, server_id=None): - - ''' Display submenus for jellyfin views. - ''' + """Display submenus for jellyfin views.""" from ..views import DYNNODES view = Jellyfin(server_id).get_client().jellyfin.get_item(view_id) - xbmcplugin.setPluginCategory(PROCESS_HANDLE, view['Name']) + xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"]) nodes = DYNNODES[media] for node in nodes: params = { - 'id': view_id, - 'mode': "browse", - 'type': media, - 'folder': view_id if node[0] == 'all' else node[0], - 'server': server_id + "id": view_id, + "mode": "browse", + "type": media, + "folder": view_id if node[0] == "all" else node[0], + "server": server_id, } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) - directory(node[1] or view['Name'], path) + directory(node[1] or view["Name"], path) - xbmcplugin.setContent(PROCESS_HANDLE, 'files') + xbmcplugin.setContent(PROCESS_HANDLE, "files") xbmcplugin.endOfDirectory(PROCESS_HANDLE) def browse_letters(media, view_id, server_id=None): - - ''' Display letters as options. - ''' + """Display letters as options.""" letters = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ" view = Jellyfin(server_id).get_client().jellyfin.get_item(view_id) - xbmcplugin.setPluginCategory(PROCESS_HANDLE, view['Name']) + xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"]) for node in letters: params = { - 'id': view_id, - 'mode': "browse", - 'type': media, - 'folder': 'firstletter-%s' % node, - 'server': server_id + "id": view_id, + "mode": "browse", + "type": media, + "folder": "firstletter-%s" % node, + "server": server_id, } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) directory(node, path) - xbmcplugin.setContent(PROCESS_HANDLE, 'files') + xbmcplugin.setContent(PROCESS_HANDLE, "files") xbmcplugin.endOfDirectory(PROCESS_HANDLE) def get_folder_type(item, content_type=None): - media = item['Type'] + media = item["Type"] - if media == 'Series': + if media == "Series": return "seasons" - elif media == 'Season': + elif media == "Season": return "episodes" - elif media == 'BoxSet': + elif media == "BoxSet": return "boxset" - elif media == 'MusicArtist': + elif media == "MusicArtist": return "albums" - elif media == 'MusicAlbum': + elif media == "MusicAlbum": return "songs" - elif media == 'CollectionFolder': - return item.get('CollectionType', 'library') - elif media == 'Folder' and content_type == 'music': + elif media == "CollectionFolder": + return item.get("CollectionType", "library") + elif media == "Folder" and content_type == "music": return "albums" def get_media_type(media): - if media == 'movies': + if media == "movies": return "Movie,BoxSet" - elif media == 'homevideos': + elif media == "homevideos": return "Video,Folder,PhotoAlbum,Photo" - elif media == 'episodes': + elif media == "episodes": return "Episode" - elif media == 'boxsets': + elif media == "boxsets": return "BoxSet" - elif media == 'tvshows': + elif media == "tvshows": return "Series" - elif media == 'music': + elif media == "music": return "MusicArtist,MusicAlbum,Audio" def get_fanart(item_id, path, server_id=None, api_client=None): - - ''' Get extra fanart for listitems. This is called by skinhelper. - Images are stored locally, due to the Kodi caching system. - ''' - if not item_id and 'plugin.video.jellyfin' in path: - item_id = path.split('/')[-2] + """Get extra fanart for listitems. This is called by skinhelper. + Images are stored locally, due to the Kodi caching system. + """ + if not item_id and "plugin.video.jellyfin" in path: + item_id = path.split("/")[-2] if not item_id: return @@ -528,9 +832,9 @@ def get_fanart(item_id, path, server_id=None, api_client=None): xbmcvfs.mkdirs(directory) item = api_client.get_item(item_id) - obj = objects.map(item, 'Artwork') + obj = objects.map(item, "Artwork") backdrops = api.API(item).get_all_artwork(obj) - tags = obj['BackdropTags'] + tags = obj["BackdropTags"] for index, backdrop in enumerate(backdrops): @@ -553,12 +857,11 @@ def get_fanart(item_id, path, server_id=None, api_client=None): def get_video_extras(item_id, path, server_id=None, api_client=None): - - ''' Returns the video files for the item as plugin listing, can be used - to browse actual files or video extras, etc. - ''' - if not item_id and 'plugin.video.jellyfin' in path: - item_id = path.split('/')[-2] + """Returns the video files for the item as plugin listing, can be used + to browse actual files or video extras, etc. + """ + if not item_id and "plugin.video.jellyfin" in path: + item_id = path.split("/")[-2] if not item_id: return @@ -595,10 +898,8 @@ def get_video_extras(item_id, path, server_id=None, api_client=None): def get_next_episodes(item_id, limit): - - ''' Only for synced content. - ''' - with Database('jellyfin') as jellyfindb: + """Only for synced content.""" + with Database("jellyfin") as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) library = db.get_view_name(item_id) @@ -606,145 +907,173 @@ def get_next_episodes(item_id, limit): if not library: return - result = JSONRPC('VideoLibrary.GetTVShows').execute({ - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % library} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - }) + result = JSONRPC("VideoLibrary.GetTVShows").execute( + { + "sort": {"order": "descending", "method": "lastplayed"}, + "filter": { + "and": [ + {"operator": "true", "field": "inprogress", "value": ""}, + {"operator": "is", "field": "tag", "value": "%s" % library}, + ] + }, + "properties": ["title", "studio", "mpaa", "file", "art"], + } + ) try: - items = result['result']['tvshows'] + items = result["result"]["tvshows"] except (KeyError, TypeError): return list_li = [] for item in items: - if settings('ignoreSpecialsNextEpisodes.bool'): + if settings("ignoreSpecialsNextEpisodes.bool"): params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - {'operator': "greaterthan", 'field': "season", 'value': "0"} - ]}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" + "tvshowid": item["tvshowid"], + "sort": {"method": "episode"}, + "filter": { + "and": [ + {"operator": "lessthan", "field": "playcount", "value": "1"}, + {"operator": "greaterthan", "field": "season", "value": "0"}, + ] + }, + "properties": [ + "title", + "playcount", + "season", + "episode", + "showtitle", + "plot", + "file", + "rating", + "resume", + "tvshowid", + "art", + "streamdetails", + "firstaired", + "runtime", + "writer", + "dateadded", + "lastplayed", ], - 'limits': {"end": 1} + "limits": {"end": 1}, } else: params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" + "tvshowid": item["tvshowid"], + "sort": {"method": "episode"}, + "filter": {"operator": "lessthan", "field": "playcount", "value": "1"}, + "properties": [ + "title", + "playcount", + "season", + "episode", + "showtitle", + "plot", + "file", + "rating", + "resume", + "tvshowid", + "art", + "streamdetails", + "firstaired", + "runtime", + "writer", + "dateadded", + "lastplayed", ], - 'limits': {"end": 1} + "limits": {"end": 1}, } - result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) + result = JSONRPC("VideoLibrary.GetEpisodes").execute(params) try: - episodes = result['result']['episodes'] + episodes = result["result"]["episodes"] except (KeyError, TypeError): pass else: for episode in episodes: li = create_listitem(episode) - list_li.append((episode['file'], li)) + list_li.append((episode["file"], li)) if len(list_li) == limit: break xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li)) - xbmcplugin.setContent(PROCESS_HANDLE, 'episodes') + xbmcplugin.setContent(PROCESS_HANDLE, "episodes") xbmcplugin.endOfDirectory(PROCESS_HANDLE) def create_listitem(item): - - ''' Listitem based on jsonrpc items. - ''' - title = item['title'] + """Listitem based on jsonrpc items.""" + title = item["title"] label2 = "" li = xbmcgui.ListItem(title) - li.setProperty('IsPlayable', "true") + li.setProperty("IsPlayable", "true") metadata = { - 'Title': title, - 'duration': str(item['runtime'] / 60), - 'Plot': item['plot'], - 'Playcount': item['playcount'] + "Title": title, + "duration": str(item["runtime"] / 60), + "Plot": item["plot"], + "Playcount": item["playcount"], } if "showtitle" in item: - metadata['TVshowTitle'] = item['showtitle'] - label2 = item['showtitle'] + metadata["TVshowTitle"] = item["showtitle"] + label2 = item["showtitle"] if "episodeid" in item: # Listitem of episode - metadata['mediatype'] = "episode" - metadata['dbid'] = item['episodeid'] + metadata["mediatype"] = "episode" + metadata["dbid"] = item["episodeid"] # TODO: Review once Krypton is RC - probably no longer needed if there's dbid if "episode" in item: - episode = item['episode'] - metadata['Episode'] = episode + episode = item["episode"] + metadata["Episode"] = episode if "season" in item: - season = item['season'] - metadata['Season'] = season + season = item["season"] + metadata["Season"] = season if season and episode: episodeno = "s%.2de%.2d" % (season, episode) - li.setProperty('episodeno', episodeno) + li.setProperty("episodeno", episodeno) label2 = "%s - %s" % (label2, episodeno) if label2 else episodeno if "firstaired" in item: - metadata['Premiered'] = item['firstaired'] + metadata["Premiered"] = item["firstaired"] if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']), 1)) + metadata["Rating"] = str(round(float(item["rating"]), 1)) if "director" in item: - metadata['Director'] = " / ".join(item['director']) + metadata["Director"] = " / ".join(item["director"]) if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) + metadata["Writer"] = " / ".join(item["writer"]) if "cast" in item: cast = [] castandrole = [] - for person in item['cast']: - name = person['name'] + for person in item["cast"]: + name = person["name"] cast.append(name) - castandrole.append((name, person['role'])) - metadata['Cast'] = cast - metadata['CastAndRole'] = castandrole + castandrole.append((name, person["role"])) + metadata["Cast"] = cast + metadata["CastAndRole"] = castandrole li.setLabel2(label2) li.setInfo(type="Video", infoLabels=metadata) - li.setProperty('resumetime', str(item['resume']['position'])) - li.setProperty('totaltime', str(item['resume']['total'])) - li.setArt(item['art']) - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart', '')) + li.setProperty("resumetime", str(item["resume"]["position"])) + li.setProperty("totaltime", str(item["resume"]["total"])) + li.setArt(item["art"]) + li.setProperty("dbid", str(item["episodeid"])) + li.setProperty("fanart_image", item["art"].get("tvshow.fanart", "")) - for key, value in iteritems(item['streamdetails']): + for key, value in iteritems(item["streamdetails"]): for stream in value: li.addStreamInfo(key, stream) @@ -752,87 +1081,98 @@ def create_listitem(item): def add_user(api_client): - - ''' Add or remove users from the default server session. - ''' - if not window('jellyfin_online.bool'): + """Add or remove users from the default server session.""" + if not window("jellyfin_online.bool"): return session = api_client.get_device(client.get_device_id()) users = api_client.get_users() - current = session[0]['AdditionalUsers'] + current = session[0]["AdditionalUsers"] - result = dialog("select", translate(33061), [translate(33062), translate(33063)] if current else [translate(33062)]) + result = dialog( + "select", + translate(33061), + [translate(33062), translate(33063)] if current else [translate(33062)], + ) if result < 0: return if not result: # Add user - eligible = [x for x in users if x['Id'] not in [current_user['UserId'] for current_user in current]] - resp = dialog("select", translate(33064), [x['Name'] for x in eligible]) + eligible = [ + x + for x in users + if x["Id"] not in [current_user["UserId"] for current_user in current] + ] + resp = dialog("select", translate(33064), [x["Name"] for x in eligible]) if resp < 0: return user = eligible[resp] - event('AddUser', {'Id': user['Id'], 'Add': True}) + event("AddUser", {"Id": user["Id"], "Add": True}) else: # Remove user - resp = dialog("select", translate(33064), [x['UserName'] for x in current]) + resp = dialog("select", translate(33064), [x["UserName"] for x in current]) if resp < 0: return user = current[resp] - event('AddUser', {'Id': user['UserId'], 'Add': False}) + event("AddUser", {"Id": user["UserId"], "Add": False}) def get_themes(api_client): - - ''' Add theme media locally, via strm. This is only for tv tunes. - If another script is used, adjust this code. - ''' + """Add theme media locally, via strm. This is only for tv tunes. + If another script is used, adjust this code. + """ from ..helper.utils import normalize_string from ..helper.playutils import PlayUtils from ..helper.xmls import tvtunes_nfo - library = translate_path("special://profile/addon_data/plugin.video.jellyfin/library") - play = settings('useDirectPaths') == "1" + library = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/library" + ) + play = settings("useDirectPaths") == "1" - if not xbmcvfs.exists(library + '/'): + if not xbmcvfs.exists(library + "/"): xbmcvfs.mkdir(library) - if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): + if xbmc.getCondVisibility("System.HasAddon(script.tvtunes)"): tvtunes = xbmcaddon.Addon(id="script.tvtunes") - tvtunes.setSetting('custom_path_enable', "true") - tvtunes.setSetting('custom_path', library) + tvtunes.setSetting("custom_path_enable", "true") + tvtunes.setSetting("custom_path", library) LOG.info("TV Tunes custom path is enabled and set.") else: dialog("ok", "{jellyfin}", translate(33152)) return - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: all_views = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() - views = [x.view_id for x in all_views if x.media_type in ('movies', 'tvshows', 'mixed')] + views = [ + x.view_id + for x in all_views + if x.media_type in ("movies", "tvshows", "mixed") + ] items = {} - server = api_client.config.data['auth.server'] + server = api_client.config.data["auth.server"] for view in views: result = api_client.get_items_theme_video(view) - for item in result['Items']: + for item in result["Items"]: - folder = normalize_string(item['Name']) - items[item['Id']] = folder + folder = normalize_string(item["Name"]) + items[item["Id"]] = folder result = api_client.get_items_theme_song(view) - for item in result['Items']: + for item in result["Items"]: - folder = normalize_string(item['Name']) - items[item['Id']] = folder + folder = normalize_string(item["Name"]) + items[item["Id"]] = folder for item in items: @@ -845,36 +1185,44 @@ def get_themes(api_client): themes = api_client.get_themes(item) paths = [] - for theme in themes['ThemeVideosResult']['Items'] + themes['ThemeSongsResult']['Items']: + for theme in ( + themes["ThemeVideosResult"]["Items"] + themes["ThemeSongsResult"]["Items"] + ): putils = PlayUtils(theme, False, None, server, api_client) if play: - paths.append(putils.direct_play(theme['MediaSources'][0])) + paths.append(putils.direct_play(theme["MediaSources"][0])) else: - paths.append(putils.direct_url(theme['MediaSources'][0])) + paths.append(putils.direct_url(theme["MediaSources"][0])) tvtunes_nfo(nfo_file, paths) - dialog("notification", heading="{jellyfin}", message=translate(33153), icon="{jellyfin}", time=1000, sound=False) + dialog( + "notification", + heading="{jellyfin}", + message=translate(33153), + icon="{jellyfin}", + time=1000, + sound=False, + ) def delete_item(): - - ''' Delete keymap action. - ''' + """Delete keymap action.""" from . import context context.Context(delete=True) def backup(): - - ''' Jellyfin backup. - ''' + """Jellyfin backup.""" from ..helper.utils import delete_folder, copytree - path = settings('backupPath') - folder_name = "Kodi%s.%s" % (kodi_version(), xbmc.getInfoLabel('System.Date(dd-mm-yy)')) + path = settings("backupPath") + folder_name = "Kodi%s.%s" % ( + kodi_version(), + xbmc.getInfoLabel("System.Date(dd-mm-yy)"), + ) folder_name = dialog("input", heading=translate(33089), defaultt=folder_name) if not folder_name: @@ -882,7 +1230,7 @@ def backup(): backup = os.path.join(path, folder_name) - if xbmcvfs.exists(backup + '/'): + if xbmcvfs.exists(backup + "/"): if not dialog("yesno", "{jellyfin}", translate(33090)): return backup() @@ -896,7 +1244,13 @@ def backup(): if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases): LOG.info("Unable to create all directories") - dialog("notification", heading="{jellyfin}", icon="{jellyfin}", message=translate(33165), sound=False) + dialog( + "notification", + heading="{jellyfin}", + icon="{jellyfin}", + message=translate(33165), + sound=False, + ) return @@ -904,19 +1258,19 @@ def backup(): databases = Objects().objects - db = translate_path(databases['jellyfin']) - xbmcvfs.copy(db, os.path.join(destination_databases, db.rsplit('\\', 1)[1])) + db = translate_path(databases["jellyfin"]) + xbmcvfs.copy(db, os.path.join(destination_databases, db.rsplit("\\", 1)[1])) LOG.info("copied jellyfin.db") - db = translate_path(databases['video']) - filename = db.rsplit('\\', 1)[1] + db = translate_path(databases["video"]) + filename = db.rsplit("\\", 1)[1] xbmcvfs.copy(db, os.path.join(destination_databases, filename)) LOG.info("copied %s", filename) - if settings('enableMusic.bool'): + if settings("enableMusic.bool"): - db = translate_path(databases['music']) - filename = db.rsplit('\\', 1)[1] + db = translate_path(databases["music"]) + filename = db.rsplit("\\", 1)[1] xbmcvfs.copy(db, os.path.join(destination_databases, filename)) LOG.info("copied %s", filename) @@ -924,35 +1278,43 @@ def backup(): dialog("ok", "{jellyfin}", "%s %s" % (translate(33091), backup)) -def get_filtered_section(parent_id=None, media=None, limit=None, recursive=None, sort=None, sort_order=None, - filters=None, extra=None, server_id=None, api_client=None): - - ''' Get dynamic listings. - ''' +def get_filtered_section( + parent_id=None, + media=None, + limit=None, + recursive=None, + sort=None, + sort_order=None, + filters=None, + extra=None, + server_id=None, + api_client=None, +): + """Get dynamic listings.""" params = { - 'ParentId': parent_id, - 'IncludeItemTypes': media, - 'IsMissing': False, - 'Recursive': recursive if recursive is not None else True, - 'Limit': limit, - 'SortBy': sort or "SortName", - 'SortOrder': sort_order or "Ascending", - 'ImageTypeLimit': 1, - 'IsVirtualUnaired': False, - 'Fields': browse_info() + "ParentId": parent_id, + "IncludeItemTypes": media, + "IsMissing": False, + "Recursive": recursive if recursive is not None else True, + "Limit": limit, + "SortBy": sort or "SortName", + "SortOrder": sort_order or "Ascending", + "ImageTypeLimit": 1, + "IsVirtualUnaired": False, + "Fields": browse_info(), } if filters: - if 'Boxsets' in filters: - filters.remove('Boxsets') - params['CollapseBoxSetItems'] = settings('groupedSets.bool') + if "Boxsets" in filters: + filters.remove("Boxsets") + params["CollapseBoxSetItems"] = settings("groupedSets.bool") - params['Filters'] = ','.join(filters) + params["Filters"] = ",".join(filters) - if settings('getCast.bool'): - params['Fields'] += ",People" + if settings("getCast.bool"): + params["Fields"] += ",People" - if media and 'Photo' in media: - params['Fields'] += ",Width,Height" + if media and "Photo" in media: + params["Fields"] += ",Width,Height" if extra is not None: params.update(extra) diff --git a/jellyfin_kodi/entrypoint/service.py b/jellyfin_kodi/entrypoint/service.py index 8c3b6aff..aaf0d03d 100644 --- a/jellyfin_kodi/entrypoint/service.py +++ b/jellyfin_kodi/entrypoint/service.py @@ -18,7 +18,15 @@ from .. import client from .. import library from .. import monitor from ..views import Views -from ..helper import translate, window, settings, event, dialog, set_addon_mode, LazyLogger +from ..helper import ( + translate, + window, + settings, + event, + dialog, + set_addon_mode, + LazyLogger, +) from ..helper.utils import JsonDebugPrinter, translate_path from ..helper.xmls import verify_kodi_defaults from ..jellyfin import Jellyfin @@ -37,83 +45,98 @@ class Service(xbmc.Monitor): monitor = None play_event = None warn = True - settings = {'last_progress': datetime.today(), 'last_progress_report': datetime.today()} + settings = { + "last_progress": datetime.today(), + "last_progress_report": datetime.today(), + } def __init__(self): - window('jellyfin_should_stop', clear=True) + window("jellyfin_should_stop", clear=True) - self.settings['addon_version'] = client.get_version() - self.settings['profile'] = translate_path('special://profile') - self.settings['mode'] = settings('useDirectPaths') - self.settings['log_level'] = settings('logLevel') or "1" - self.settings['auth_check'] = True - self.settings['enable_context'] = settings('enableContext.bool') - self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') - self.settings['kodi_companion'] = settings('kodiCompanion.bool') - window('jellyfin_kodiProfile', value=self.settings['profile']) - settings('platformDetected', client.get_platform()) + self.settings["addon_version"] = client.get_version() + self.settings["profile"] = translate_path("special://profile") + self.settings["mode"] = settings("useDirectPaths") + self.settings["log_level"] = settings("logLevel") or "1" + self.settings["auth_check"] = True + self.settings["enable_context"] = settings("enableContext.bool") + self.settings["enable_context_transcode"] = settings( + "enableContextTranscode.bool" + ) + self.settings["kodi_companion"] = settings("kodiCompanion.bool") + window("jellyfin_kodiProfile", value=self.settings["profile"]) + settings("platformDetected", client.get_platform()) - if self.settings['enable_context']: - window('jellyfin_context.bool', True) - if self.settings['enable_context_transcode']: - window('jellyfin_context_transcode.bool', True) + if self.settings["enable_context"]: + window("jellyfin_context.bool", True) + if self.settings["enable_context_transcode"]: + window("jellyfin_context_transcode.bool", True) LOG.info("--->>>[ %s ]", client.get_addon_name()) LOG.info("Version: %s", client.get_version()) - LOG.info("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) - LOG.info("Platform: %s", settings('platformDetected')) + LOG.info("KODI Version: %s", xbmc.getInfoLabel("System.BuildVersion")) + LOG.info("Platform: %s", settings("platformDetected")) LOG.info("Python Version: %s", sys.version) - LOG.info("Using dynamic paths: %s", settings('useDirectPaths') == "0") - LOG.info("Log Level: %s", self.settings['log_level']) + LOG.info("Using dynamic paths: %s", settings("useDirectPaths") == "0") + LOG.info("Log Level: %s", self.settings["log_level"]) verify_kodi_defaults() - window('jellyfin.connected.bool', True) - settings('groupedSets.bool', objects.utils.get_grouped_set()) + window("jellyfin.connected.bool", True) + settings("groupedSets.bool", objects.utils.get_grouped_set()) xbmc.Monitor.__init__(self) def service(self): + """Keeps the service monitor going. + Exit on Kodi shutdown or profile switch. - ''' Keeps the service monitor going. - Exit on Kodi shutdown or profile switch. - - if profile switch happens more than once, - Threads depending on abortRequest will not trigger. - ''' + if profile switch happens more than once, + Threads depending on abortRequest will not trigger. + """ self.monitor = monitor.Monitor() player = self.monitor.player self.connect = connect.Connect() self.start_default() - self.settings['mode'] = settings('useDirectPaths') + self.settings["mode"] = settings("useDirectPaths") while self.running: - if window('jellyfin_online.bool'): + if window("jellyfin_online.bool"): - if self.settings['profile'] != window('jellyfin_kodiProfile'): - LOG.info("[ profile switch ] %s", self.settings['profile']) + if self.settings["profile"] != window("jellyfin_kodiProfile"): + LOG.info("[ profile switch ] %s", self.settings["profile"]) break - if player.isPlaying() and player.is_playing_file(player.get_playing_file()): - difference = datetime.today() - self.settings['last_progress'] + if player.isPlaying() and player.is_playing_file( + player.get_playing_file() + ): + difference = datetime.today() - self.settings["last_progress"] if difference.seconds > 10: - self.settings['last_progress'] = datetime.today() + self.settings["last_progress"] = datetime.today() - update = (datetime.today() - self.settings['last_progress_report']).seconds > 250 - event('ReportProgressRequested', {'Report': update}) + update = ( + datetime.today() - self.settings["last_progress_report"] + ).seconds > 250 + event("ReportProgressRequested", {"Report": update}) if update: - self.settings['last_progress_report'] = datetime.today() + self.settings["last_progress_report"] = datetime.today() - if window('jellyfin.restart.bool'): + if window("jellyfin.restart.bool"): - window('jellyfin.restart', clear=True) - dialog("notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False) + window("jellyfin.restart", clear=True) + dialog( + "notification", + heading="{jellyfin}", + message=translate(33193), + icon="{jellyfin}", + time=1000, + sound=False, + ) - raise Exception('RestartService') + raise Exception("RestartService") if self.waitForAbort(1): break @@ -126,14 +149,14 @@ class Service(xbmc.Monitor): try: self.connect.register() - if not settings('SyncInstallRunDone.bool'): + if not settings("SyncInstallRunDone.bool"): set_addon_mode() except Exception as error: LOG.exception(error) def stop_default(self): - window('jellyfin_online', clear=True) + window("jellyfin_online", clear=True) Jellyfin().close() if self.library_thread is not None: @@ -142,59 +165,93 @@ class Service(xbmc.Monitor): self.library_thread = None def onNotification(self, sender, method, data): - - ''' All notifications are sent via NotifyAll built-in or Kodi. - Central hub. - ''' - if sender.lower() not in ('plugin.video.jellyfin', 'xbmc'): + """All notifications are sent via NotifyAll built-in or Kodi. + Central hub. + """ + if sender.lower() not in ("plugin.video.jellyfin", "xbmc"): return - if sender == 'plugin.video.jellyfin': - method = method.split('.')[1] + if sender == "plugin.video.jellyfin": + method = method.split(".")[1] - if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect', - 'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', - 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer', - 'Unauthorized', 'UserConfigurationUpdated', 'ServerRestarting', - 'RemoveServer', 'UpdatePassword', 'AddLibrarySelection', 'RemoveLibrarySelection'): + if method not in ( + "ServerUnreachable", + "ServerShuttingDown", + "UserDataChanged", + "ServerConnect", + "LibraryChanged", + "ServerOnline", + "SyncLibrary", + "RepairLibrary", + "RemoveLibrary", + "SyncLibrarySelection", + "RepairLibrarySelection", + "AddServer", + "Unauthorized", + "UserConfigurationUpdated", + "ServerRestarting", + "RemoveServer", + "UpdatePassword", + "AddLibrarySelection", + "RemoveLibrarySelection", + ): return data = json.loads(data)[0] else: - if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'): + if method not in ("System.OnQuit", "System.OnSleep", "System.OnWake"): return data = json.loads(data) LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data)) - if method == 'ServerOnline': - if data.get('ServerId') is None: + if method == "ServerOnline": + if data.get("ServerId") is None: - window('jellyfin_online.bool', True) - self.settings['auth_check'] = True + window("jellyfin_online.bool", True) + self.settings["auth_check"] = True self.warn = True - if settings('connectMsg.bool'): + if settings("connectMsg.bool"): - users = [user for user in (settings('additionalUsers') or "").split(',') if user] - users.insert(0, settings('username')) - dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33000), ", ".join(users)), - icon="{jellyfin}", time=1500, sound=False) + users = [ + user + for user in (settings("additionalUsers") or "").split(",") + if user + ] + users.insert(0, settings("username")) + dialog( + "notification", + heading="{jellyfin}", + message="%s %s" % (translate(33000), ", ".join(users)), + icon="{jellyfin}", + time=1500, + sound=False, + ) if self.library_thread is None: self.library_thread = library.Library(self) self.library_thread.start() - elif method in ('ServerUnreachable', 'ServerShuttingDown'): + elif method in ("ServerUnreachable", "ServerShuttingDown"): - if self.warn or data.get('ServerId'): + if self.warn or data.get("ServerId"): - self.warn = data.get('ServerId') is not None - dialog("notification", heading="{jellyfin}", message=translate(33146) if data.get('ServerId') is None else translate(33149), icon=xbmcgui.NOTIFICATION_ERROR) + self.warn = data.get("ServerId") is not None + dialog( + "notification", + heading="{jellyfin}", + message=( + translate(33146) + if data.get("ServerId") is None + else translate(33149) + ), + icon=xbmcgui.NOTIFICATION_ERROR, + ) - if data.get('ServerId') is None: + if data.get("ServerId") is None: self.stop_default() if self.waitForAbort(120): @@ -202,12 +259,19 @@ class Service(xbmc.Monitor): self.start_default() - elif method == 'Unauthorized': - dialog("notification", heading="{jellyfin}", message=translate(33147) if data['ServerId'] is None else translate(33148), icon=xbmcgui.NOTIFICATION_ERROR) + elif method == "Unauthorized": + dialog( + "notification", + heading="{jellyfin}", + message=( + translate(33147) if data["ServerId"] is None else translate(33148) + ), + icon=xbmcgui.NOTIFICATION_ERROR, + ) - if data.get('ServerId') is None and self.settings['auth_check']: + if data.get("ServerId") is None and self.settings["auth_check"]: - self.settings['auth_check'] = False + self.settings["auth_check"] = False self.stop_default() if self.waitForAbort(5): @@ -215,12 +279,17 @@ class Service(xbmc.Monitor): self.start_default() - elif method == 'ServerRestarting': - if data.get('ServerId'): + elif method == "ServerRestarting": + if data.get("ServerId"): return - if settings('restartMsg.bool'): - dialog("notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}") + if settings("restartMsg.bool"): + dialog( + "notification", + heading="{jellyfin}", + message=translate(33006), + icon="{jellyfin}", + ) self.stop_default() @@ -229,67 +298,72 @@ class Service(xbmc.Monitor): self.start_default() - elif method == 'ServerConnect': - self.connect.register(data['Id']) + elif method == "ServerConnect": + self.connect.register(data["Id"]) xbmc.executebuiltin("Container.Refresh") - elif method == 'AddServer': + elif method == "AddServer": self.connect.setup_manual_server() xbmc.executebuiltin("Container.Refresh") - elif method == 'RemoveServer': + elif method == "RemoveServer": - self.connect.remove_server(data['Id']) + self.connect.remove_server(data["Id"]) xbmc.executebuiltin("Container.Refresh") - elif method == 'UpdatePassword': + elif method == "UpdatePassword": self.connect.setup_login_manual() - elif method == 'UserDataChanged' and self.library_thread: - if data.get('ServerId') or not window('jellyfin_startup.bool'): + elif method == "UserDataChanged" and self.library_thread: + if data.get("ServerId") or not window("jellyfin_startup.bool"): return LOG.info("[ UserDataChanged ] %s", data) - self.library_thread.userdata(data['UserDataList']) + self.library_thread.userdata(data["UserDataList"]) - elif method == 'LibraryChanged' and self.library_thread: - if data.get('ServerId') or not window('jellyfin_startup.bool'): + elif method == "LibraryChanged" and self.library_thread: + if data.get("ServerId") or not window("jellyfin_startup.bool"): return LOG.info("[ LibraryChanged ] %s", data) - self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded']) - self.library_thread.removed(data['ItemsRemoved']) + self.library_thread.updated(data["ItemsUpdated"] + data["ItemsAdded"]) + self.library_thread.removed(data["ItemsRemoved"]) - elif method == 'System.OnQuit': - window('jellyfin_should_stop.bool', True) + elif method == "System.OnQuit": + window("jellyfin_should_stop.bool", True) self.running = False - elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'): + elif method in ( + "SyncLibrarySelection", + "RepairLibrarySelection", + "AddLibrarySelection", + "RemoveLibrarySelection", + ): self.library_thread.select_libraries(method) - elif method == 'SyncLibrary': - if not data.get('Id'): + elif method == "SyncLibrary": + if not data.get("Id"): return - self.library_thread.add_library(data['Id'], data.get('Update', False)) + self.library_thread.add_library(data["Id"], data.get("Update", False)) xbmc.executebuiltin("Container.Refresh") - elif method == 'RepairLibrary': - if not data.get('Id'): + elif method == "RepairLibrary": + if not data.get("Id"): return - libraries = data['Id'].split(',') + libraries = data["Id"].split(",") for lib in libraries: if not self.library_thread.remove_library(lib): return - self.library_thread.add_library(data['Id']) + self.library_thread.add_library(data["Id"]) xbmc.executebuiltin("Container.Refresh") - elif method == 'RemoveLibrary': - libraries = data['Id'].split(',') + elif method == "RemoveLibrary": + libraries = data["Id"].split(",") for lib in libraries: @@ -298,10 +372,10 @@ class Service(xbmc.Monitor): xbmc.executebuiltin("Container.Refresh") - elif method == 'System.OnSleep': + elif method == "System.OnSleep": LOG.info("-->[ sleep ]") - window('jellyfin_should_stop.bool', True) + window("jellyfin_should_stop.bool", True) if self.library_thread is not None: @@ -312,7 +386,7 @@ class Service(xbmc.Monitor): self.monitor.server = [] self.monitor.sleep = True - elif method == 'System.OnWake': + elif method == "System.OnWake": if not self.monitor.sleep: LOG.warning("System.OnSleep was never called, skip System.OnWake") @@ -322,14 +396,14 @@ class Service(xbmc.Monitor): LOG.info("--<[ sleep ]") xbmc.sleep(10000) # Allow network to wake up self.monitor.sleep = False - window('jellyfin_should_stop', clear=True) + window("jellyfin_should_stop", clear=True) try: self.connect.register() except Exception as error: LOG.exception(error) - elif method == 'GUI.OnScreensaverDeactivated': + elif method == "GUI.OnScreensaverDeactivated": LOG.info("--<[ screensaver ]") xbmc.sleep(5000) @@ -337,60 +411,80 @@ class Service(xbmc.Monitor): if self.library_thread is not None: self.library_thread.fast_sync() - elif method == 'UserConfigurationUpdated' and data.get('ServerId') is None: + elif method == "UserConfigurationUpdated" and data.get("ServerId") is None: Views().get_views() def onSettingsChanged(self): - - ''' React to setting changes that impact window values. - ''' - if window('jellyfin_should_stop.bool'): + """React to setting changes that impact window values.""" + if window("jellyfin_should_stop.bool"): return - if settings('logLevel') != self.settings['log_level']: + if settings("logLevel") != self.settings["log_level"]: - log_level = settings('logLevel') - self.settings['logLevel'] = log_level + log_level = settings("logLevel") + self.settings["logLevel"] = log_level LOG.info("New log level: %s", log_level) - if settings('enableContext.bool') != self.settings['enable_context']: + if settings("enableContext.bool") != self.settings["enable_context"]: - window('jellyfin_context', settings('enableContext')) - self.settings['enable_context'] = settings('enableContext.bool') - LOG.info("New context setting: %s", self.settings['enable_context']) + window("jellyfin_context", settings("enableContext")) + self.settings["enable_context"] = settings("enableContext.bool") + LOG.info("New context setting: %s", self.settings["enable_context"]) - if settings('enableContextTranscode.bool') != self.settings['enable_context_transcode']: + if ( + settings("enableContextTranscode.bool") + != self.settings["enable_context_transcode"] + ): - window('jellyfin_context_transcode', settings('enableContextTranscode')) - self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') - LOG.info("New context transcode setting: %s", self.settings['enable_context_transcode']) + window("jellyfin_context_transcode", settings("enableContextTranscode")) + self.settings["enable_context_transcode"] = settings( + "enableContextTranscode.bool" + ) + LOG.info( + "New context transcode setting: %s", + self.settings["enable_context_transcode"], + ) - if settings('useDirectPaths') != self.settings['mode'] and self.library_thread.started: + if ( + settings("useDirectPaths") != self.settings["mode"] + and self.library_thread.started + ): - self.settings['mode'] = settings('useDirectPaths') - LOG.info("New playback mode setting: %s", self.settings['mode']) + self.settings["mode"] = settings("useDirectPaths") + LOG.info("New playback mode setting: %s", self.settings["mode"]) - if not self.settings.get('mode_warn'): + if not self.settings.get("mode_warn"): - self.settings['mode_warn'] = True + self.settings["mode_warn"] = True dialog("yesno", "{jellyfin}", translate(33118)) - if settings('kodiCompanion.bool') != self.settings['kodi_companion']: - self.settings['kodi_companion'] = settings('kodiCompanion.bool') + if settings("kodiCompanion.bool") != self.settings["kodi_companion"]: + self.settings["kodi_companion"] = settings("kodiCompanion.bool") - if not self.settings['kodi_companion']: + if not self.settings["kodi_companion"]: dialog("ok", "{jellyfin}", translate(33138)) def reload_objects(self): - - ''' Reload objects which depends on the patch module. - This allows to see the changes in code without restarting the python interpreter. - ''' - reload_modules = ['objects.movies', 'objects.musicvideos', 'objects.tvshows', - 'objects.music', 'objects.obj', 'objects.actions', 'objects.kodi.kodi', - 'objects.kodi.movies', 'objects.kodi.musicvideos', 'objects.kodi.tvshows', - 'objects.kodi.music', 'objects.kodi.artwork', 'objects.kodi.queries', - 'objects.kodi.queries_music', 'objects.kodi.queries_texture'] + """Reload objects which depends on the patch module. + This allows to see the changes in code without restarting the python interpreter. + """ + reload_modules = [ + "objects.movies", + "objects.musicvideos", + "objects.tvshows", + "objects.music", + "objects.obj", + "objects.actions", + "objects.kodi.kodi", + "objects.kodi.movies", + "objects.kodi.musicvideos", + "objects.kodi.tvshows", + "objects.kodi.music", + "objects.kodi.artwork", + "objects.kodi.queries", + "objects.kodi.queries_music", + "objects.kodi.queries_texture", + ] for mod in reload_modules: del sys.modules[mod] @@ -407,14 +501,22 @@ class Service(xbmc.Monitor): def shutdown(self): LOG.info("---<[ EXITING ]") - window('jellyfin_should_stop.bool', True) + window("jellyfin_should_stop.bool", True) properties = [ # TODO: review - "jellyfin_state", "jellyfin_serverStatus", "jellyfin_currUser", - - "jellyfin_play", "jellyfin_online", "jellyfin.connected", "jellyfin_startup", - "jellyfin.external", "jellyfin.external_check", "jellyfin_deviceId", "jellyfin_db_check", "jellyfin_pathverified", - "jellyfin_sync" + "jellyfin_state", + "jellyfin_serverStatus", + "jellyfin_currUser", + "jellyfin_play", + "jellyfin_online", + "jellyfin.connected", + "jellyfin_startup", + "jellyfin.external", + "jellyfin.external_check", + "jellyfin_deviceId", + "jellyfin_db_check", + "jellyfin_pathverified", + "jellyfin_sync", ] for prop in properties: window(prop, clear=True) diff --git a/jellyfin_kodi/full_sync.py b/jellyfin_kodi/full_sync.py index 7463c300..6143e960 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -23,11 +23,11 @@ LOG = LazyLogger(__name__) class FullSync(object): + """This should be called like a context. + i.e. with FullSync('jellyfin') as sync: + sync.libraries() + """ - ''' This should be called like a context. - i.e. with FullSync('jellyfin') as sync: - sync.libraries() - ''' # Borg - multiple instances, shared state _shared_state = {} sync = None @@ -35,10 +35,9 @@ class FullSync(object): screensaver = None def __init__(self, library, server): - - ''' You can call all big syncing methods here. - Initial, update, repair, remove. - ''' + """You can call all big syncing methods here. + Initial, update, repair, remove. + """ self.__dict__ = self._shared_state if self.running: @@ -50,78 +49,81 @@ class FullSync(object): self.server = server def __enter__(self): - - ''' Do everything we need before the sync - ''' + """Do everything we need before the sync""" LOG.info("-->[ fullsync ]") - if not settings('dbSyncScreensaver.bool'): + if not settings("dbSyncScreensaver.bool"): - xbmc.executebuiltin('InhibitIdleShutdown(true)') + xbmc.executebuiltin("InhibitIdleShutdown(true)") self.screensaver = get_screensaver() set_screensaver(value="") self.running = True - window('jellyfin_sync.bool', True) + window("jellyfin_sync.bool", True) return self def libraries(self, libraries=None, update=False): - - ''' Map the syncing process and start the sync. Ensure only one sync is running. - ''' - self.direct_path = settings('useDirectPaths') == "1" + """Map the syncing process and start the sync. Ensure only one sync is running.""" + self.direct_path = settings("useDirectPaths") == "1" self.update_library = update self.sync = get_sync() if libraries: # Can be a single ID or a comma separated list - libraries = libraries.split(',') + libraries = libraries.split(",") for library_id in libraries: # Look up library in local Jellyfin database library = self.get_library(library_id) if library: - if library.media_type == 'mixed': - self.sync['Libraries'].append("Mixed:%s" % library_id) + if library.media_type == "mixed": + self.sync["Libraries"].append("Mixed:%s" % library_id) # Include boxsets library libraries = self.get_libraries() - boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets'] + boxsets = [ + row.view_id + for row in libraries + if row.media_type == "boxsets" + ] if boxsets: - self.sync['Libraries'].append('Boxsets:%s' % boxsets[0]) - elif library.media_type == 'movies': - self.sync['Libraries'].append(library_id) + self.sync["Libraries"].append("Boxsets:%s" % boxsets[0]) + elif library.media_type == "movies": + self.sync["Libraries"].append(library_id) # Include boxsets library libraries = self.get_libraries() - boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets'] + boxsets = [ + row.view_id + for row in libraries + if row.media_type == "boxsets" + ] # Verify we're only trying to sync boxsets once - if boxsets and boxsets[0] not in self.sync['Libraries']: - self.sync['Libraries'].append('Boxsets:%s' % boxsets[0]) + if boxsets and boxsets[0] not in self.sync["Libraries"]: + self.sync["Libraries"].append("Boxsets:%s" % boxsets[0]) else: # Only called if the library isn't already known about - self.sync['Libraries'].append(library_id) + self.sync["Libraries"].append(library_id) else: - self.sync['Libraries'].append(library_id) + self.sync["Libraries"].append(library_id) else: self.mapping() - if not xmls.advanced_settings() and self.sync['Libraries']: + if not xmls.advanced_settings() and self.sync["Libraries"]: self.start() def get_libraries(self): - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() def get_library(self, library_id): - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_view(library_id) def mapping(self): - - ''' Load the mapping of the full sync. - This allows us to restore a previous sync. - ''' - if self.sync['Libraries']: + """Load the mapping of the full sync. + This allows us to restore a previous sync. + """ + if self.sync["Libraries"]: if not dialog("yesno", "{jellyfin}", translate(33102)): @@ -130,38 +132,48 @@ class FullSync(object): raise LibraryException("ProgressStopped") else: - self.sync['Libraries'] = [] - self.sync['RestorePoint'] = {} + self.sync["Libraries"] = [] + self.sync["RestorePoint"] = {} else: LOG.info("generate full sync") libraries = [] for library in self.get_libraries(): - if library.media_type in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'): - libraries.append({'Id': library.view_id, 'Name': library.view_name, 'Media': library.media_type}) + if library.media_type in ( + "movies", + "tvshows", + "musicvideos", + "music", + "mixed", + ): + libraries.append( + { + "Id": library.view_id, + "Name": library.view_name, + "Media": library.media_type, + } + ) libraries = self.select_libraries(libraries) - if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]: - self.sync['Libraries'].append("Boxsets:") + if [x["Media"] for x in libraries if x["Media"] in ("movies", "mixed")]: + self.sync["Libraries"].append("Boxsets:") save_sync(self.sync) def select_libraries(self, libraries): + """Select all or certain libraries to be whitelisted.""" - ''' Select all or certain libraries to be whitelisted. - ''' - - choices = [x['Name'] for x in libraries] + choices = [x["Name"] for x in libraries] choices.insert(0, translate(33121)) selection = dialog("multi", translate(33120), choices) if selection is None: - raise LibraryException('LibrarySelection') + raise LibraryException("LibrarySelection") elif not selection: LOG.info("Nothing was selected.") - raise LibraryException('SyncLibraryLater') + raise LibraryException("SyncLibraryLater") if 0 in selection: selection = list(range(1, len(libraries) + 1)) @@ -171,96 +183,100 @@ class FullSync(object): for x in selection: library = libraries[x - 1] - if library['Media'] != 'mixed': - selected_libraries.append(library['Id']) + if library["Media"] != "mixed": + selected_libraries.append(library["Id"]) else: - selected_libraries.append("Mixed:%s" % library['Id']) + selected_libraries.append("Mixed:%s" % library["Id"]) - self.sync['Libraries'] = selected_libraries + self.sync["Libraries"] = selected_libraries return [libraries[x - 1] for x in selection] def start(self): - - ''' Main sync process. - ''' - LOG.info("starting sync with %s", self.sync['Libraries']) + """Main sync process.""" + LOG.info("starting sync with %s", self.sync["Libraries"]) save_sync(self.sync) start_time = datetime.datetime.now() - for library in list(self.sync['Libraries']): + for library in list(self.sync["Libraries"]): self.process_library(library) - if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']: - self.sync['Whitelist'].append(library) + if ( + not library.startswith("Boxsets:") + and library not in self.sync["Whitelist"] + ): + self.sync["Whitelist"].append(library) - self.sync['Libraries'].pop(self.sync['Libraries'].index(library)) - self.sync['RestorePoint'] = {} + self.sync["Libraries"].pop(self.sync["Libraries"].index(library)) + self.sync["RestorePoint"] = {} elapsed = datetime.datetime.now() - start_time - settings('SyncInstallRunDone.bool', True) + settings("SyncInstallRunDone.bool", True) self.library.save_last_sync() save_sync(self.sync) - xbmc.executebuiltin('UpdateLibrary(video)') - dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33025), str(elapsed).split('.')[0]), - icon="{jellyfin}", sound=False) - LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0]) + xbmc.executebuiltin("UpdateLibrary(video)") + dialog( + "notification", + heading="{jellyfin}", + message="%s %s" % (translate(33025), str(elapsed).split(".")[0]), + icon="{jellyfin}", + sound=False, + ) + LOG.info("Full sync completed in: %s", str(elapsed).split(".")[0]) def process_library(self, library_id): - - ''' Add a library by its id. Create a node and a playlist whenever appropriate. - ''' + """Add a library by its id. Create a node and a playlist whenever appropriate.""" media = { - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows, - 'music': self.music + "movies": self.movies, + "musicvideos": self.musicvideos, + "tvshows": self.tvshows, + "music": self.music, } try: - if library_id.startswith('Boxsets:'): + if library_id.startswith("Boxsets:"): boxset_library = {} # Initial library sync is 'Boxsets:' # Refresh from the addon menu is 'Boxsets:Refresh' # Incremental syncs are 'Boxsets:$library_id' - sync_id = library_id.split(':')[1] + sync_id = library_id.split(":")[1] - if not sync_id or sync_id == 'Refresh': + if not sync_id or sync_id == "Refresh": libraries = self.get_libraries() else: _lib = self.get_library(sync_id) libraries = [_lib] if _lib else [] for entry in libraries: - if entry.media_type == 'boxsets': - boxset_library = {'Id': entry.view_id, 'Name': entry.view_name} + if entry.media_type == "boxsets": + boxset_library = {"Id": entry.view_id, "Name": entry.view_name} break if boxset_library: - if sync_id == 'Refresh': + if sync_id == "Refresh": self.refresh_boxsets(boxset_library) else: self.boxsets(boxset_library) return - library = self.server.jellyfin.get_item(library_id.replace('Mixed:', "")) + library = self.server.jellyfin.get_item(library_id.replace("Mixed:", "")) - if library_id.startswith('Mixed:'): - for mixed in ('movies', 'tvshows'): + if library_id.startswith("Mixed:"): + for mixed in ("movies", "tvshows"): media[mixed](library) - self.sync['RestorePoint'] = {} + self.sync["RestorePoint"] = {} else: - if library['CollectionType']: - settings('enableMusic.bool', True) + if library["CollectionType"]: + settings("enableMusic.bool", True) - media[library['CollectionType']](library) + media[library["CollectionType"]](library) except LibraryException as error: - if error.status == 'StopCalled': + if error.status == "StopCalled": save_sync(self.sync) raise @@ -282,31 +298,41 @@ class FullSync(object): def video_database_locks(self): with self.library.database_lock: with Database() as videodb: - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: yield videodb, jellyfindb @progress() def movies(self, library, dialog): - - ''' Process movies from a single library. - ''' + """Process movies from a single library.""" processed_ids = [] - for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): + for items in server.get_items( + library["Id"], "Movie", False, self.sync["RestorePoint"].get("params") + ): with self.video_database_locks() as (videodb, jellyfindb): - obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library) + obj = Movies( + self.server, jellyfindb, videodb, self.direct_path, library + ) - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] - for index, movie in enumerate(items['Items']): + for index, movie in enumerate(items["Items"]): - dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100), - heading="%s: %s" % (translate('addon_name'), library['Name']), - message=movie['Name']) + dialog.update( + int( + ( + float(start_index + index) + / float(items["TotalRecordCount"]) + ) + * 100 + ), + heading="%s: %s" % (translate("addon_name"), library["Name"]), + message=movie["Name"], + ) obj.movie(movie) - processed_ids.append(movie['Id']) + processed_ids.append(movie["Id"]) with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library) @@ -316,158 +342,199 @@ class FullSync(object): self.movies_compare(library, obj, jellyfindb) def movies_compare(self, library, obj, jellyfinydb): - - ''' Compare entries from library to what's in the jellyfindb. Remove surplus - ''' + """Compare entries from library to what's in the jellyfindb. Remove surplus""" db = jellyfin_db.JellyfinDatabase(jellyfinydb.cursor) - items = db.get_item_by_media_folder(library['Id']) + items = db.get_item_by_media_folder(library["Id"]) current = obj.item_ids for x in items: - if x[0] not in current and x[1] == 'Movie': + if x[0] not in current and x[1] == "Movie": obj.remove(x[0]) @progress() def tvshows(self, library, dialog): - - ''' Process tvshows and episodes from a single library. - ''' + """Process tvshows and episodes from a single library.""" processed_ids = [] - for items in server.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): + for items in server.get_items( + library["Id"], "Series", False, self.sync["RestorePoint"].get("params") + ): with self.video_database_locks() as (videodb, jellyfindb): - obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True) + obj = TVShows( + self.server, jellyfindb, videodb, self.direct_path, library, True + ) - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] - for index, show in enumerate(items['Items']): + for index, show in enumerate(items["Items"]): - percent = int((float(start_index + index) / float(items['TotalRecordCount'])) * 100) - message = show['Name'] - dialog.update(percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message) + percent = int( + (float(start_index + index) / float(items["TotalRecordCount"])) + * 100 + ) + message = show["Name"] + dialog.update( + percent, + heading="%s: %s" % (translate("addon_name"), library["Name"]), + message=message, + ) if obj.tvshow(show) is not False: - for episodes in server.get_episode_by_show(show['Id']): - for episode in episodes['Items']: - if episode.get('Path'): - dialog.update(percent, message="%s/%s" % (message, episode['Name'][:10])) + for episodes in server.get_episode_by_show(show["Id"]): + for episode in episodes["Items"]: + if episode.get("Path"): + dialog.update( + percent, + message="%s/%s" + % (message, episode["Name"][:10]), + ) obj.episode(episode) - processed_ids.append(show['Id']) + processed_ids.append(show["Id"]) with self.video_database_locks() as (videodb, jellyfindb): - obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True) + obj = TVShows( + self.server, jellyfindb, videodb, self.direct_path, library, True + ) obj.item_ids = processed_ids if self.update_library: self.tvshows_compare(library, obj, jellyfindb) def tvshows_compare(self, library, obj, jellyfindb): - - ''' Compare entries from library to what's in the jellyfindb. Remove surplus - ''' + """Compare entries from library to what's in the jellyfindb. Remove surplus""" db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - items = db.get_item_by_media_folder(library['Id']) + items = db.get_item_by_media_folder(library["Id"]) for x in list(items): items.extend(obj.get_child(x[0])) current = obj.item_ids for x in items: - if x[0] not in current and x[1] == 'Series': + if x[0] not in current and x[1] == "Series": obj.remove(x[0]) @progress() def musicvideos(self, library, dialog): - - ''' Process musicvideos from a single library. - ''' + """Process musicvideos from a single library.""" processed_ids = [] - for items in server.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')): + for items in server.get_items( + library["Id"], "MusicVideo", False, self.sync["RestorePoint"].get("params") + ): with self.video_database_locks() as (videodb, jellyfindb): - obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library) + obj = MusicVideos( + self.server, jellyfindb, videodb, self.direct_path, library + ) - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] - for index, mvideo in enumerate(items['Items']): + for index, mvideo in enumerate(items["Items"]): - dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100), - heading="%s: %s" % (translate('addon_name'), library['Name']), - message=mvideo['Name']) + dialog.update( + int( + ( + float(start_index + index) + / float(items["TotalRecordCount"]) + ) + * 100 + ), + heading="%s: %s" % (translate("addon_name"), library["Name"]), + message=mvideo["Name"], + ) obj.musicvideo(mvideo) - processed_ids.append(mvideo['Id']) + processed_ids.append(mvideo["Id"]) with self.video_database_locks() as (videodb, jellyfindb): - obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library) + obj = MusicVideos( + self.server, jellyfindb, videodb, self.direct_path, library + ) obj.item_ids = processed_ids if self.update_library: self.musicvideos_compare(library, obj, jellyfindb) def musicvideos_compare(self, library, obj, jellyfindb): - - ''' Compare entries from library to what's in the jellyfindb. Remove surplus - ''' + """Compare entries from library to what's in the jellyfindb. Remove surplus""" db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - items = db.get_item_by_media_folder(library['Id']) + items = db.get_item_by_media_folder(library["Id"]) current = obj.item_ids for x in items: - if x[0] not in current and x[1] == 'MusicVideo': + if x[0] not in current and x[1] == "MusicVideo": obj.remove(x[0]) @progress() def music(self, library, dialog): - - ''' Process artists, album, songs from a single library. - ''' + """Process artists, album, songs from a single library.""" with self.library.music_database_lock: - with Database('music') as musicdb: - with Database('jellyfin') as jellyfindb: - obj = Music(self.server, jellyfindb, musicdb, self.direct_path, library) + with Database("music") as musicdb: + with Database("jellyfin") as jellyfindb: + obj = Music( + self.server, jellyfindb, musicdb, self.direct_path, library + ) - library_id = library['Id'] + library_id = library["Id"] - total_items = server.get_item_count(library_id, 'MusicArtist,MusicAlbum,Audio') + total_items = server.get_item_count( + library_id, "MusicArtist,MusicAlbum,Audio" + ) count = 0 - ''' + """ Music database syncing. Artists must be in the database before albums, albums before songs. Pulls batches of items in sizes of setting "Paging - Max items". 'artists', 'albums', and 'songs' are generators containing a dict of api responses - ''' + """ artists = server.get_artists(library_id) for batch in artists: - for item in batch['Items']: - LOG.debug('Artist: {}'.format(item.get('Name'))) + for item in batch["Items"]: + LOG.debug("Artist: {}".format(item.get("Name"))) percent = int((float(count) / float(total_items)) * 100) - dialog.update(percent, message='Artist: {}'.format(item.get('Name'))) + dialog.update( + percent, message="Artist: {}".format(item.get("Name")) + ) obj.artist(item) count += 1 - albums = server.get_items(library_id, item_type='MusicAlbum', params={'SortBy': 'AlbumArtist'}) + albums = server.get_items( + library_id, + item_type="MusicAlbum", + params={"SortBy": "AlbumArtist"}, + ) for batch in albums: - for item in batch['Items']: - LOG.debug('Album: {}'.format(item.get('Name'))) + for item in batch["Items"]: + LOG.debug("Album: {}".format(item.get("Name"))) percent = int((float(count) / float(total_items)) * 100) - dialog.update(percent, message='Album: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name'))) + dialog.update( + percent, + message="Album: {} - {}".format( + item.get("AlbumArtist", ""), item.get("Name") + ), + ) obj.album(item) count += 1 - songs = server.get_items(library_id, item_type='Audio', params={'SortBy': 'AlbumArtist'}) + songs = server.get_items( + library_id, item_type="Audio", params={"SortBy": "AlbumArtist"} + ) for batch in songs: - for item in batch['Items']: - LOG.debug('Song: {}'.format(item.get('Name'))) + for item in batch["Items"]: + LOG.debug("Song: {}".format(item.get("Name"))) percent = int((float(count) / float(total_items)) * 100) - dialog.update(percent, message='Track: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name'))) + dialog.update( + percent, + message="Track: {} - {}".format( + item.get("AlbumArtist", ""), item.get("Name") + ), + ) obj.song(item) count += 1 @@ -475,45 +542,52 @@ class FullSync(object): self.music_compare(library, obj, jellyfindb) def music_compare(self, library, obj, jellyfindb): - - ''' Compare entries from library to what's in the jellyfindb. Remove surplus - ''' + """Compare entries from library to what's in the jellyfindb. Remove surplus""" db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - items = db.get_item_by_media_folder(library['Id']) + items = db.get_item_by_media_folder(library["Id"]) for x in list(items): items.extend(obj.get_child(x[0])) current = obj.item_ids for x in items: - if x[0] not in current and x[1] == 'MusicArtist': + if x[0] not in current and x[1] == "MusicArtist": obj.remove(x[0]) @progress(translate(33018)) def boxsets(self, library, dialog=None): - - ''' Process all boxsets. - ''' - for items in server.get_items(library['Id'], "BoxSet", False, self.sync['RestorePoint'].get('params')): + """Process all boxsets.""" + for items in server.get_items( + library["Id"], "BoxSet", False, self.sync["RestorePoint"].get("params") + ): with self.video_database_locks() as (videodb, jellyfindb): - obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library) + obj = Movies( + self.server, jellyfindb, videodb, self.direct_path, library + ) - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] - for index, boxset in enumerate(items['Items']): + for index, boxset in enumerate(items["Items"]): - dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100), - heading="%s: %s" % (translate('addon_name'), translate('boxsets')), - message=boxset['Name']) + dialog.update( + int( + ( + float(start_index + index) + / float(items["TotalRecordCount"]) + ) + * 100 + ), + heading="%s: %s" + % (translate("addon_name"), translate("boxsets")), + message=boxset["Name"], + ) obj.boxset(boxset) def refresh_boxsets(self, library): - - ''' Delete all existing boxsets and re-add. - ''' + """Delete all existing boxsets and re-add.""" with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library) obj.boxsets_reset() @@ -522,82 +596,108 @@ class FullSync(object): @progress(translate(33144)) def remove_library(self, library_id, dialog): - - ''' Remove library by their id from the Kodi database. - ''' + """Remove library by their id from the Kodi database.""" direct_path = self.library.direct_path - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - library = db.get_view(library_id.replace('Mixed:', "")) - items = db.get_item_by_media_folder(library_id.replace('Mixed:', "")) - media = 'music' if library.media_type == 'music' else 'video' + library = db.get_view(library_id.replace("Mixed:", "")) + items = db.get_item_by_media_folder(library_id.replace("Mixed:", "")) + media = "music" if library.media_type == "music" else "video" - if media == 'music': - settings('MusicRescan.bool', False) + if media == "music": + settings("MusicRescan.bool", False) if items: - with self.library.music_database_lock if media == 'music' else self.library.database_lock: + with ( + self.library.music_database_lock + if media == "music" + else self.library.database_lock + ): with Database(media) as kodidb: count = 0 - if library.media_type == 'mixed': + if library.media_type == "mixed": - movies = [x for x in items if x[1] == 'Movie'] - tvshows = [x for x in items if x[1] == 'Series'] + movies = [x for x in items if x[1] == "Movie"] + tvshows = [x for x in items if x[1] == "Series"] - obj = Movies(self.server, jellyfindb, kodidb, direct_path, library).remove + obj = Movies( + self.server, jellyfindb, kodidb, direct_path, library + ).remove for item in movies: obj(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name)) + dialog.update( + int((float(count) / float(len(items)) * 100)), + heading="%s: %s" + % (translate("addon_name"), library.view_name), + ) count += 1 - obj = TVShows(self.server, jellyfindb, kodidb, direct_path, library).remove + obj = TVShows( + self.server, jellyfindb, kodidb, direct_path, library + ).remove for item in tvshows: obj(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name)) + dialog.update( + int((float(count) / float(len(items)) * 100)), + heading="%s: %s" + % (translate("addon_name"), library.view_name), + ) count += 1 else: - default_args = (self.server, jellyfindb, kodidb, direct_path) + default_args = ( + self.server, + jellyfindb, + kodidb, + direct_path, + ) for item in items: - if item[1] in ('Series', 'Season', 'Episode'): + if item[1] in ("Series", "Season", "Episode"): TVShows(*default_args).remove(item[0]) - elif item[1] in ('Movie', 'BoxSet'): + elif item[1] in ("Movie", "BoxSet"): Movies(*default_args).remove(item[0]) - elif item[1] in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + elif item[1] in ( + "MusicAlbum", + "MusicArtist", + "AlbumArtist", + "Audio", + ): Music(*default_args).remove(item[0]) - elif item[1] == 'MusicVideo': + elif item[1] == "MusicVideo": MusicVideos(*default_args).remove(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) + dialog.update( + int((float(count) / float(len(items)) * 100)), + heading="%s: %s" + % (translate("addon_name"), library[0]), + ) count += 1 self.sync = get_sync() - if library_id in self.sync['Whitelist']: - self.sync['Whitelist'].remove(library_id) + if library_id in self.sync["Whitelist"]: + self.sync["Whitelist"].remove(library_id) - elif 'Mixed:%s' % library_id in self.sync['Whitelist']: - self.sync['Whitelist'].remove('Mixed:%s' % library_id) + elif "Mixed:%s" % library_id in self.sync["Whitelist"]: + self.sync["Whitelist"].remove("Mixed:%s" % library_id) save_sync(self.sync) def __exit__(self, exc_type, exc_val, exc_tb): - - ''' Exiting sync - ''' + """Exiting sync""" self.running = False - window('jellyfin_sync', clear=True) + window("jellyfin_sync", clear=True) - if not settings('dbSyncScreensaver.bool') and self.screensaver is not None: + if not settings("dbSyncScreensaver.bool") and self.screensaver is not None: - xbmc.executebuiltin('InhibitIdleShutdown(false)') + xbmc.executebuiltin("InhibitIdleShutdown(false)") set_screensaver(value=self.screensaver) LOG.info("--<[ fullsync ]") diff --git a/jellyfin_kodi/helper/api.py b/jellyfin_kodi/helper/api.py index 7ac839ad..0174c3ed 100644 --- a/jellyfin_kodi/helper/api.py +++ b/jellyfin_kodi/helper/api.py @@ -14,100 +14,102 @@ LOG = LazyLogger(__name__) class API(object): def __init__(self, item, server=None): - - ''' Get item information in special cases. - server is the server address, provide if your functions requires it. - ''' + """Get item information in special cases. + server is the server address, provide if your functions requires it. + """ self.item = item self.server = server def get_playcount(self, played, playcount): - - ''' Convert Jellyfin played/playcount into - the Kodi equivalent. The playcount is tied to the watch status. - ''' + """Convert Jellyfin played/playcount into + the Kodi equivalent. The playcount is tied to the watch status. + """ return (playcount or 1) if played else None def get_naming(self): - if self.item['Type'] == 'Episode' and 'SeriesName' in self.item: - return "%s: %s" % (self.item['SeriesName'], self.item['Name']) + if self.item["Type"] == "Episode" and "SeriesName" in self.item: + return "%s: %s" % (self.item["SeriesName"], self.item["Name"]) - elif self.item['Type'] == 'MusicAlbum' and 'AlbumArtist' in self.item: - return "%s: %s" % (self.item['AlbumArtist'], self.item['Name']) + elif self.item["Type"] == "MusicAlbum" and "AlbumArtist" in self.item: + return "%s: %s" % (self.item["AlbumArtist"], self.item["Name"]) - elif self.item['Type'] == 'Audio' and self.item.get('Artists'): - return "%s: %s" % (self.item['Artists'][0], self.item['Name']) + elif self.item["Type"] == "Audio" and self.item.get("Artists"): + return "%s: %s" % (self.item["Artists"][0], self.item["Name"]) - return self.item['Name'] + return self.item["Name"] def get_actors(self): cast = [] - if 'People' in self.item: - self.get_people_artwork(self.item['People']) + if "People" in self.item: + self.get_people_artwork(self.item["People"]) - for person in self.item['People']: + for person in self.item["People"]: - if person['Type'] == "Actor": - cast.append({ - 'name': person['Name'], - 'role': person.get('Role', "Unknown"), - 'order': len(cast) + 1, - 'thumbnail': person['imageurl'] - }) + if person["Type"] == "Actor": + cast.append( + { + "name": person["Name"], + "role": person.get("Role", "Unknown"), + "order": len(cast) + 1, + "thumbnail": person["imageurl"], + } + ) return cast def media_streams(self, video, audio, subtitles): - return { - 'video': video or [], - 'audio': audio or [], - 'subtitle': subtitles or [] - } + return {"video": video or [], "audio": audio or [], "subtitle": subtitles or []} def video_streams(self, tracks, container=None): if container: - container = container.split(',')[0] + container = container.split(",")[0] for track in tracks: if "DvProfile" in track: - track['hdrtype'] = "dolbyvision" - elif track.get('VideoRangeType', '') in ["HDR10", "HDR10Plus"]: - track['hdrtype'] = "hdr10" - elif "HLG" in track.get('VideoRangeType', ''): - track['hdrtype'] = "hlg" + track["hdrtype"] = "dolbyvision" + elif track.get("VideoRangeType", "") in ["HDR10", "HDR10Plus"]: + track["hdrtype"] = "hdr10" + elif "HLG" in track.get("VideoRangeType", ""): + track["hdrtype"] = "hlg" - track.update({ - 'hdrtype': track.get('hdrtype', "").lower(), - 'codec': track.get('Codec', "").lower(), - 'profile': track.get('Profile', "").lower(), - 'height': track.get('Height'), - 'width': track.get('Width'), - '3d': self.item.get('Video3DFormat'), - 'aspect': 1.85 - }) + track.update( + { + "hdrtype": track.get("hdrtype", "").lower(), + "codec": track.get("Codec", "").lower(), + "profile": track.get("Profile", "").lower(), + "height": track.get("Height"), + "width": track.get("Width"), + "3d": self.item.get("Video3DFormat"), + "aspect": 1.85, + } + ) - if "msmpeg4" in track['codec']: - track['codec'] = "divx" + if "msmpeg4" in track["codec"]: + track["codec"] = "divx" - elif "mpeg4" in track['codec'] and ("simple profile" in track['profile'] or not track['profile']): - track['codec'] = "xvid" + elif "mpeg4" in track["codec"] and ( + "simple profile" in track["profile"] or not track["profile"] + ): + track["codec"] = "xvid" - elif "h264" in track['codec'] and container in ('mp4', 'mov', 'm4v'): - track['codec'] = "avc1" + elif "h264" in track["codec"] and container in ("mp4", "mov", "m4v"): + track["codec"] = "avc1" try: - width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':') - track['aspect'] = round(float(width) / float(height), 6) + width, height = self.item.get( + "AspectRatio", track.get("AspectRatio", "0") + ).split(":") + track["aspect"] = round(float(width) / float(height), 6) except (ValueError, ZeroDivisionError): - if track['width'] and track['height']: - track['aspect'] = round(float(track['width'] / track['height']), 6) + if track["width"] and track["height"]: + track["aspect"] = round(float(track["width"] / track["height"]), 6) - track['duration'] = self.get_runtime() + track["duration"] = self.get_runtime() return tracks @@ -115,28 +117,30 @@ class API(object): for track in tracks: - track.update({ - 'codec': track.get('Codec', "").lower(), - 'profile': track.get('Profile', "").lower(), - 'channels': track.get('Channels'), - 'language': track.get('Language') - }) + track.update( + { + "codec": track.get("Codec", "").lower(), + "profile": track.get("Profile", "").lower(), + "channels": track.get("Channels"), + "language": track.get("Language"), + } + ) - if "dts-hd ma" in track['profile']: - track['codec'] = "dtshd_ma" + if "dts-hd ma" in track["profile"]: + track["codec"] = "dtshd_ma" - elif "dts-hd hra" in track['profile']: - track['codec'] = "dtshd_hra" + elif "dts-hd hra" in track["profile"]: + track["codec"] = "dtshd_hra" return tracks def get_runtime(self): try: - runtime = self.item['RunTimeTicks'] / 10000000.0 + runtime = self.item["RunTimeTicks"] / 10000000.0 except KeyError: - runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0 + runtime = self.item.get("CumulativeRunTimeTicks", 0) / 10000000.0 return runtime @@ -146,7 +150,7 @@ class API(object): resume = 0 if resume_seconds: resume = round(float(resume_seconds), 6) - jumpback = int(settings('resumeJumpBack')) + jumpback = int(settings("resumeJumpBack")) if resume > jumpback: # To avoid negative bookmark resume = resume - jumpback @@ -156,25 +160,25 @@ class API(object): def validate_studio(self, studio_name): # Convert studio for Kodi to properly detect them studios = { - 'abc (us)': "ABC", - 'fox (us)': "FOX", - 'mtv (us)': "MTV", - 'showcase (ca)': "Showcase", - 'wgn america': "WGN", - 'bravo (us)': "Bravo", - 'tnt (us)': "TNT", - 'comedy central': "Comedy Central (US)" + "abc (us)": "ABC", + "fox (us)": "FOX", + "mtv (us)": "MTV", + "showcase (ca)": "Showcase", + "wgn america": "WGN", + "bravo (us)": "Bravo", + "tnt (us)": "TNT", + "comedy central": "Comedy Central (US)", } return studios.get(studio_name.lower(), studio_name) def get_overview(self, overview=None): - overview = overview or self.item.get('Overview') + overview = overview or self.item.get("Overview") if not overview: return - overview = overview.replace("\"", "\'") + overview = overview.replace('"', "'") overview = overview.replace("\n", "[CR]") overview = overview.replace("\r", " ") overview = overview.replace("
", "[CR]") @@ -183,7 +187,7 @@ class API(object): def get_mpaa(self, rating=None): - mpaa = rating or self.item.get('OfficialRating', "") + mpaa = rating or self.item.get("OfficialRating", "") if mpaa in ("NR", "UR"): # Kodi seems to not like NR, but will accept Not Rated @@ -197,112 +201,123 @@ class API(object): def get_file_path(self, path=None): if path is None: - path = self.item.get('Path') + path = self.item.get("Path") if not path: return "" - if path.startswith('\\\\'): - path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/") + if path.startswith("\\\\"): + path = ( + path.replace("\\\\", "smb://", 1) + .replace("\\\\", "\\") + .replace("\\", "/") + ) - if 'Container' in self.item: + if "Container" in self.item: - if self.item['Container'] == 'dvd': + if self.item["Container"] == "dvd": path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path - elif self.item['Container'] == 'bluray': + elif self.item["Container"] == "bluray": path = "%s/BDMV/index.bdmv" % path - path = path.replace('\\\\', "\\") + path = path.replace("\\\\", "\\") - if '\\' in path: - path = path.replace('/', "\\") + if "\\" in path: + path = path.replace("/", "\\") - if '://' in path: - protocol = path.split('://')[0] + if "://" in path: + protocol = path.split("://")[0] path = path.replace(protocol, protocol.lower()) return path def get_user_artwork(self, user_id): - - ''' Get jellyfin user profile picture. - ''' + """Get jellyfin user profile picture.""" return "%s/Users/%s/Images/Primary?Format=original" % (self.server, user_id) def get_people_artwork(self, people): - - ''' Get people (actor, director, etc) artwork. - ''' + """Get people (actor, director, etc) artwork.""" for person in people: - if 'PrimaryImageTag' in person: + if "PrimaryImageTag" in person: query = "&MaxWidth=400&MaxHeight=400&Index=0" - person['imageurl'] = self.get_artwork(person['Id'], "Primary", person['PrimaryImageTag'], query) + person["imageurl"] = self.get_artwork( + person["Id"], "Primary", person["PrimaryImageTag"], query + ) else: - person['imageurl'] = None + person["imageurl"] = None return people def get_all_artwork(self, obj, parent_info=False): + """Get all artwork possible. If parent_info is True, + it will fill missing artwork with parent artwork. - ''' Get all artwork possible. If parent_info is True, - it will fill missing artwork with parent artwork. - - obj is from objects.Objects().map(item, 'Artwork') - ''' + obj is from objects.Objects().map(item, 'Artwork') + """ query = "" all_artwork = { - 'Primary': "", - 'BoxRear': "", - 'Art': "", - 'Banner': "", - 'Logo': "", - 'Thumb': "", - 'Disc': "", - 'Backdrop': [] + "Primary": "", + "BoxRear": "", + "Art": "", + "Banner": "", + "Logo": "", + "Thumb": "", + "Disc": "", + "Backdrop": [], } - if settings('compressArt.bool'): + if settings("compressArt.bool"): query = "&Quality=90" - if not settings('enableCoverArt.bool'): + if not settings("enableCoverArt.bool"): query += "&EnableImageEnhancers=false" art_maxheight = [360, 480, 600, 720, 1080, -1] - maxheight = art_maxheight[int(settings('maxArtResolution') or 5)] + maxheight = art_maxheight[int(settings("maxArtResolution") or 5)] if maxheight != -1: query += "&MaxHeight=%d" % maxheight - all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query) + all_artwork["Backdrop"] = self.get_backdrops( + obj["Id"], obj["BackdropTags"] or [], query + ) - for artwork in (obj['Tags'] or []): - all_artwork[artwork] = self.get_artwork(obj['Id'], artwork, obj['Tags'][artwork], query) + for artwork in obj["Tags"] or []: + all_artwork[artwork] = self.get_artwork( + obj["Id"], artwork, obj["Tags"][artwork], query + ) if parent_info: - if not all_artwork['Backdrop'] and obj['ParentBackdropId']: - all_artwork['Backdrop'] = self.get_backdrops(obj['ParentBackdropId'], obj['ParentBackdropTags'], query) + if not all_artwork["Backdrop"] and obj["ParentBackdropId"]: + all_artwork["Backdrop"] = self.get_backdrops( + obj["ParentBackdropId"], obj["ParentBackdropTags"], query + ) - for art in ('Logo', 'Art', 'Thumb'): - if not all_artwork[art] and obj['Parent%sId' % art]: - all_artwork[art] = self.get_artwork(obj['Parent%sId' % art], art, obj['Parent%sTag' % art], query) + for art in ("Logo", "Art", "Thumb"): + if not all_artwork[art] and obj["Parent%sId" % art]: + all_artwork[art] = self.get_artwork( + obj["Parent%sId" % art], art, obj["Parent%sTag" % art], query + ) - if obj.get('SeriesTag'): - all_artwork['Series.Primary'] = self.get_artwork(obj['SeriesId'], "Primary", obj['SeriesTag'], query) + if obj.get("SeriesTag"): + all_artwork["Series.Primary"] = self.get_artwork( + obj["SeriesId"], "Primary", obj["SeriesTag"], query + ) - if not all_artwork['Primary']: - all_artwork['Primary'] = all_artwork['Series.Primary'] + if not all_artwork["Primary"]: + all_artwork["Primary"] = all_artwork["Series.Primary"] - elif not all_artwork['Primary'] and obj.get('AlbumId'): - all_artwork['Primary'] = self.get_artwork(obj['AlbumId'], "Primary", obj['AlbumTag'], query) + elif not all_artwork["Primary"] and obj.get("AlbumId"): + all_artwork["Primary"] = self.get_artwork( + obj["AlbumId"], "Primary", obj["AlbumTag"], query + ) return all_artwork def get_backdrops(self, item_id, tags, query=None): - - ''' Get backdrops based of "BackdropImageTags" in the jellyfin object. - ''' + """Get backdrops based of "BackdropImageTags" in the jellyfin object.""" backdrops = [] if item_id is None: @@ -310,15 +325,19 @@ class API(object): for index, tag in enumerate(tags): - artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, (query or "")) + artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % ( + self.server, + item_id, + index, + tag, + (query or ""), + ) backdrops.append(artwork) return backdrops def get_artwork(self, item_id, image, tag=None, query=None): - - ''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc - ''' + """Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc""" if item_id is None: return "" diff --git a/jellyfin_kodi/helper/exceptions.py b/jellyfin_kodi/helper/exceptions.py index 12105ed8..712a64a9 100644 --- a/jellyfin_kodi/helper/exceptions.py +++ b/jellyfin_kodi/helper/exceptions.py @@ -9,7 +9,11 @@ import warnings class HTTPException(Exception): # Jellyfin HTTP exception def __init__(self, status, message): - warnings.warn(f'{self.__class__.__name__} will be deprecated.', DeprecationWarning, stacklevel=2) + warnings.warn( + f"{self.__class__.__name__} will be deprecated.", + DeprecationWarning, + stacklevel=2, + ) self.status = status self.message = message @@ -26,4 +30,5 @@ class PathValidationException(Exception): TODO: Investigate the usage of this to see if it can be done better. """ + pass diff --git a/jellyfin_kodi/helper/lazylogger.py b/jellyfin_kodi/helper/lazylogger.py index cead1f11..bfd15f4b 100644 --- a/jellyfin_kodi/helper/lazylogger.py +++ b/jellyfin_kodi/helper/lazylogger.py @@ -6,6 +6,7 @@ class LazyLogger(object): """`helper.loghandler.getLogger()` is used everywhere. This class helps to avoid import errors. """ + __logger = None __logger_name = None @@ -15,5 +16,6 @@ class LazyLogger(object): def __getattr__(self, name): if self.__logger is None: from .loghandler import getLogger + self.__logger = getLogger(self.__logger_name) return getattr(self.__logger, name) diff --git a/jellyfin_kodi/helper/loghandler.py b/jellyfin_kodi/helper/loghandler.py index ce06ecb1..9d234324 100644 --- a/jellyfin_kodi/helper/loghandler.py +++ b/jellyfin_kodi/helper/loghandler.py @@ -16,8 +16,8 @@ from .utils import translate_path ################################################################################################## -__addon__ = xbmcaddon.Addon(id='plugin.video.jellyfin') -__pluginpath__ = translate_path(__addon__.getAddonInfo('path')) +__addon__ = xbmcaddon.Addon(id="plugin.video.jellyfin") +__pluginpath__ = translate_path(__addon__.getAddonInfo("path")) ################################################################################################## @@ -36,17 +36,17 @@ class LogHandler(logging.StreamHandler): logging.StreamHandler.__init__(self) self.setFormatter(MyFormatter()) - self.sensitive = {'Token': [], 'Server': []} + self.sensitive = {"Token": [], "Server": []} - for server in database.get_credentials()['Servers']: + for server in database.get_credentials()["Servers"]: - if server.get('AccessToken'): - self.sensitive['Token'].append(server['AccessToken']) + if server.get("AccessToken"): + self.sensitive["Token"].append(server["AccessToken"]) - if server.get('address'): - self.sensitive['Server'].append(server['address'].split('://')[1]) + if server.get("address"): + self.sensitive["Server"].append(server["address"].split("://")[1]) - self.mask_info = settings('maskInfo.bool') + self.mask_info = settings("maskInfo.bool") if kodi_version() > 18: self.level = xbmc.LOGINFO @@ -59,10 +59,10 @@ class LogHandler(logging.StreamHandler): string = self.format(record) if self.mask_info: - for server in self.sensitive['Server']: + for server in self.sensitive["Server"]: string = string.replace(server or "{server}", "{jellyfin-server}") - for token in self.sensitive['Token']: + for token in self.sensitive["Token"]: string = string.replace(token or "{token}", "{jellyfin-token}") xbmc.log(string, level=self.level) @@ -74,10 +74,10 @@ class LogHandler(logging.StreamHandler): logging.ERROR: 0, logging.WARNING: 0, logging.INFO: 1, - logging.DEBUG: 2 + logging.DEBUG: 2, } try: - log_level = int(settings('logLevel')) + log_level = int(settings("logLevel")) except ValueError: log_level = 2 # If getting settings fail, we probably want debug logging. @@ -86,7 +86,9 @@ class LogHandler(logging.StreamHandler): class MyFormatter(logging.Formatter): - def __init__(self, fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'): + def __init__( + self, fmt="%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s" + ): logging.Formatter.__init__(self, fmt) def format(self, record): @@ -116,14 +118,14 @@ class MyFormatter(logging.Formatter): res.append(o) - return ''.join(res) + return "".join(res) def _gen_rel_path(self, record): if record.pathname: record.relpath = os.path.relpath(record.pathname, __pluginpath__) -__LOGGER = logging.getLogger('JELLYFIN') +__LOGGER = logging.getLogger("JELLYFIN") for handler in __LOGGER.handlers: __LOGGER.removeHandler(handler) diff --git a/jellyfin_kodi/helper/playutils.py b/jellyfin_kodi/helper/playutils.py index 6fc850fc..baec4b56 100644 --- a/jellyfin_kodi/helper/playutils.py +++ b/jellyfin_kodi/helper/playutils.py @@ -26,81 +26,81 @@ class Transcode(object): Disabled = 3 MediaDefault = 4 + ################################################################################################# def set_properties(item, method, server_id=None): + """Set all properties for playback detection.""" + info = item.get("PlaybackInfo") or {} - ''' Set all properties for playback detection. - ''' - info = item.get('PlaybackInfo') or {} + current = window("jellyfin_play.json") or [] + current.append( + { + "Type": item["Type"], + "Id": item["Id"], + "Path": info["Path"], + "PlayMethod": method, + "PlayOption": "Addon" if info.get("PlaySessionId") else "Native", + "MediaSourceId": info.get("MediaSourceId", item["Id"]), + "Runtime": item.get("RunTimeTicks"), + "PlaySessionId": info.get("PlaySessionId", str(uuid4()).replace("-", "")), + "ServerId": server_id, + "DeviceId": client.get_device_id(), + "SubsMapping": info.get("Subtitles"), + "AudioStreamIndex": info.get("AudioStreamIndex"), + "SubtitleStreamIndex": info.get("SubtitleStreamIndex"), + "CurrentPosition": info.get("CurrentPosition"), + "CurrentEpisode": info.get("CurrentEpisode"), + } + ) - current = window('jellyfin_play.json') or [] - current.append({ - 'Type': item['Type'], - 'Id': item['Id'], - 'Path': info['Path'], - 'PlayMethod': method, - 'PlayOption': 'Addon' if info.get('PlaySessionId') else 'Native', - 'MediaSourceId': info.get('MediaSourceId', item['Id']), - 'Runtime': item.get('RunTimeTicks'), - 'PlaySessionId': info.get('PlaySessionId', str(uuid4()).replace("-", "")), - 'ServerId': server_id, - 'DeviceId': client.get_device_id(), - 'SubsMapping': info.get('Subtitles'), - 'AudioStreamIndex': info.get('AudioStreamIndex'), - 'SubtitleStreamIndex': info.get('SubtitleStreamIndex'), - 'CurrentPosition': info.get('CurrentPosition'), - 'CurrentEpisode': info.get('CurrentEpisode') - }) - - window('jellyfin_play.json', current) + window("jellyfin_play.json", current) class PlayUtils(object): - def __init__(self, item, force_transcode=False, server_id=None, server=None, api_client=None): - - ''' Item will be updated with the property PlaybackInfo, which - holds all the playback information. - ''' + def __init__( + self, item, force_transcode=False, server_id=None, server=None, api_client=None + ): + """Item will be updated with the property PlaybackInfo, which + holds all the playback information. + """ self.item = item - self.item['PlaybackInfo'] = {} + self.item["PlaybackInfo"] = {} self.api_client = api_client self.info = { - 'ServerId': server_id, - 'ServerAddress': server, - 'ForceTranscode': force_transcode, - 'Token': api_client.config.data['auth.token'] + "ServerId": server_id, + "ServerAddress": server, + "ForceTranscode": force_transcode, + "Token": api_client.config.data["auth.token"], } def get_sources(self, source_id=None): - - ''' Return sources based on the optional source_id or the device profile. - ''' - info = self.api_client.get_play_info(self.item['Id'], self.get_device_profile()) + """Return sources based on the optional source_id or the device profile.""" + info = self.api_client.get_play_info(self.item["Id"], self.get_device_profile()) LOG.info(info) - self.info['PlaySessionId'] = info['PlaySessionId'] + self.info["PlaySessionId"] = info["PlaySessionId"] sources = [] - if not info.get('MediaSources'): + if not info.get("MediaSources"): LOG.info("No MediaSources found.") elif source_id: for source in info: - if source['Id'] == source_id: + if source["Id"] == source_id: sources.append(source) break - elif not self.is_selection(info) or len(info['MediaSources']) == 1: + elif not self.is_selection(info) or len(info["MediaSources"]) == 1: LOG.info("Skip source selection.") - sources.append(info['MediaSources'][0]) + sources.append(info["MediaSources"][0]) else: - sources.extend([x for x in info['MediaSources']]) + sources.extend([x for x in info["MediaSources"]]) return sources @@ -110,7 +110,7 @@ class PlayUtils(object): selection = [] for source in sources: - selection.append(source.get('Name', "na")) + selection.append(source.get("Name", "na")) resp = dialog("select", translate(33130), selection) @@ -127,25 +127,23 @@ class PlayUtils(object): return source def is_selection(self, sources): - - ''' Do not allow source selection for. - ''' - if self.item['MediaType'] != 'Video': + """Do not allow source selection for.""" + if self.item["MediaType"] != "Video": LOG.debug("MediaType is not a video.") return False - elif self.item['Type'] == 'TvChannel': + elif self.item["Type"] == "TvChannel": LOG.debug("TvChannel detected.") return False - elif len(sources) == 1 and sources[0]['Type'] == 'Placeholder': + elif len(sources) == 1 and sources[0]["Type"] == "Placeholder": LOG.debug("Placeholder detected.") return False - elif 'SourceType' in self.item and self.item['SourceType'] != 'Library': + elif "SourceType" in self.item and self.item["SourceType"] != "Library": LOG.debug("SourceType not from library.") return False @@ -156,7 +154,7 @@ class PlayUtils(object): self.direct_play(source) - if xbmcvfs.exists(self.info['Path']): + if xbmcvfs.exists(self.info["Path"]): LOG.info("Path exists.") return True @@ -167,7 +165,7 @@ class PlayUtils(object): def is_strm(self, source): - if source.get('Container') == 'strm' or self.item['Path'].endswith('.strm'): + if source.get("Container") == "strm" or self.item["Path"].endswith(".strm"): LOG.info("strm detected") return True @@ -175,31 +173,37 @@ class PlayUtils(object): return False def get(self, source, audio=None, subtitle=None): + """The server returns sources based on the MaxStreamingBitrate value and other filters. + prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working? + """ + self.info["MediaSourceId"] = source["Id"] - ''' The server returns sources based on the MaxStreamingBitrate value and other filters. - prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working? - ''' - self.info['MediaSourceId'] = source['Id'] + if source.get("RequiresClosing"): - if source.get('RequiresClosing'): + """Server returning live tv stream for direct play is hardcoded with 127.0.0.1.""" + self.info["LiveStreamId"] = source["LiveStreamId"] + source["SupportsDirectPlay"] = False + source["Protocol"] = "LiveTV" - ''' Server returning live tv stream for direct play is hardcoded with 127.0.0.1. - ''' - self.info['LiveStreamId'] = source['LiveStreamId'] - source['SupportsDirectPlay'] = False - source['Protocol'] = "LiveTV" + if self.info["ForceTranscode"]: - if self.info['ForceTranscode']: + source["SupportsDirectPlay"] = False + source["SupportsDirectStream"] = False - source['SupportsDirectPlay'] = False - source['SupportsDirectStream'] = False - - if source.get('Protocol') == 'Http' or source['SupportsDirectPlay'] and (self.is_strm(source) or not settings('playFromStream.bool') and self.is_file_exists(source)): + if ( + source.get("Protocol") == "Http" + or source["SupportsDirectPlay"] + and ( + self.is_strm(source) + or not settings("playFromStream.bool") + and self.is_file_exists(source) + ) + ): LOG.info("--[ direct play ]") self.direct_play(source) - elif source['SupportsDirectStream'] or source['SupportsDirectPlay']: + elif source["SupportsDirectStream"] or source["SupportsDirectPlay"]: LOG.info("--[ direct stream ]") self.direct_url(source) @@ -208,158 +212,209 @@ class PlayUtils(object): LOG.info("--[ transcode ]") self.transcode(source, audio, subtitle) - self.info['AudioStreamIndex'] = self.info.get('AudioStreamIndex') or source.get('DefaultAudioStreamIndex') - self.info['SubtitleStreamIndex'] = self.info.get('SubtitleStreamIndex') or source.get('DefaultSubtitleStreamIndex') - self.item['PlaybackInfo'].update(self.info) + self.info["AudioStreamIndex"] = self.info.get("AudioStreamIndex") or source.get( + "DefaultAudioStreamIndex" + ) + self.info["SubtitleStreamIndex"] = self.info.get( + "SubtitleStreamIndex" + ) or source.get("DefaultSubtitleStreamIndex") + self.item["PlaybackInfo"].update(self.info) - API = api.API(self.item, self.info['ServerAddress']) - window('jellyfinfilename', value=API.get_file_path(source.get('Path'))) + API = api.API(self.item, self.info["ServerAddress"]) + window("jellyfinfilename", value=API.get_file_path(source.get("Path"))) def live_stream(self, source): - - ''' Get live stream media info. - ''' - info = self.api_client.get_live_stream(self.item['Id'], self.info['PlaySessionId'], source['OpenToken'], self.get_device_profile()) + """Get live stream media info.""" + info = self.api_client.get_live_stream( + self.item["Id"], + self.info["PlaySessionId"], + source["OpenToken"], + self.get_device_profile(), + ) LOG.info(info) - if info['MediaSource'].get('RequiresClosing'): - self.info['LiveStreamId'] = source['LiveStreamId'] + if info["MediaSource"].get("RequiresClosing"): + self.info["LiveStreamId"] = source["LiveStreamId"] - return info['MediaSource'] + return info["MediaSource"] def transcode(self, source, audio=None, subtitle=None): - if 'TranscodingUrl' not in source: + if "TranscodingUrl" not in source: raise Exception("use get_sources to get transcoding url") - self.info['Method'] = "Transcode" + self.info["Method"] = "Transcode" - if self.item['MediaType'] == 'Video': - base, params = source['TranscodingUrl'].split('?') - url_parsed = params.split('&') - manual_tracks = '' + if self.item["MediaType"] == "Video": + base, params = source["TranscodingUrl"].split("?") + url_parsed = params.split("&") + manual_tracks = "" # manual bitrate - url_parsed = [p for p in url_parsed if 'AudioBitrate' not in p and 'VideoBitrate' not in p] + url_parsed = [ + p + for p in url_parsed + if "AudioBitrate" not in p and "VideoBitrate" not in p + ] - if settings('skipDialogTranscode') != Transcode.Enabled and source.get('MediaStreams'): + if settings("skipDialogTranscode") != Transcode.Enabled and source.get( + "MediaStreams" + ): # manual tracks - url_parsed = [p for p in url_parsed if 'AudioStreamIndex' not in p and 'SubtitleStreamIndex' not in p] + url_parsed = [ + p + for p in url_parsed + if "AudioStreamIndex" not in p and "SubtitleStreamIndex" not in p + ] manual_tracks = self.get_audio_subs(source, audio, subtitle) audio_bitrate = self.get_transcoding_audio_bitrate() video_bitrate = self.get_max_bitrate() - audio_bitrate - params = "%s%s" % ('&'.join(url_parsed), manual_tracks) - params += "&VideoBitrate=%s&AudioBitrate=%s" % (video_bitrate, audio_bitrate) + params = "%s%s" % ("&".join(url_parsed), manual_tracks) + params += "&VideoBitrate=%s&AudioBitrate=%s" % ( + video_bitrate, + audio_bitrate, + ) - video_type = 'live' if source['Protocol'] == 'LiveTV' else 'master' - base = base.replace('stream' if 'stream' in base else 'master', video_type, 1) - self.info['Path'] = "%s%s?%s" % (self.info['ServerAddress'], base, params) - self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution()) + video_type = "live" if source["Protocol"] == "LiveTV" else "master" + base = base.replace( + "stream" if "stream" in base else "master", video_type, 1 + ) + self.info["Path"] = "%s%s?%s" % (self.info["ServerAddress"], base, params) + self.info["Path"] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution()) else: - self.info['Path'] = "%s/%s" % (self.info['ServerAddress'], source['TranscodingUrl']) + self.info["Path"] = "%s/%s" % ( + self.info["ServerAddress"], + source["TranscodingUrl"], + ) - return self.info['Path'] + return self.info["Path"] def direct_play(self, source): - API = api.API(self.item, self.info['ServerAddress']) - self.info['Method'] = "DirectPlay" - self.info['Path'] = API.get_file_path(source.get('Path')) + API = api.API(self.item, self.info["ServerAddress"]) + self.info["Method"] = "DirectPlay" + self.info["Path"] = API.get_file_path(source.get("Path")) - return self.info['Path'] + return self.info["Path"] def direct_url(self, source): - self.info['Method'] = "DirectStream" + self.info["Method"] = "DirectStream" - if self.item['Type'] == "Audio": - self.info['Path'] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % ( - self.info['ServerAddress'], - self.item['Id'], - source.get('Container', "mp4").split(',')[0], - self.info['Token'] + if self.item["Type"] == "Audio": + self.info["Path"] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % ( + self.info["ServerAddress"], + self.item["Id"], + source.get("Container", "mp4").split(",")[0], + self.info["Token"], ) else: - self.info['Path'] = "%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" % ( - self.info['ServerAddress'], - self.item['Id'], - source['Id'], - self.info['Token'] + self.info["Path"] = ( + "%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" + % ( + self.info["ServerAddress"], + self.item["Id"], + source["Id"], + self.info["Token"], + ) ) - return self.info['Path'] + return self.info["Path"] def get_max_bitrate(self): - - ''' Get the video quality based on add-on settings. - Max bit rate supported by server: 2147483 (max signed 32bit integer) - ''' - bitrate = [500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000, - 7000, 8000, 9000, 10000, 12000, 14000, 16000, 18000, - 20000, 25000, 30000, 35000, 40000, 100000, 1000000, 2147483] - return bitrate[int(settings('maxBitrate') or 24)] * 1000 + """Get the video quality based on add-on settings. + Max bit rate supported by server: 2147483 (max signed 32bit integer) + """ + bitrate = [ + 500, + 1000, + 1500, + 2000, + 2500, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 12000, + 14000, + 16000, + 18000, + 20000, + 25000, + 30000, + 35000, + 40000, + 100000, + 1000000, + 2147483, + ] + return bitrate[int(settings("maxBitrate") or 24)] * 1000 def get_resolution(self): - return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight')) + return int(xbmc.getInfoLabel("System.ScreenWidth")), int( + xbmc.getInfoLabel("System.ScreenHeight") + ) def get_directplay_video_codec(self): - codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1', 'vp9', 'av1'] + codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1", "vp9", "av1"] - if settings('transcode_h265.bool'): - codecs.remove('hevc') - codecs.remove('h265') + if settings("transcode_h265.bool"): + codecs.remove("hevc") + codecs.remove("h265") - if settings('transcode_mpeg2.bool'): - codecs.remove('mpeg2video') + if settings("transcode_mpeg2.bool"): + codecs.remove("mpeg2video") - if settings('transcode_vc1.bool'): - codecs.remove('vc1') + if settings("transcode_vc1.bool"): + codecs.remove("vc1") - if settings('transcode_vp9.bool'): - codecs.remove('vp9') + if settings("transcode_vp9.bool"): + codecs.remove("vp9") - if settings('transcode_av1.bool'): - codecs.remove('av1') + if settings("transcode_av1.bool"): + codecs.remove("av1") - return ','.join(codecs) + return ",".join(codecs) def get_transcoding_video_codec(self): - codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1'] + codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1"] - if settings('transcode_h265.bool'): - codecs.remove('hevc') - codecs.remove('h265') + if settings("transcode_h265.bool"): + codecs.remove("hevc") + codecs.remove("h265") else: - if settings('videoPreferredCodec') == 'H265/HEVC': - codecs.insert(2, codecs.pop(codecs.index('h264'))) + if settings("videoPreferredCodec") == "H265/HEVC": + codecs.insert(2, codecs.pop(codecs.index("h264"))) - if settings('transcode_mpeg2.bool'): - codecs.remove('mpeg2video') + if settings("transcode_mpeg2.bool"): + codecs.remove("mpeg2video") - if settings('transcode_vc1.bool'): - codecs.remove('vc1') + if settings("transcode_vc1.bool"): + codecs.remove("vc1") - return ','.join(codecs) + return ",".join(codecs) def get_transcoding_audio_codec(self): - codecs = ['aac', 'mp3', 'ac3', 'opus', 'flac', 'vorbis'] + codecs = ["aac", "mp3", "ac3", "opus", "flac", "vorbis"] - preferred = settings('audioPreferredCodec').lower() + preferred = settings("audioPreferredCodec").lower() if preferred in codecs: codecs.insert(0, codecs.pop(codecs.index(preferred))) - return ','.join(codecs) + return ",".join(codecs) def get_transcoding_audio_bitrate(self): bitrate = [96, 128, 160, 192, 256, 320, 384] - return bitrate[int(settings('audioBitrate') or 6)] * 1000 + return bitrate[int(settings("audioBitrate") or 6)] * 1000 def get_device_profile(self): - - ''' Get device profile based on the add-on settings. - ''' + """Get device profile based on the add-on settings.""" profile = { "Name": "Kodi", "MaxStaticBitrate": self.get_max_bitrate(), @@ -372,154 +427,96 @@ class PlayUtils(object): "Container": "m3u8", "AudioCodec": self.get_transcoding_audio_codec(), "VideoCodec": self.get_transcoding_video_codec(), - "MaxAudioChannels": settings('audioMaxChannels') + "MaxAudioChannels": settings("audioMaxChannels"), }, - { - "Type": "Audio" - }, - { - "Type": "Photo", - "Container": "jpeg" - } + {"Type": "Audio"}, + {"Type": "Photo", "Container": "jpeg"}, ], "DirectPlayProfiles": [ - { - "Type": "Video", - "VideoCodec": self.get_directplay_video_codec() - }, - { - "Type": "Audio" - }, - { - "Type": "Photo" - } + {"Type": "Video", "VideoCodec": self.get_directplay_video_codec()}, + {"Type": "Audio"}, + {"Type": "Photo"}, ], "ResponseProfiles": [], "ContainerProfiles": [], "CodecProfiles": [], "SubtitleProfiles": [ - { - "Format": "srt", - "Method": "External" - }, - { - "Format": "srt", - "Method": "Embed" - }, - { - "Format": "ass", - "Method": "External" - }, - { - "Format": "ass", - "Method": "Embed" - }, - { - "Format": "sub", - "Method": "Embed" - }, - { - "Format": "sub", - "Method": "External" - }, - { - "Format": "ssa", - "Method": "Embed" - }, - { - "Format": "ssa", - "Method": "External" - }, - { - "Format": "smi", - "Method": "Embed" - }, - { - "Format": "smi", - "Method": "External" - }, - { - "Format": "pgssub", - "Method": "Embed" - }, - { - "Format": "pgssub", - "Method": "External" - }, - { - "Format": "dvdsub", - "Method": "Embed" - }, - { - "Format": "dvdsub", - "Method": "External" - }, - { - "Format": "pgs", - "Method": "Embed" - }, - { - "Format": "pgs", - "Method": "External" - } - ] + {"Format": "srt", "Method": "External"}, + {"Format": "srt", "Method": "Embed"}, + {"Format": "ass", "Method": "External"}, + {"Format": "ass", "Method": "Embed"}, + {"Format": "sub", "Method": "Embed"}, + {"Format": "sub", "Method": "External"}, + {"Format": "ssa", "Method": "Embed"}, + {"Format": "ssa", "Method": "External"}, + {"Format": "smi", "Method": "Embed"}, + {"Format": "smi", "Method": "External"}, + {"Format": "pgssub", "Method": "Embed"}, + {"Format": "pgssub", "Method": "External"}, + {"Format": "dvdsub", "Method": "Embed"}, + {"Format": "dvdsub", "Method": "External"}, + {"Format": "pgs", "Method": "Embed"}, + {"Format": "pgs", "Method": "External"}, + ], } - if settings('transcodeHi10P.bool'): - profile['CodecProfiles'].append( + if settings("transcodeHi10P.bool"): + profile["CodecProfiles"].append( { - 'Type': 'Video', - 'codec': 'h264', - 'Conditions': [ + "Type": "Video", + "codec": "h264", + "Conditions": [ { - 'Condition': "LessThanEqual", - 'Property': "VideoBitDepth", - 'Value': "8" + "Condition": "LessThanEqual", + "Property": "VideoBitDepth", + "Value": "8", } - ] + ], } ) - if settings('transcode_h265_rext.bool'): - profile['CodecProfiles'].append( + if settings("transcode_h265_rext.bool"): + profile["CodecProfiles"].append( { - 'Type': 'Video', - 'codec': 'h265,hevc', - 'Conditions': [ + "Type": "Video", + "codec": "h265,hevc", + "Conditions": [ { - 'Condition': "EqualsAny", - 'Property': "VideoProfile", - 'Value': "main|main 10" + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", } - ] + ], } ) - if self.info['ForceTranscode']: - profile['DirectPlayProfiles'] = [] + if self.info["ForceTranscode"]: + profile["DirectPlayProfiles"] = [] - if self.item['Type'] == 'TvChannel': - profile['TranscodingProfiles'].insert(0, { - "Container": "ts", - "Type": "Video", - "AudioCodec": "mp3,aac", - "VideoCodec": "h264", - "Context": "Streaming", - "Protocol": "hls", - "MaxAudioChannels": "2", - "MinSegments": "1", - "BreakOnNonKeyFrames": True - }) + if self.item["Type"] == "TvChannel": + profile["TranscodingProfiles"].insert( + 0, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "mp3,aac", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "1", + "BreakOnNonKeyFrames": True, + }, + ) return profile def set_external_subs(self, source, listitem): - - ''' Try to download external subs locally, so we can label them. - Since Jellyfin returns all possible tracks together, sort them. - IsTextSubtitleStream if true, is available to download from server. - ''' - if not settings('enableExternalSubs.bool') or not source['MediaStreams']: + """Try to download external subs locally, so we can label them. + Since Jellyfin returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + """ + if not settings("enableExternalSubs.bool") or not source["MediaStreams"]: return subs = [] @@ -528,12 +525,19 @@ class PlayUtils(object): server_settings = self.api_client.get_transcode_settings() - for stream in source['MediaStreams']: - if stream['SupportsExternalStream'] and stream['Type'] == 'Subtitle' and stream['DeliveryMethod'] == 'External': - if not stream['IsExternal'] and not server_settings['EnableSubtitleExtraction']: + for stream in source["MediaStreams"]: + if ( + stream["SupportsExternalStream"] + and stream["Type"] == "Subtitle" + and stream["DeliveryMethod"] == "External" + ): + if ( + not stream["IsExternal"] + and not server_settings["EnableSubtitleExtraction"] + ): continue - index = stream['Index'] + index = stream["Index"] url = self.get_subtitles(source, stream, index) if url is None: @@ -541,8 +545,12 @@ class PlayUtils(object): LOG.info("[ subtitles/%s ] %s", index, url) - if 'Language' in stream: - filename = "%s.%s.%s" % (source['Id'], stream['Language'], stream['Codec']) + if "Language" in stream: + filename = "%s.%s.%s" % ( + source["Id"], + stream["Language"], + stream["Codec"], + ) try: subs.append(self.download_external_subs(url, filename)) @@ -556,15 +564,16 @@ class PlayUtils(object): kodi += 1 listitem.setSubtitles(subs) - self.item['PlaybackInfo']['Subtitles'] = mapping + self.item["PlaybackInfo"]["Subtitles"] = mapping @classmethod def download_external_subs(cls, src, filename): - - ''' Download external subtitles to temp folder - to be able to have proper names to streams. - ''' - temp = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/") + """Download external subtitles to temp folder + to be able to have proper names to streams. + """ + temp = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/temp/" + ) if not xbmcvfs.exists(temp): xbmcvfs.mkdir(temp) @@ -572,59 +581,61 @@ class PlayUtils(object): path = os.path.join(temp, filename) try: - response = requests.get(src, stream=True, verify=settings('sslverify.bool')) + response = requests.get(src, stream=True, verify=settings("sslverify.bool")) response.raise_for_status() except Exception as error: LOG.exception(error) raise else: - response.encoding = 'utf-8' - with open(path, 'wb') as f: + response.encoding = "utf-8" + with open(path, "wb") as f: f.write(response.content) del response return path def get_audio_subs(self, source, audio=None, subtitle=None): + """For transcoding only + Present the list of audio/subs to select from, before playback starts. - ''' For transcoding only - Present the list of audio/subs to select from, before playback starts. - - Since Jellyfin returns all possible tracks together, sort them. - IsTextSubtitleStream if true, is available to download from server. - ''' + Since Jellyfin returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + """ prefs = "" audio_streams = list() subs_streams = list() - streams = source['MediaStreams'] + streams = source["MediaStreams"] server_settings = self.api_client.get_transcode_settings() - allow_burned_subs = settings('allowBurnedSubs.bool') + allow_burned_subs = settings("allowBurnedSubs.bool") for stream in streams: - index = stream['Index'] - stream_type = stream['Type'] + index = stream["Index"] + stream_type = stream["Type"] - if stream_type == 'Audio': + if stream_type == "Audio": audio_streams.append(index) - elif stream_type == 'Subtitle': - if stream['IsExternal']: - if not stream['SupportsExternalStream'] and not allow_burned_subs: + elif stream_type == "Subtitle": + if stream["IsExternal"]: + if not stream["SupportsExternalStream"] and not allow_burned_subs: continue else: - avail_for_extraction = stream['SupportsExternalStream'] and server_settings['EnableSubtitleExtraction'] + avail_for_extraction = ( + stream["SupportsExternalStream"] + and server_settings["EnableSubtitleExtraction"] + ) if not avail_for_extraction and not allow_burned_subs: continue subs_streams.append(index) - skip_dialog = int(settings('skipDialogTranscode') or 0) + skip_dialog = int(settings("skipDialogTranscode") or 0) def get_track_title(track_index): - return streams[track_index]['DisplayTitle'] or ("Track %s" % track_index) + return streams[track_index]["DisplayTitle"] or ("Track %s" % track_index) # Select audio stream audio_selected = None @@ -633,7 +644,7 @@ class PlayUtils(object): # NOTE: "DefaultAudioStreamIndex" is the default according to Jellyfin. # The media's default is marked by the "IsDefault" value. for track_index in audio_streams: - if streams[track_index]['IsDefault']: + if streams[track_index]["IsDefault"]: audio = track_index break @@ -648,16 +659,16 @@ class PlayUtils(object): if resp > -1: audio_selected = audio_streams[resp] else: - audio_selected = source['DefaultAudioStreamIndex'] + audio_selected = source["DefaultAudioStreamIndex"] elif audio_streams: # Only one choice audio_selected = audio_streams[0] else: - audio_selected = source['DefaultAudioStreamIndex'] + audio_selected = source["DefaultAudioStreamIndex"] if audio_selected is not None: - self.info['AudioStreamIndex'] = audio_selected + self.info["AudioStreamIndex"] = audio_selected prefs += "&AudioStreamIndex=%s" % audio_selected # Select audio stream @@ -665,7 +676,7 @@ class PlayUtils(object): if skip_dialog == Transcode.MediaDefault: for track_index in subs_streams: - if streams[track_index]['IsDefault']: + if streams[track_index]["IsDefault"]: subtitle = track_index break @@ -673,7 +684,9 @@ class PlayUtils(object): subtitle_selected = subtitle elif skip_dialog in (Transcode.Enabled, Transcode.Subtitle) and subs_streams: - selection = list(['No subtitles']) + list(map(get_track_title, subs_streams)) + selection = list(["No subtitles"]) + list( + map(get_track_title, subs_streams) + ) resp = dialog("select", translate(33014), selection) - 1 # Possible responses: # >=0 Subtitle track @@ -686,27 +699,36 @@ class PlayUtils(object): if subtitle_selected is not None: server_settings = self.api_client.get_transcode_settings() stream = streams[track_index] - if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']: - self.info['SubtitleUrl'] = self.get_subtitles(source, stream, subtitle_selected) - self.info['SubtitleStreamIndex'] = subtitle_selected + if ( + server_settings["EnableSubtitleExtraction"] + and stream["SupportsExternalStream"] + ): + self.info["SubtitleUrl"] = self.get_subtitles( + source, stream, subtitle_selected + ) + self.info["SubtitleStreamIndex"] = subtitle_selected elif allow_burned_subs: prefs += "&SubtitleStreamIndex=%s" % subtitle_selected - self.info['SubtitleStreamIndex'] = subtitle_selected + self.info["SubtitleStreamIndex"] = subtitle_selected return prefs def get_subtitles(self, source, stream, index): - if stream['IsTextSubtitleStream'] and 'DeliveryUrl' in stream and stream['DeliveryUrl'].lower().startswith('/videos'): - url = "%s%s" % (self.info['ServerAddress'], stream['DeliveryUrl']) + if ( + stream["IsTextSubtitleStream"] + and "DeliveryUrl" in stream + and stream["DeliveryUrl"].lower().startswith("/videos") + ): + url = "%s%s" % (self.info["ServerAddress"], stream["DeliveryUrl"]) else: url = "%s/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % ( - self.info['ServerAddress'], - self.item['Id'], - source['Id'], + self.info["ServerAddress"], + self.item["Id"], + source["Id"], index, - stream['Codec'], - self.info['Token'] + stream["Codec"], + self.info["Token"], ) return url diff --git a/jellyfin_kodi/helper/translate.py b/jellyfin_kodi/helper/translate.py index 2714e9ee..781890c7 100644 --- a/jellyfin_kodi/helper/translate.py +++ b/jellyfin_kodi/helper/translate.py @@ -15,13 +15,11 @@ LOG = LazyLogger(__name__) def translate(string): - - ''' Get add-on string. Returns in unicode. - ''' + """Get add-on string. Returns in unicode.""" if type(string) != int: string = STRINGS[string] - result = xbmcaddon.Addon('plugin.video.jellyfin').getLocalizedString(string) + result = xbmcaddon.Addon("plugin.video.jellyfin").getLocalizedString(string) if not result: result = xbmc.getLocalizedString(string) @@ -30,23 +28,23 @@ def translate(string): STRINGS = { - 'addon_name': 29999, - 'playback_mode': 30511, - 'empty_user': 30613, - 'empty_user_pass': 30608, - 'empty_server': 30617, - 'network_credentials': 30517, - 'invalid_auth': 33009, - 'addon_mode': 33036, - 'native_mode': 33037, - 'cancel': 30606, - 'username': 30024, - 'password': 30602, - 'gathering': 33021, - 'boxsets': 30185, - 'movies': 30302, - 'tvshows': 30305, - 'fav_movies': 30180, - 'fav_tvshows': 30181, - 'fav_episodes': 30182 + "addon_name": 29999, + "playback_mode": 30511, + "empty_user": 30613, + "empty_user_pass": 30608, + "empty_server": 30617, + "network_credentials": 30517, + "invalid_auth": 33009, + "addon_mode": 33036, + "native_mode": 33037, + "cancel": 30606, + "username": 30024, + "password": 30602, + "gathering": 33021, + "boxsets": 30185, + "movies": 30302, + "tvshows": 30305, + "fav_movies": 30180, + "fav_tvshows": 30181, + "fav_episodes": 30182, } diff --git a/jellyfin_kodi/helper/utils.py b/jellyfin_kodi/helper/utils.py index 9d4bfdef..fb651cf6 100644 --- a/jellyfin_kodi/helper/utils.py +++ b/jellyfin_kodi/helper/utils.py @@ -40,62 +40,59 @@ def kodi_version(): else: default_versionstring = "19.1 (19.1.0) Git:20210509-85e05228b4" - version_string = xbmc.getInfoLabel('System.BuildVersion') or default_versionstring - return int(version_string.split(' ', 1)[0].split('.', 1)[0]) + version_string = xbmc.getInfoLabel("System.BuildVersion") or default_versionstring + return int(version_string.split(" ", 1)[0].split(".", 1)[0]) def window(key, value=None, clear=False, window_id=10000): - - ''' Get or set window properties. - ''' + """Get or set window properties.""" window = xbmcgui.Window(window_id) if clear: LOG.debug("--[ window clear: %s ]", key) - window.clearProperty(key.replace('.json', "").replace('.bool', "")) + window.clearProperty(key.replace(".json", "").replace(".bool", "")) elif value is not None: - if key.endswith('.json'): + if key.endswith(".json"): - key = key.replace('.json', "") + key = key.replace(".json", "") value = json.dumps(value) - elif key.endswith('.bool'): + elif key.endswith(".bool"): - key = key.replace('.bool', "") + key = key.replace(".bool", "") value = "true" if value else "false" window.setProperty(key, value) else: - result = window.getProperty(key.replace('.json', "").replace('.bool', "")) + result = window.getProperty(key.replace(".json", "").replace(".bool", "")) if result: - if key.endswith('.json'): + if key.endswith(".json"): result = json.loads(result) - elif key.endswith('.bool'): + elif key.endswith(".bool"): result = result in ("true", "1") return result def settings(setting, value=None): - - ''' Get or add add-on settings. - getSetting returns unicode object. - ''' + """Get or add add-on settings. + getSetting returns unicode object. + """ addon = xbmcaddon.Addon(addon_id()) if value is not None: - if setting.endswith('.bool'): + if setting.endswith(".bool"): - setting = setting.replace('.bool', "") + setting = setting.replace(".bool", "") value = "true" if value else "false" addon.setSetting(setting, value) else: - result = addon.getSetting(setting.replace('.bool', "")) + result = addon.getSetting(setting.replace(".bool", "")) - if result and setting.endswith('.bool'): + if result and setting.endswith(".bool"): result = result in ("true", "1") return result @@ -106,9 +103,7 @@ def create_id(): def find(dict, item): - - ''' Find value in dictionary. - ''' + """Find value in dictionary.""" if item in dict: return dict[item] @@ -119,9 +114,7 @@ def find(dict, item): def event(method, data=None, sender=None, hexlify=False): - - ''' Data is a dictionary. - ''' + """Data is a dictionary.""" data = data or {} sender = sender or "plugin.video.jellyfin" @@ -132,7 +125,7 @@ def event(method, data=None, sender=None, hexlify=False): LOG.debug("---[ event: %s/%s ] %s", sender, method, data) - xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data)) + xbmc.executebuiltin("NotifyAll(%s, %s, %s)" % (sender, method, data)) def dialog(dialog_type, *args, **kwargs): @@ -140,63 +133,58 @@ def dialog(dialog_type, *args, **kwargs): d = xbmcgui.Dialog() if "icon" in kwargs: - kwargs['icon'] = kwargs['icon'].replace( + kwargs["icon"] = kwargs["icon"].replace( "{jellyfin}", - "special://home/addons/plugin.video.jellyfin/resources/icon.png" + "special://home/addons/plugin.video.jellyfin/resources/icon.png", ) if "heading" in kwargs: - kwargs['heading'] = kwargs['heading'].replace("{jellyfin}", translate('addon_name')) + kwargs["heading"] = kwargs["heading"].replace( + "{jellyfin}", translate("addon_name") + ) if args: args = list(args) - args[0] = args[0].replace("{jellyfin}", translate('addon_name')) + args[0] = args[0].replace("{jellyfin}", translate("addon_name")) types = { - 'yesno': d.yesno, - 'ok': d.ok, - 'notification': d.notification, - 'input': d.input, - 'select': d.select, - 'numeric': d.numeric, - 'multi': d.multiselect + "yesno": d.yesno, + "ok": d.ok, + "notification": d.notification, + "input": d.input, + "select": d.select, + "numeric": d.numeric, + "multi": d.multiselect, } return types[dialog_type](*args, **kwargs) def should_stop(): - - ''' Checkpoint during the sync process. - ''' + """Checkpoint during the sync process.""" if xbmc.Monitor().waitForAbort(0.00001): return True - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): LOG.info("exiiiiitttinggg") return True - return not window('jellyfin_online.bool') + return not window("jellyfin_online.bool") def get_screensaver(): - - ''' Get the current screensaver value. - ''' - result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) + """Get the current screensaver value.""" + result = JSONRPC("Settings.getSettingValue").execute( + {"setting": "screensaver.mode"} + ) try: - return result['result']['value'] + return result["result"]["value"] except KeyError: return "" def set_screensaver(value): - - ''' Toggle the screensaver - ''' - params = { - 'setting': "screensaver.mode", - 'value': value - } - result = JSONRPC('Settings.setSettingValue').execute(params) + """Toggle the screensaver""" + params = {"setting": "screensaver.mode", "value": value} + result = JSONRPC("Settings.setSettingValue").execute(params) LOG.info("---[ screensaver/%s ] %s", value, result) @@ -215,12 +203,12 @@ class JSONRPC(object): def _query(self): query = { - 'jsonrpc': self.jsonrpc_version, - 'id': self.id, - 'method': self.method, + "jsonrpc": self.jsonrpc_version, + "id": self.id, + "method": self.method, } if self.params is not None: - query['params'] = self.params + query["params"] = self.params return json.dumps(query) @@ -231,66 +219,68 @@ class JSONRPC(object): def validate(path): - - ''' Verify if path is accessible. - ''' - if window('jellyfin_pathverified.bool'): + """Verify if path is accessible.""" + if window("jellyfin_pathverified.bool"): return True if not xbmcvfs.exists(path): LOG.info("Could not find %s", path) - if dialog("yesno", "{jellyfin}", "%s %s. %s" % (translate(33047), path, translate(33048))): + if dialog( + "yesno", + "{jellyfin}", + "%s %s. %s" % (translate(33047), path, translate(33048)), + ): return False - window('jellyfin_pathverified.bool', True) + window("jellyfin_pathverified.bool", True) return True def validate_bluray_dir(path): + """Verify if path/BDMV/ is accessible.""" - ''' Verify if path/BDMV/ is accessible. - ''' - - path = path + '/BDMV/' + path = path + "/BDMV/" if not xbmcvfs.exists(path): return False - window('jellyfin_pathverified.bool', True) + window("jellyfin_pathverified.bool", True) return True def validate_dvd_dir(path): + """Verify if path/VIDEO_TS/ is accessible.""" - ''' Verify if path/VIDEO_TS/ is accessible. - ''' - - path = path + '/VIDEO_TS/' + path = path + "/VIDEO_TS/" if not xbmcvfs.exists(path): return False - window('jellyfin_pathverified.bool', True) + window("jellyfin_pathverified.bool", True) return True def values(item, keys): - - ''' Grab the values in the item for a list of keys {key},{key1}.... - If the key has no brackets, the key will be passed as is. - ''' - return (item[key.replace('{', "").replace('}', "")] if isinstance(key, text_type) and key.startswith('{') else key for key in keys) + """Grab the values in the item for a list of keys {key},{key1}.... + If the key has no brackets, the key will be passed as is. + """ + return ( + ( + item[key.replace("{", "").replace("}", "")] + if isinstance(key, text_type) and key.startswith("{") + else key + ) + for key in keys + ) def delete_folder(path): - - ''' Delete objects from kodi cache - ''' + """Delete objects from kodi cache""" LOG.debug("--[ delete folder ]") dirs, files = xbmcvfs.listdir(path) @@ -305,9 +295,7 @@ def delete_folder(path): def delete_recursive(path, dirs): - - ''' Delete files and dirs recursively. - ''' + """Delete files and dirs recursively.""" for directory in dirs: dirs2, files = xbmcvfs.listdir(os.path.join(path, directory)) @@ -319,11 +307,9 @@ def delete_recursive(path, dirs): def unzip(path, dest, folder=None): - - ''' Unzip file. zipfile module seems to fail on android with badziperror. - ''' + """Unzip file. zipfile module seems to fail on android with badziperror.""" path = quote_plus(path) - root = "zip://" + path + '/' + root = "zip://" + path + "/" if folder: @@ -360,9 +346,7 @@ def unzip_recursive(path, dirs, dest): def unzip_file(path, dest): - - ''' Unzip specific file. Path should start with zip:// - ''' + """Unzip specific file. Path should start with zip://""" xbmcvfs.copy(path, dest) LOG.debug("unzip: %s to %s", path, dest) @@ -381,9 +365,7 @@ def get_zip_directory(path, folder): def copytree(path, dest): - - ''' Copy folder content from one to another. - ''' + """Copy folder content from one to another.""" dirs, files = xbmcvfs.listdir(path) if not xbmcvfs.exists(dest): @@ -416,10 +398,8 @@ def copy_recursive(path, dirs, dest): def copy_file(path, dest): - - ''' Copy specific file. - ''' - if path.endswith('.pyo'): + """Copy specific file.""" + if path.endswith(".pyo"): return xbmcvfs.copy(path, dest) @@ -427,11 +407,10 @@ def copy_file(path, dest): def normalize_string(text): - - ''' For theme media, do not modify unless modified in TV Tunes. - Remove dots from the last character as windows can not have directories - with dots at the end - ''' + """For theme media, do not modify unless modified in TV Tunes. + Remove dots from the last character as windows can not have directories + with dots at the end + """ text = text.replace(":", "") text = text.replace("/", "-") text = text.replace("\\", "-") @@ -439,26 +418,24 @@ def normalize_string(text): text = text.replace(">", "") text = text.replace("*", "") text = text.replace("?", "") - text = text.replace('|', "") + text = text.replace("|", "") text = text.strip() - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', text_type(text, 'utf-8')).encode('ascii', 'ignore') + text = text.rstrip(".") + text = unicodedata.normalize("NFKD", text_type(text, "utf-8")).encode( + "ascii", "ignore" + ) return text def split_list(itemlist, size): - - ''' Split up list in pieces of size. Will generate a list of lists - ''' - return [itemlist[i:i + size] for i in range(0, len(itemlist), size)] + """Split up list in pieces of size. Will generate a list of lists""" + return [itemlist[i : i + size] for i in range(0, len(itemlist), size)] def convert_to_local(date, timezone=tz.tzlocal()): - - ''' Convert the local datetime to local. - ''' + """Convert the local datetime to local.""" try: date = parser.parse(date) if isinstance(date, string_types) else date date = date.replace(tzinfo=tz.tzutc()) @@ -475,9 +452,9 @@ def convert_to_local(date, timezone=tz.tzlocal()): date.second, ) else: - return date.strftime('%Y-%m-%dT%H:%M:%S') + return date.strftime("%Y-%m-%dT%H:%M:%S") except Exception as error: - LOG.exception('Item date: {} --- {}'.format(str(date), error)) + LOG.exception("Item date: {} --- {}".format(str(date), error)) return str(date) @@ -491,28 +468,27 @@ def has_attribute(obj, name): def set_addon_mode(): + """Setup playback mode. If native mode selected, check network credentials.""" + value = dialog( + "yesno", + translate("playback_mode"), + translate(33035), + nolabel=translate("addon_mode"), + yeslabel=translate("native_mode"), + ) - ''' Setup playback mode. If native mode selected, check network credentials. - ''' - value = dialog("yesno", - translate('playback_mode'), - translate(33035), - nolabel=translate('addon_mode'), - yeslabel=translate('native_mode')) - - settings('useDirectPaths', value="1" if value else "0") + settings("useDirectPaths", value="1" if value else "0") if value: dialog("ok", "{jellyfin}", translate(33145)) - LOG.info("Add-on playback: %s", settings('useDirectPaths') == "0") + LOG.info("Add-on playback: %s", settings("useDirectPaths") == "0") class JsonDebugPrinter(object): - - ''' Helper class to defer converting data to JSON until it is needed. + """Helper class to defer converting data to JSON until it is needed. See: https://github.com/jellyfin/jellyfin-kodi/pull/193 - ''' + """ def __init__(self, data): self.data = data @@ -527,8 +503,8 @@ def get_filesystem_encoding(): if not enc: enc = sys.getdefaultencoding() - if not enc or enc == 'ascii': - enc = 'utf-8' + if not enc or enc == "ascii": + enc = "utf-8" return enc @@ -537,20 +513,20 @@ def find_library(server, item): from ..database import get_sync sync = get_sync() - whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] - ancestors = server.jellyfin.get_ancestors(item['Id']) + whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]] + ancestors = server.jellyfin.get_ancestors(item["Id"]) for ancestor in ancestors: - if ancestor['Id'] in whitelist: + if ancestor["Id"] in whitelist: return ancestor - LOG.error('No ancestor found, not syncing item with ID: {}'.format(item['Id'])) + LOG.error("No ancestor found, not syncing item with ID: {}".format(item["Id"])) return {} def translate_path(path): - ''' + """ Use new library location for translate path starting in Kodi 19 - ''' + """ version = kodi_version() if version > 18: diff --git a/jellyfin_kodi/helper/wrapper.py b/jellyfin_kodi/helper/wrapper.py index 1b814da1..8dc59745 100644 --- a/jellyfin_kodi/helper/wrapper.py +++ b/jellyfin_kodi/helper/wrapper.py @@ -19,9 +19,8 @@ LOG = LazyLogger(__name__) def progress(message=None): + """Will start and close the progress dialog.""" - ''' Will start and close the progress dialog. - ''' def decorator(func): def wrapper(self, item=None, *args, **kwargs): @@ -29,10 +28,13 @@ def progress(message=None): if item and type(item) == dict: - dialog.create(translate('addon_name'), "%s %s" % (translate('gathering'), item['Name'])) - LOG.info("Processing %s: %s", item['Name'], item['Id']) + dialog.create( + translate("addon_name"), + "%s %s" % (translate("gathering"), item["Name"]), + ) + LOG.info("Processing %s: %s", item["Name"], item["Id"]) else: - dialog.create(translate('addon_name'), message) + dialog.create(translate("addon_name"), message) LOG.info("Processing %s", message) if item: @@ -44,13 +46,13 @@ def progress(message=None): return result return wrapper + return decorator def stop(func): + """Wrapper to catch exceptions and return using catch""" - ''' Wrapper to catch exceptions and return using catch - ''' def wrapper(*args, **kwargs): try: @@ -68,11 +70,12 @@ def stop(func): def jellyfin_item(func): + """Wrapper to retrieve the jellyfin_db item.""" - ''' Wrapper to retrieve the jellyfin_db item. - ''' def wrapper(self, item, *args, **kwargs): - e_item = self.jellyfin_db.get_item_by_id(item['Id'] if type(item) == dict else item) + e_item = self.jellyfin_db.get_item_by_id( + item["Id"] if type(item) == dict else item + ) return func(self, item, e_item=e_item, *args, **kwargs) diff --git a/jellyfin_kodi/helper/xmls.py b/jellyfin_kodi/helper/xmls.py index 64c93440..fdf9e3a9 100644 --- a/jellyfin_kodi/helper/xmls.py +++ b/jellyfin_kodi/helper/xmls.py @@ -19,45 +19,42 @@ LOG = LazyLogger(__name__) def tvtunes_nfo(path, urls): - - ''' Create tvtunes.nfo - ''' + """Create tvtunes.nfo""" try: xml = etree.parse(path).getroot() except Exception: - xml = etree.Element('tvtunes') + xml = etree.Element("tvtunes") - for elem in xml.getiterator('tvtunes'): + for elem in xml.getiterator("tvtunes"): for file in list(elem): elem.remove(file) for url in urls: - etree.SubElement(xml, 'file').text = url + etree.SubElement(xml, "file").text = url tree = etree.ElementTree(xml) tree.write(path) def advanced_settings(): - - ''' Track the existence of true - It is incompatible with plugin paths. - ''' - if settings('useDirectPaths') != "0": + """Track the existence of true + It is incompatible with plugin paths. + """ + if settings("useDirectPaths") != "0": return path = translate_path("special://profile/") - file = os.path.join(path, 'advancedsettings.xml') + file = os.path.join(path, "advancedsettings.xml") try: xml = etree.parse(file).getroot() except Exception: return - video = xml.find('videolibrary') + video = xml.find("videolibrary") if video is not None: - cleanonupdate = video.find('cleanonupdate') + cleanonupdate = video.find("cleanonupdate") if cleanonupdate is not None and cleanonupdate.text == "true": @@ -68,13 +65,13 @@ def advanced_settings(): tree.write(file) dialog("ok", "{jellyfin}", translate(33097)) - xbmc.executebuiltin('RestartApp') + xbmc.executebuiltin("RestartApp") return True + def verify_kodi_defaults(): - ''' Make sure we have the kodi default folder in place. - ''' + """Make sure we have the kodi default folder in place.""" source_base_path = translate_path("special://xbmc/system/library/video") dest_base_path = translate_path("special://profile/library/video") @@ -97,11 +94,15 @@ def verify_kodi_defaults(): if not os.path.exists(dest_file): copy = True - elif os.path.splitext(file_name)[1].lower() == '.xml': + elif os.path.splitext(file_name)[1].lower() == ".xml": try: etree.parse(dest_file) except etree.ParseError: - LOG.warning("Unable to parse `{}`, recovering from default.".format(dest_file)) + LOG.warning( + "Unable to parse `{}`, recovering from default.".format( + dest_file + ) + ) copy = True if copy: @@ -112,7 +113,7 @@ def verify_kodi_defaults(): # This code seems to enforce a fixed ordering. # Is it really desirable to force this on users? # The default (system wide) order is [10, 20, 30] in Kodi 19. - for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): + for index, node in enumerate(["movies", "tvshows", "musicvideos"]): file_name = os.path.join(dest_base_path, node, "index.xml") if xbmcvfs.exists(file_name): @@ -126,8 +127,8 @@ def verify_kodi_defaults(): tree = None if tree is not None: - tree.getroot().set('order', str(17 + index)) - with xbmcvfs.File(file_name, 'w') as f: + tree.getroot().set("order", str(17 + index)) + with xbmcvfs.File(file_name, "w") as f: f.write(etree.tostring(tree.getroot())) playlist_path = translate_path("special://profile/playlists/video") diff --git a/jellyfin_kodi/jellyfin/__init__.py b/jellyfin_kodi/jellyfin/__init__.py index 6d0bcdec..c9aee294 100644 --- a/jellyfin_kodi/jellyfin/__init__.py +++ b/jellyfin_kodi/jellyfin/__init__.py @@ -25,22 +25,22 @@ def ensure_client(): return func(self, *args, **kwargs) return wrapper + return decorator class Jellyfin(object): + """This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing + to communicate with the JellyfinClient(). - ''' This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing - to communicate with the JellyfinClient(). + from jellyfin_kodi.jellyfin import Jellyfin - from jellyfin_kodi.jellyfin import Jellyfin + Jellyfin('123456').config.data['app'] - Jellyfin('123456').config.data['app'] - - # Permanent client reference - client = Jellyfin('123456').get_client() - client.config.data['app'] - ''' + # Permanent client reference + client = Jellyfin('123456').get_client() + client.config.data['app'] + """ # Borg - multiple instances, shared state _shared_state = {} @@ -94,7 +94,7 @@ class Jellyfin(object): self.client[self.server_id] = JellyfinClient() - if self.server_id == 'default': + if self.server_id == "default": LOG.info("---[ START JELLYFINCLIENT ]---") else: LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id) diff --git a/jellyfin_kodi/jellyfin/api.py b/jellyfin_kodi/jellyfin/api.py index a65c5836..4ec7ce31 100644 --- a/jellyfin_kodi/jellyfin/api.py +++ b/jellyfin_kodi/jellyfin/api.py @@ -15,7 +15,7 @@ LOG = LazyLogger(__name__) def jellyfin_url(client, handler): - return "%s/%s" % (client.config.data['auth.server'], handler) + return "%s/%s" % (client.config.data["auth.server"], handler) def basic_info(): @@ -42,9 +42,8 @@ def music_info(): class API(object): + """All the api calls to the server.""" - ''' All the api calls to the server. - ''' def __init__(self, client, *args, **kwargs): self.client = client self.config = client.config @@ -54,18 +53,18 @@ class API(object): if request is None: request = {} - request.update({'type': action, 'handler': url}) + request.update({"type": action, "handler": url}) return self.client.request(request) def _get(self, handler, params=None): - return self._http("GET", handler, {'params': params}) + return self._http("GET", handler, {"params": params}) def _post(self, handler, json=None, params=None): - return self._http("POST", handler, {'params': params, 'json': json}) + return self._http("POST", handler, {"params": params, "json": json}) def _delete(self, handler, params=None): - return self._http("DELETE", handler, {'params': params}) + return self._http("DELETE", handler, {"params": params}) ################################################################################################# @@ -111,9 +110,17 @@ class API(object): def artwork(self, item_id, art, max_width, ext="jpg", index=None): if index is None: - return jellyfin_url(self.client, "Items/%s/Images/%s?MaxWidth=%s&format=%s" % (item_id, art, max_width, ext)) + return jellyfin_url( + self.client, + "Items/%s/Images/%s?MaxWidth=%s&format=%s" + % (item_id, art, max_width, ext), + ) - return jellyfin_url(self.client, "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (item_id, art, index, max_width, ext)) + return jellyfin_url( + self.client, + "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" + % (item_id, art, index, max_width, ext), + ) ################################################################################################# @@ -140,16 +147,16 @@ class API(object): return self.users("/Items/%s" % item_id) def get_items(self, item_ids): - return self.users("/Items", params={ - 'Ids': ','.join(str(x) for x in item_ids), - 'Fields': info() - }) + return self.users( + "/Items", + params={"Ids": ",".join(str(x) for x in item_ids), "Fields": info()}, + ) def get_sessions(self): - return self.sessions(params={'ControllableByUserId': "{UserId}"}) + return self.sessions(params={"ControllableByUserId": "{UserId}"}) def get_device(self, device_id): - return self.sessions(params={'DeviceId': device_id}) + return self.sessions(params={"DeviceId": device_id}) def post_session(self, session_id, url, params=None, data=None): return self.sessions("/%s/%s" % (session_id, url), "POST", params, data) @@ -158,64 +165,68 @@ class API(object): return self.items("/%s/Images" % item_id) def get_suggestion(self, media="Movie,Episode", limit=1): - return self.users("/Suggestions", params={ - 'Type': media, - 'Limit': limit - }) + return self.users("/Suggestions", params={"Type": media, "Limit": limit}) def get_recently_added(self, media=None, parent_id=None, limit=20): - return self.user_items("/Latest", { - 'Limit': limit, - 'UserId': "{UserId}", - 'IncludeItemTypes': media, - 'ParentId': parent_id, - 'Fields': info() - }) + return self.user_items( + "/Latest", + { + "Limit": limit, + "UserId": "{UserId}", + "IncludeItemTypes": media, + "ParentId": parent_id, + "Fields": info(), + }, + ) def get_next(self, index=None, limit=1): - return self.shows("/NextUp", { - 'Limit': limit, - 'UserId': "{UserId}", - 'StartIndex': None if index is None else int(index) - }) + return self.shows( + "/NextUp", + { + "Limit": limit, + "UserId": "{UserId}", + "StartIndex": None if index is None else int(index), + }, + ) def get_adjacent_episodes(self, show_id, item_id): - return self.shows("/%s/Episodes" % show_id, { - 'UserId': "{UserId}", - 'AdjacentTo': item_id, - 'Fields': "Overview" - }) + return self.shows( + "/%s/Episodes" % show_id, + {"UserId": "{UserId}", "AdjacentTo": item_id, "Fields": "Overview"}, + ) def get_genres(self, parent_id=None): - return self._get("Genres", { - 'ParentId': parent_id, - 'UserId': "{UserId}", - 'Fields': info() - }) + return self._get( + "Genres", {"ParentId": parent_id, "UserId": "{UserId}", "Fields": info()} + ) def get_recommendation(self, parent_id=None, limit=20): - return self._get("Movies/Recommendations", { - 'ParentId': parent_id, - 'UserId': "{UserId}", - 'Fields': info(), - 'Limit': limit - }) + return self._get( + "Movies/Recommendations", + { + "ParentId": parent_id, + "UserId": "{UserId}", + "Fields": info(), + "Limit": limit, + }, + ) def get_items_by_letter(self, parent_id=None, media=None, letter=None): - return self.user_items(params={ - 'ParentId': parent_id, - 'NameStartsWith': letter, - 'Fields': info(), - 'Recursive': True, - 'IncludeItemTypes': media - }) + return self.user_items( + params={ + "ParentId": parent_id, + "NameStartsWith": letter, + "Fields": info(), + "Recursive": True, + "IncludeItemTypes": media, + } + ) def get_channels(self): - return self._get("LiveTv/Channels", { - 'UserId': "{UserId}", - 'EnableImages': True, - 'EnableUserData': True - }) + return self._get( + "LiveTv/Channels", + {"UserId": "{UserId}", "EnableImages": True, "EnableUserData": True}, + ) def get_intros(self, item_id): return self.user_items("/%s/Intros" % item_id) @@ -230,30 +241,26 @@ class API(object): return self.user_items("/%s/LocalTrailers" % item_id) def get_transcode_settings(self): - return self._get('System/Configuration/encoding') + return self._get("System/Configuration/encoding") def get_ancestors(self, item_id): - return self.items("/%s/Ancestors" % item_id, params={ - 'UserId': "{UserId}" - }) + return self.items("/%s/Ancestors" % item_id, params={"UserId": "{UserId}"}) def get_items_theme_video(self, parent_id): - return self.users("/Items", params={ - 'HasThemeVideo': True, - 'ParentId': parent_id - }) + return self.users( + "/Items", params={"HasThemeVideo": True, "ParentId": parent_id} + ) def get_themes(self, item_id): - return self.items("/%s/ThemeMedia" % item_id, params={ - 'UserId': "{UserId}", - 'InheritFromParent': True - }) + return self.items( + "/%s/ThemeMedia" % item_id, + params={"UserId": "{UserId}", "InheritFromParent": True}, + ) def get_items_theme_song(self, parent_id): - return self.users("/Items", params={ - 'HasThemeSong': True, - 'ParentId': parent_id - }) + return self.users( + "/Items", params={"HasThemeSong": True, "ParentId": parent_id} + ) def check_companion_enabled(self): """ @@ -262,8 +269,10 @@ class API(object): None = Unknown """ try: - plugin_settings = self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {} - return plugin_settings.get('IsEnabled') + plugin_settings = ( + self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {} + ) + return plugin_settings.get("IsEnabled") except requests.RequestException as e: LOG.warning("Error checking companion installed state: %s", e) @@ -277,42 +286,51 @@ class API(object): return None def get_seasons(self, show_id): - return self.shows("/%s/Seasons" % show_id, params={ - 'UserId': "{UserId}", - 'EnableImages': True, - 'Fields': info() - }) + return self.shows( + "/%s/Seasons" % show_id, + params={"UserId": "{UserId}", "EnableImages": True, "Fields": info()}, + ) def get_date_modified(self, date, parent_id, media=None): - return self.users("/Items", params={ - 'ParentId': parent_id, - 'Recursive': False, - 'IsMissing': False, - 'IsVirtualUnaired': False, - 'IncludeItemTypes': media or None, - 'MinDateLastSaved': date, - 'Fields': info() - }) + return self.users( + "/Items", + params={ + "ParentId": parent_id, + "Recursive": False, + "IsMissing": False, + "IsVirtualUnaired": False, + "IncludeItemTypes": media or None, + "MinDateLastSaved": date, + "Fields": info(), + }, + ) def get_userdata_date_modified(self, date, parent_id, media=None): - return self.users("/Items", params={ - 'ParentId': parent_id, - 'Recursive': True, - 'IsMissing': False, - 'IsVirtualUnaired': False, - 'IncludeItemTypes': media or None, - 'MinDateLastSavedForUser': date, - 'Fields': info() - }) + return self.users( + "/Items", + params={ + "ParentId": parent_id, + "Recursive": True, + "IsMissing": False, + "IsVirtualUnaired": False, + "IncludeItemTypes": media or None, + "MinDateLastSavedForUser": date, + "Fields": info(), + }, + ) def refresh_item(self, item_id): - return self.items("/%s/Refresh" % item_id, "POST", json={ - 'Recursive': True, - 'ImageRefreshMode': "FullRefresh", - 'MetadataRefreshMode': "FullRefresh", - 'ReplaceAllImages': False, - 'ReplaceAllMetadata': True - }) + return self.items( + "/%s/Refresh" % item_id, + "POST", + json={ + "Recursive": True, + "ImageRefreshMode": "FullRefresh", + "MetadataRefreshMode": "FullRefresh", + "ReplaceAllImages": False, + "ReplaceAllMetadata": True, + }, + ) def favorite(self, item_id, option=True): return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE") @@ -324,7 +342,9 @@ class API(object): return self.sessions("/Capabilities/Full", "POST", json=data) def session_add_user(self, session_id, user_id, option=True): - return self.sessions("/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE") + return self.sessions( + "/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE" + ) def session_playing(self, data): return self.sessions("/Playing", "POST", json=data) @@ -339,115 +359,132 @@ class API(object): return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE") def get_sync_queue(self, date, filters=None): - return self._get("Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems", params={ - 'LastUpdateDT': date, - 'filter': filters or 'None' - }) + return self._get( + "Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems", + params={"LastUpdateDT": date, "filter": filters or "None"}, + ) def get_server_time(self): return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime") def get_play_info(self, item_id, profile): - return self.items("/%s/PlaybackInfo" % item_id, "POST", json={ - 'UserId': "{UserId}", - 'DeviceProfile': profile, - 'AutoOpenLiveStream': True - }) + return self.items( + "/%s/PlaybackInfo" % item_id, + "POST", + json={ + "UserId": "{UserId}", + "DeviceProfile": profile, + "AutoOpenLiveStream": True, + }, + ) def get_live_stream(self, item_id, play_id, token, profile): - return self._post("LiveStreams/Open", json={ - 'UserId': "{UserId}", - 'DeviceProfile': profile, - 'OpenToken': token, - 'PlaySessionId': play_id, - 'ItemId': item_id - }) + return self._post( + "LiveStreams/Open", + json={ + "UserId": "{UserId}", + "DeviceProfile": profile, + "OpenToken": token, + "PlaySessionId": play_id, + "ItemId": item_id, + }, + ) def close_live_stream(self, live_id): - return self._post("LiveStreams/Close", json={ - 'LiveStreamId': live_id - }) + return self._post("LiveStreams/Close", json={"LiveStreamId": live_id}) def close_transcode(self, device_id, play_id): - return self._delete("Videos/ActiveEncodings", params={ - 'DeviceId': device_id, - 'PlaySessionId': play_id - }) + return self._delete( + "Videos/ActiveEncodings", + params={"DeviceId": device_id, "PlaySessionId": play_id}, + ) def get_default_headers(self): auth = "MediaBrowser " - auth += "Client=%s, " % self.config.data['app.name'] - auth += "Device=%s, " % self.config.data['app.device_name'] - auth += "DeviceId=%s, " % self.config.data['app.device_id'] - auth += "Version=%s" % self.config.data['app.version'] + auth += "Client=%s, " % self.config.data["app.name"] + auth += "Device=%s, " % self.config.data["app.device_name"] + auth += "DeviceId=%s, " % self.config.data["app.device_id"] + auth += "Version=%s" % self.config.data["app.version"] return { "Accept": "application/json", "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", - "X-Application": "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']), + "X-Application": "%s/%s" + % (self.config.data["app.name"], self.config.data["app.version"]), "Accept-Charset": "UTF-8,*", "Accept-encoding": "gzip", - "User-Agent": self.config.data['http.user_agent'] or "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']), - "x-emby-authorization": ensure_str(auth, 'utf-8') + "User-Agent": self.config.data["http.user_agent"] + or "%s/%s" + % (self.config.data["app.name"], self.config.data["app.version"]), + "x-emby-authorization": ensure_str(auth, "utf-8"), } - def send_request(self, url, path, method="get", timeout=None, headers=None, data=None): + def send_request( + self, url, path, method="get", timeout=None, headers=None, data=None + ): request_method = getattr(requests, method.lower()) url = "%s/%s" % (url, path) request_settings = { "timeout": timeout or self.default_timeout, "headers": headers or self.get_default_headers(), - "data": data + "data": data, } - request_settings["verify"] = settings('sslverify.bool') + request_settings["verify"] = settings("sslverify.bool") LOG.info("Sending %s request to %s" % (method, path)) - LOG.debug(request_settings['timeout']) - LOG.debug(request_settings['headers']) + LOG.debug(request_settings["timeout"]) + LOG.debug(request_settings["headers"]) return request_method(url, **request_settings) def login(self, server_url, username, password=""): path = "Users/AuthenticateByName" - auth_data = { - "username": username, - "Pw": password - } + auth_data = {"username": username, "Pw": password} headers = self.get_default_headers() - headers.update({'Content-type': "application/json"}) + headers.update({"Content-type": "application/json"}) try: LOG.info("Trying to login to %s/%s as %s" % (server_url, path, username)) - response = self.send_request(server_url, path, method="post", timeout=10, headers=headers, data=json.dumps(auth_data)) + response = self.send_request( + server_url, + path, + method="post", + timeout=10, + headers=headers, + data=json.dumps(auth_data), + ) if response.status_code == 200: return response.json() else: - LOG.error("Failed to login to server with status code: " + str(response.status_code)) + LOG.error( + "Failed to login to server with status code: " + + str(response.status_code) + ) LOG.error("Server Response:\n" + str(response.content)) LOG.debug(headers) return {} - except Exception as e: # Find exceptions for likely cases i.e, server timeout, etc + except ( + Exception + ) as e: # Find exceptions for likely cases i.e, server timeout, etc LOG.error(e) return {} def validate_authentication_token(self, server): - auth_token_header = { - 'X-MediaBrowser-Token': server['AccessToken'] - } + auth_token_header = {"X-MediaBrowser-Token": server["AccessToken"]} headers = self.get_default_headers() headers.update(auth_token_header) - response = self.send_request(server['address'], "system/info", headers=headers) + response = self.send_request(server["address"], "system/info", headers=headers) if response.status_code == 200: return response.json() else: - return {'Status_Code': response.status_code} + return {"Status_Code": response.status_code} def get_public_info(self, server_address): response = self.send_request(server_address, "system/info/public") @@ -459,8 +496,8 @@ class API(object): return {} def check_redirect(self, server_address): - ''' Checks if the server is redirecting traffic to a new URL and + """Checks if the server is redirecting traffic to a new URL and returns the URL the server prefers to use - ''' + """ response = self.send_request(server_address, "system/info/public") - return response.url.replace('/system/info/public', '') + return response.url.replace("/system/info/public", "") diff --git a/jellyfin_kodi/jellyfin/client.py b/jellyfin_kodi/jellyfin/client.py index 5026a73e..d7802df3 100644 --- a/jellyfin_kodi/jellyfin/client.py +++ b/jellyfin_kodi/jellyfin/client.py @@ -19,11 +19,10 @@ LOG = LazyLogger(__name__) def callback(message, data): - - ''' Callback function should receive message, data - message: string - data: json dictionary - ''' + """Callback function should receive message, data + message: string + data: json dictionary + """ pass @@ -53,13 +52,13 @@ class JellyfinClient(object): self.set_credentials(credentials or {}) state = self.auth.connect(options or {}) - if state['State'] == CONNECTION_STATE['SignedIn']: + if state["State"] == CONNECTION_STATE["SignedIn"]: LOG.info("User is authenticated.") self.logged_in = True - self.callback("ServerOnline", {'Id': self.auth.server_id}) + self.callback("ServerOnline", {"Id": self.auth.server_id}) - state['Credentials'] = self.get_credentials() + state["Credentials"] = self.get_credentials() return state diff --git a/jellyfin_kodi/jellyfin/configuration.py b/jellyfin_kodi/jellyfin/configuration.py index c8407dbf..09a2b264 100644 --- a/jellyfin_kodi/jellyfin/configuration.py +++ b/jellyfin_kodi/jellyfin/configuration.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals -''' This will hold all configs from the client. +""" This will hold all configs from the client. Configuration set here will be used for the HTTP client. -''' +""" ################################################################################################# @@ -26,28 +26,41 @@ class Config(object): self.data = {} self.http() - def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): + def app( + self, + name, + version, + device_name, + device_id, + capabilities=None, + device_pixel_ratio=None, + ): LOG.debug("Begin app constructor.") - self.data['app.name'] = name - self.data['app.version'] = version - self.data['app.device_name'] = device_name - self.data['app.device_id'] = device_id - self.data['app.capabilities'] = capabilities - self.data['app.device_pixel_ratio'] = device_pixel_ratio - self.data['app.default'] = False + self.data["app.name"] = name + self.data["app.version"] = version + self.data["app.device_name"] = device_name + self.data["app.device_id"] = device_id + self.data["app.capabilities"] = capabilities + self.data["app.device_pixel_ratio"] = device_pixel_ratio + self.data["app.default"] = False def auth(self, server, user_id, token=None, ssl=None): LOG.debug("Begin auth constructor.") - self.data['auth.server'] = server - self.data['auth.user_id'] = user_id - self.data['auth.token'] = token - self.data['auth.ssl'] = ssl + self.data["auth.server"] = server + self.data["auth.user_id"] = user_id + self.data["auth.token"] = token + self.data["auth.ssl"] = ssl - def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT): + def http( + self, + user_agent=None, + max_retries=DEFAULT_HTTP_MAX_RETRIES, + timeout=DEFAULT_HTTP_TIMEOUT, + ): LOG.debug("Begin http constructor.") - self.data['http.max_retries'] = max_retries - self.data['http.timeout'] = timeout - self.data['http.user_agent'] = user_agent + self.data["http.max_retries"] = max_retries + self.data["http.timeout"] = timeout + self.data["http.user_agent"] = user_agent diff --git a/jellyfin_kodi/jellyfin/connection_manager.py b/jellyfin_kodi/jellyfin/connection_manager.py index 9e562a63..304f7148 100644 --- a/jellyfin_kodi/jellyfin/connection_manager.py +++ b/jellyfin_kodi/jellyfin/connection_manager.py @@ -20,10 +20,10 @@ from .api import API LOG = LazyLogger(__name__) CONNECTION_STATE = { - 'Unavailable': 0, - 'ServerSelection': 1, - 'ServerSignIn': 2, - 'SignedIn': 3 + "Unavailable": 0, + "ServerSelection": 1, + "ServerSignIn": 2, + "SignedIn": 3, } ################################################################################################# @@ -48,10 +48,10 @@ class ConnectionManager(object): LOG.info("revoking token") - self['server']['AccessToken'] = None + self["server"]["AccessToken"] = None self.credentials.set_credentials(self.credentials.get()) - self.config.data['auth.token'] = None + self.config.data["auth.token"] = None def get_available_servers(self): @@ -61,11 +61,13 @@ class ConnectionManager(object): credentials = self.credentials.get() found_servers = self.process_found_servers(self._server_discovery()) - if not found_servers and not credentials['Servers']: # back out right away, no point in continuing + if ( + not found_servers and not credentials["Servers"] + ): # back out right away, no point in continuing LOG.info("Found no servers") return list() - servers = list(credentials['Servers']) + servers = list(credentials["Servers"]) # Merges servers we already knew with newly found ones for found_server in found_servers: @@ -74,8 +76,8 @@ class ConnectionManager(object): except KeyError: continue - servers.sort(key=itemgetter('DateLastAccessed'), reverse=True) - credentials['Servers'] = servers + servers.sort(key=itemgetter("DateLastAccessed"), reverse=True) + credentials["Servers"] = servers self.credentials.set(credentials) return servers @@ -88,36 +90,35 @@ class ConnectionManager(object): if not server_url: raise AttributeError("server url cannot be empty") - data = self.API.login(server_url, username, password) # returns empty dict on failure + data = self.API.login( + server_url, username, password + ) # returns empty dict on failure if not data: - LOG.info("Failed to login as `"+username+"`") + LOG.info("Failed to login as `" + username + "`") return {} LOG.info("Successfully logged in as %s" % (username)) # TODO Change when moving to database storage of server details credentials = self.credentials.get() - self.config.data['auth.user_id'] = data['User']['Id'] - self.config.data['auth.token'] = data['AccessToken'] + self.config.data["auth.user_id"] = data["User"]["Id"] + self.config.data["auth.token"] = data["AccessToken"] - for server in credentials['Servers']: - if server['Id'] == data['ServerId']: + for server in credentials["Servers"]: + if server["Id"] == data["ServerId"]: found_server = server break else: return {} # No server found - found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - found_server['UserId'] = data['User']['Id'] - found_server['AccessToken'] = data['AccessToken'] + found_server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + found_server["UserId"] = data["User"]["Id"] + found_server["AccessToken"] = data["AccessToken"] - self.credentials.add_update_server(credentials['Servers'], found_server) + self.credentials.add_update_server(credentials["Servers"], found_server) - info = { - 'Id': data['User']['Id'], - 'IsSignedInOffline': True - } + info = {"Id": data["User"]["Id"], "IsSignedInOffline": True} self.credentials.add_update_user(server, info) self.credentials.set_credentials(credentials) @@ -137,40 +138,44 @@ class ConnectionManager(object): address = response_url LOG.info("connectToAddress %s succeeded", address) server = { - 'address': address, + "address": address, } server = self.connect_to_server(server, options) if server is False: LOG.error("connectToAddress %s failed", address) - return {'State': CONNECTION_STATE['Unavailable']} + return {"State": CONNECTION_STATE["Unavailable"]} return server except Exception as error: LOG.exception(error) LOG.error("connectToAddress %s failed", address) - return {'State': CONNECTION_STATE['Unavailable']} + return {"State": CONNECTION_STATE["Unavailable"]} def connect_to_server(self, server, options={}): LOG.info("begin connectToServer") try: - result = self.API.get_public_info(server.get('address')) + result = self.API.get_public_info(server.get("address")) if not result: - LOG.error("Failed to connect to server: %s" % server.get('address')) - return {'State': CONNECTION_STATE['Unavailable']} + LOG.error("Failed to connect to server: %s" % server.get("address")) + return {"State": CONNECTION_STATE["Unavailable"]} - LOG.info("calling onSuccessfulConnection with server %s", server.get('Name')) + LOG.info( + "calling onSuccessfulConnection with server %s", server.get("Name") + ) self._update_server_info(server, result) credentials = self.credentials.get() - return self._after_connect_validated(server, credentials, result, True, options) + return self._after_connect_validated( + server, credentials, result, True, options + ) except Exception as e: LOG.error(traceback.format_exc()) LOG.error("Failing server connection. ERROR msg: {}".format(e)) - return {'State': CONNECTION_STATE['Unavailable']} + return {"State": CONNECTION_STATE["Unavailable"]} def connect(self, options={}): @@ -180,9 +185,7 @@ class ConnectionManager(object): LOG.info("connect has %s servers", len(servers)) if not (len(servers)): # No servers provided - return { - 'State': ['ServerSelection'] - } + return {"State": ["ServerSelection"]} result = self.connect_to_server(servers[0], options) LOG.debug("resolving connect with result: %s", result) @@ -190,7 +193,7 @@ class ConnectionManager(object): return result def jellyfin_token(self): # Called once monitor.py#163 - return self.get_server_info(self.server_id)['AccessToken'] + return self.get_server_info(self.server_id)["AccessToken"] def get_server_info(self, server_id): @@ -198,14 +201,14 @@ class ConnectionManager(object): LOG.info("server_id is empty") return {} - servers = self.credentials.get()['Servers'] + servers = self.credentials.get()["Servers"] for server in servers: - if server['Id'] == server_id: + if server["Id"] == server_id: return server def get_server_address(self, server_id): - return self.get_server_info(server_id or self.server_id).get('address') + return self.get_server_info(server_id or self.server_id).get("address") def get_public_users(self): return self.client.jellyfin.get_public_users() @@ -258,9 +261,9 @@ class ConnectionManager(object): server = self._convert_endpoint_address_to_manual_address(found_server) info = { - 'Id': found_server['Id'], - 'address': server or found_server['Address'], - 'Name': found_server['Name'] + "Id": found_server["Id"], + "address": server or found_server["Address"], + "Name": found_server["Name"], } servers.append(info) @@ -270,11 +273,11 @@ class ConnectionManager(object): # TODO: Make IPv6 compatible def _convert_endpoint_address_to_manual_address(self, info): - if info.get('Address') and info.get('EndpointAddress'): - address = info['EndpointAddress'].split(':')[0] + if info.get("Address") and info.get("EndpointAddress"): + address = info["EndpointAddress"].split(":")[0] # Determine the port, if any - parts = info['Address'].split(':') + parts = info["Address"].split(":") if len(parts) > 1: port_string = parts[len(parts) - 1] @@ -288,64 +291,70 @@ class ConnectionManager(object): def _normalize_address(self, address): # TODO: Try HTTPS first, then HTTP if that fails. - if '://' not in address: - address = 'http://' + address + if "://" not in address: + address = "http://" + address # Attempt to correct bad input url = urllib3.util.parse_url(address.strip()) if url.scheme is None: - url = url._replace(scheme='http') + url = url._replace(scheme="http") - if url.scheme == 'http' and url.port == 80: + if url.scheme == "http" and url.port == 80: url = url._replace(port=None) - if url.scheme == 'https' and url.port == 443: + if url.scheme == "https" and url.port == 443: url = url._replace(port=None) return url.url - def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options): - if options.get('enableAutoLogin') is False: + def _after_connect_validated( + self, server, credentials, system_info, verify_authentication, options + ): + if options.get("enableAutoLogin") is False: - self.config.data['auth.user_id'] = server.pop('UserId', None) - self.config.data['auth.token'] = server.pop('AccessToken', None) + self.config.data["auth.user_id"] = server.pop("UserId", None) + self.config.data["auth.token"] = server.pop("AccessToken", None) - elif verify_authentication and server.get('AccessToken'): + elif verify_authentication and server.get("AccessToken"): system_info = self.API.validate_authentication_token(server) - if 'Status_Code' not in system_info: + if "Status_Code" not in system_info: self._update_server_info(server, system_info) - self.config.data['auth.user_id'] = server['UserId'] - self.config.data['auth.token'] = server['AccessToken'] - system_info['Status_Code'] = 200 + self.config.data["auth.user_id"] = server["UserId"] + self.config.data["auth.token"] = server["AccessToken"] + system_info["Status_Code"] = 200 - return self._after_connect_validated(server, credentials, system_info, False, options) + return self._after_connect_validated( + server, credentials, system_info, False, options + ) - server['UserId'] = None - server['AccessToken'] = None - system_info['State'] = CONNECTION_STATE['Unavailable'] + server["UserId"] = None + server["AccessToken"] = None + system_info["State"] = CONNECTION_STATE["Unavailable"] return system_info self._update_server_info(server, system_info) - server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - self.credentials.add_update_server(credentials['Servers'], server) + server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + self.credentials.add_update_server(credentials["Servers"], server) self.credentials.set(credentials) - self.server_id = server['Id'] + self.server_id = server["Id"] # Update configs - self.config.data['auth.server'] = server['address'] - self.config.data['auth.server-name'] = server['Name'] - self.config.data['auth.server=id'] = server['Id'] - self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl']) + self.config.data["auth.server"] = server["address"] + self.config.data["auth.server-name"] = server["Name"] + self.config.data["auth.server=id"] = server["Id"] + self.config.data["auth.ssl"] = options.get("ssl", self.config.data["auth.ssl"]) # Connected return { - 'Servers': [server], - 'State': CONNECTION_STATE['SignedIn'] - if server.get('AccessToken') - else CONNECTION_STATE['ServerSignIn'], + "Servers": [server], + "State": ( + CONNECTION_STATE["SignedIn"] + if server.get("AccessToken") + else CONNECTION_STATE["ServerSignIn"] + ), } def _update_server_info(self, server, system_info): @@ -353,8 +362,8 @@ class ConnectionManager(object): if server is None or system_info is None: return - server['Name'] = system_info['ServerName'] - server['Id'] = system_info['Id'] + server["Name"] = system_info["ServerName"] + server["Id"] = system_info["Id"] - if system_info.get('address'): - server['address'] = system_info['address'] + if system_info.get("address"): + server["address"] = system_info["address"] diff --git a/jellyfin_kodi/jellyfin/credentials.py b/jellyfin_kodi/jellyfin/credentials.py index 638f4af3..1ef3d164 100644 --- a/jellyfin_kodi/jellyfin/credentials.py +++ b/jellyfin_kodi/jellyfin/credentials.py @@ -41,7 +41,7 @@ class Credentials(object): self.credentials = {} LOG.debug("credentials initialized with: %s", self.credentials) - self.credentials['Servers'] = self.credentials.setdefault('Servers', []) + self.credentials["Servers"] = self.credentials.setdefault("Servers", []) def get(self): self._ensure() @@ -62,53 +62,55 @@ class Credentials(object): def add_update_user(self, server, user): - for existing in server.setdefault('Users', []): - if existing['Id'] == user['Id']: + for existing in server.setdefault("Users", []): + if existing["Id"] == user["Id"]: # Merge the data - existing['IsSignedInOffline'] = True + existing["IsSignedInOffline"] = True break else: - server['Users'].append(user) + server["Users"].append(user) def add_update_server(self, servers, server): - if server.get('Id') is None: + if server.get("Id") is None: raise KeyError("Server['Id'] cannot be null or empty") # Add default DateLastAccessed if doesn't exist. - server.setdefault('DateLastAccessed', "1970-01-01T00:00:00Z") + server.setdefault("DateLastAccessed", "1970-01-01T00:00:00Z") for existing in servers: - if existing['Id'] == server['Id']: + if existing["Id"] == server["Id"]: # Merge the data - if server.get('DateLastAccessed') and self._date_object(server['DateLastAccessed']) > self._date_object(existing['DateLastAccessed']): - existing['DateLastAccessed'] = server['DateLastAccessed'] + if server.get("DateLastAccessed") and self._date_object( + server["DateLastAccessed"] + ) > self._date_object(existing["DateLastAccessed"]): + existing["DateLastAccessed"] = server["DateLastAccessed"] - if server.get('UserLinkType'): - existing['UserLinkType'] = server['UserLinkType'] + if server.get("UserLinkType"): + existing["UserLinkType"] = server["UserLinkType"] - if server.get('AccessToken'): - existing['AccessToken'] = server['AccessToken'] - existing['UserId'] = server['UserId'] + if server.get("AccessToken"): + existing["AccessToken"] = server["AccessToken"] + existing["UserId"] = server["UserId"] - if server.get('ExchangeToken'): - existing['ExchangeToken'] = server['ExchangeToken'] + if server.get("ExchangeToken"): + existing["ExchangeToken"] = server["ExchangeToken"] - if server.get('ManualAddress'): - existing['ManualAddress'] = server['ManualAddress'] + if server.get("ManualAddress"): + existing["ManualAddress"] = server["ManualAddress"] - if server.get('LocalAddress'): - existing['LocalAddress'] = server['LocalAddress'] + if server.get("LocalAddress"): + existing["LocalAddress"] = server["LocalAddress"] - if server.get('Name'): - existing['Name'] = server['Name'] + if server.get("Name"): + existing["Name"] = server["Name"] - if server.get('LastConnectionMode') is not None: - existing['LastConnectionMode'] = server['LastConnectionMode'] + if server.get("LastConnectionMode") is not None: + existing["LastConnectionMode"] = server["LastConnectionMode"] - if server.get('ConnectServerId'): - existing['ConnectServerId'] = server['ConnectServerId'] + if server.get("ConnectServerId"): + existing["ConnectServerId"] = server["ConnectServerId"] return existing diff --git a/jellyfin_kodi/jellyfin/http.py b/jellyfin_kodi/jellyfin/http.py index 646dc071..0c328213 100644 --- a/jellyfin_kodi/jellyfin/http.py +++ b/jellyfin_kodi/jellyfin/http.py @@ -34,9 +34,13 @@ class HTTP(object): self.session = requests.Session() - max_retries = self.config.data['http.max_retries'] - self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries)) - self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries)) + max_retries = self.config.data["http.max_retries"] + self.session.mount( + "http://", requests.adapters.HTTPAdapter(max_retries=max_retries) + ) + self.session.mount( + "https://", requests.adapters.HTTPAdapter(max_retries=max_retries) + ) def stop_session(self): @@ -51,43 +55,44 @@ class HTTP(object): def _replace_user_info(self, string): - if '{server}' in string: - if self.config.data.get('auth.server', None): - string = string.replace("{server}", self.config.data['auth.server']) + if "{server}" in string: + if self.config.data.get("auth.server", None): + string = string.replace("{server}", self.config.data["auth.server"]) else: LOG.debug("Server address not set") - if '{UserId}' in string: - if self.config.data.get('auth.user_id', None): - string = string.replace("{UserId}", self.config.data['auth.user_id']) + if "{UserId}" in string: + if self.config.data.get("auth.user_id", None): + string = string.replace("{UserId}", self.config.data["auth.user_id"]) else: LOG.debug("UserId is not set.") return string def request(self, data, session=None): - - ''' Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back - data dictionary can contain: - type: GET, POST, etc. - url: (optional) - handler: not considered when url is provided (optional) - params: request parameters (optional) - json: request body (optional) - headers: (optional), - verify: ssl certificate, True (verify using device built-in library) or False - ''' + """Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back + data dictionary can contain: + type: GET, POST, etc. + url: (optional) + handler: not considered when url is provided (optional) + params: request parameters (optional) + json: request body (optional) + headers: (optional), + verify: ssl certificate, True (verify using device built-in library) or False + """ if not data: raise AttributeError("Request cannot be empty") data = self._request(data) LOG.debug("--->[ http ] %s", JsonDebugPrinter(data)) - retry = data.pop('retry', 5) + retry = data.pop("retry", 5) while True: try: - r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data) + r = self._requests( + session or self.session or requests, data.pop("type", "GET"), **data + ) r.content # release the connection if not self.keep_alive and self.session is not None: @@ -104,7 +109,10 @@ class HTTP(object): continue LOG.error(error) - self.client.callback("ServerUnreachable", {'ServerId': self.config.data['auth.server-id']}) + self.client.callback( + "ServerUnreachable", + {"ServerId": self.config.data["auth.server-id"]}, + ) raise HTTPException("ServerUnreachable", error) @@ -125,12 +133,18 @@ class HTTP(object): if r.status_code == 401: - if 'X-Application-Error-Code' in r.headers: - self.client.callback("AccessRestricted", {'ServerId': self.config.data['auth.server-id']}) + if "X-Application-Error-Code" in r.headers: + self.client.callback( + "AccessRestricted", + {"ServerId": self.config.data["auth.server-id"]}, + ) raise HTTPException("AccessRestricted", error) else: - self.client.callback("Unauthorized", {'ServerId': self.config.data['auth.server-id']}) + self.client.callback( + "Unauthorized", + {"ServerId": self.config.data["auth.server-id"]}, + ) self.client.auth.revoke_token() raise HTTPException("Unauthorized", error) @@ -160,11 +174,13 @@ class HTTP(object): except requests.exceptions.MissingSchema as error: LOG.error("Request missing Schema. " + str(error)) - raise HTTPException("MissingSchema", {'Id': self.config.data.get('auth.server', "None")}) + raise HTTPException( + "MissingSchema", {"Id": self.config.data.get("auth.server", "None")} + ) else: try: - self.config.data['server-time'] = r.headers['Date'] + self.config.data["server-time"] = r.headers["Date"] elapsed = int(r.elapsed.total_seconds() * 1000) response = r.json() LOG.debug("---<[ http ][%s ms]", elapsed) @@ -179,15 +195,18 @@ class HTTP(object): def _request(self, data): - if 'url' not in data: - data['url'] = "%s/%s" % (self.config.data.get("auth.server", ""), data.pop('handler', "")) + if "url" not in data: + data["url"] = "%s/%s" % ( + self.config.data.get("auth.server", ""), + data.pop("handler", ""), + ) self._get_header(data) - data['timeout'] = data.get('timeout') or self.config.data['http.timeout'] - data['verify'] = data.get('verify') or self.config.data.get('auth.ssl', False) - data['url'] = self._replace_user_info(data['url']) - self._process_params(data.get('params') or {}) - self._process_params(data.get('json') or {}) + data["timeout"] = data.get("timeout") or self.config.data["http.timeout"] + data["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False) + data["url"] = self._replace_user_info(data["url"]) + self._process_params(data.get("params") or {}) + self._process_params(data.get("json") or {}) return data @@ -204,17 +223,24 @@ class HTTP(object): def _get_header(self, data): - data['headers'] = data.setdefault('headers', {}) + data["headers"] = data.setdefault("headers", {}) - if not data['headers']: - data['headers'].update({ - 'Content-type': "application/json", - 'Accept-Charset': "UTF-8,*", - 'Accept-encoding': "gzip", - 'User-Agent': self.config.data['http.user_agent'] or "%s/%s" % (self.config.data.get('app.name', 'Jellyfin for Kodi'), self.config.data.get('app.version', "0.0.0")) - }) + if not data["headers"]: + data["headers"].update( + { + "Content-type": "application/json", + "Accept-Charset": "UTF-8,*", + "Accept-encoding": "gzip", + "User-Agent": self.config.data["http.user_agent"] + or "%s/%s" + % ( + self.config.data.get("app.name", "Jellyfin for Kodi"), + self.config.data.get("app.version", "0.0.0"), + ), + } + ) - if 'x-emby-authorization' not in data['headers']: + if "x-emby-authorization" not in data["headers"]: self._authorization(data) return data @@ -222,19 +248,26 @@ class HTTP(object): def _authorization(self, data): auth = "MediaBrowser " - auth += "Client=%s, " % self.config.data.get('app.name', "Jellyfin for Kodi") - auth += "Device=%s, " % self.config.data.get('app.device_name', 'Unknown Device') - auth += "DeviceId=%s, " % self.config.data.get('app.device_id', 'Unknown Device id') - auth += "Version=%s" % self.config.data.get('app.version', '0.0.0') + auth += "Client=%s, " % self.config.data.get("app.name", "Jellyfin for Kodi") + auth += "Device=%s, " % self.config.data.get( + "app.device_name", "Unknown Device" + ) + auth += "DeviceId=%s, " % self.config.data.get( + "app.device_id", "Unknown Device id" + ) + auth += "Version=%s" % self.config.data.get("app.version", "0.0.0") - data['headers'].update({'x-emby-authorization': ensure_str(auth, 'utf-8')}) + data["headers"].update({"x-emby-authorization": ensure_str(auth, "utf-8")}) - if self.config.data.get('auth.token') and self.config.data.get('auth.user_id'): + if self.config.data.get("auth.token") and self.config.data.get("auth.user_id"): - auth += ', UserId=%s' % self.config.data.get('auth.user_id') - data['headers'].update({ - 'x-emby-authorization': ensure_str(auth, 'utf-8'), - 'X-MediaBrowser-Token': self.config.data.get('auth.token')}) + auth += ", UserId=%s" % self.config.data.get("auth.user_id") + data["headers"].update( + { + "x-emby-authorization": ensure_str(auth, "utf-8"), + "X-MediaBrowser-Token": self.config.data.get("auth.token"), + } + ) return data diff --git a/jellyfin_kodi/jellyfin/ws_client.py b/jellyfin_kodi/jellyfin/ws_client.py index 65da191e..38a17f8b 100644 --- a/jellyfin_kodi/jellyfin/ws_client.py +++ b/jellyfin_kodi/jellyfin/ws_client.py @@ -14,7 +14,8 @@ from ..helper import LazyLogger, settings # If numpy is installed, the websockets library tries to use it, and then # kodi hard crashes for reasons I don't even want to pretend to understand import sys # noqa: E402,I100 -sys.modules['numpy'] = None + +sys.modules["numpy"] = None import websocket # noqa: E402,I201 ################################################################################################## @@ -41,23 +42,29 @@ class WSClient(threading.Thread): if self.wsc is None: raise ValueError("The websocket client is not started.") - self.wsc.send(json.dumps({'MessageType': message, "Data": data})) + self.wsc.send(json.dumps({"MessageType": message, "Data": data})) def run(self): monitor = xbmc.Monitor() - token = self.client.config.data['auth.token'] - device_id = self.client.config.data['app.device_id'] - server = self.client.config.data['auth.server'] - server = server.replace('https://', 'wss://') if server.startswith('https') else server.replace('http://', 'ws://') + token = self.client.config.data["auth.token"] + device_id = self.client.config.data["app.device_id"] + server = self.client.config.data["auth.server"] + server = ( + server.replace("https://", "wss://") + if server.startswith("https") + else server.replace("http://", "ws://") + ) wsc_url = "%s/socket?api_key=%s&device_id=%s" % (server, token, device_id) LOG.info("Websocket url: %s", wsc_url) - self.wsc = websocket.WebSocketApp(wsc_url, - on_open=lambda ws: self.on_open(ws), - on_message=lambda ws, message: self.on_message(ws, message), - on_error=lambda ws, error: self.on_error(ws, error)) + self.wsc = websocket.WebSocketApp( + wsc_url, + on_open=lambda ws: self.on_open(ws), + on_message=lambda ws, message: self.on_message(ws, message), + on_error=lambda ws, error: self.on_error(ws, error), + ) while not self.stop: @@ -73,41 +80,42 @@ class WSClient(threading.Thread): LOG.info("--->[ websocket opened ]") # Avoid a timing issue where the capabilities are not correctly registered time.sleep(5) - if settings('remoteControl.bool'): - self.client.jellyfin.post_capabilities({ - 'PlayableMediaTypes': "Audio,Video", - 'SupportsMediaControl': True, - 'SupportedCommands': ( - "MoveUp,MoveDown,MoveLeft,MoveRight,Select," - "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," - "GoHome,PageUp,NextLetter,GoToSearch," - "GoToSettings,PageDown,PreviousLetter,TakeScreenshot," - "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage," - "SetAudioStreamIndex,SetSubtitleStreamIndex," - "SetRepeatMode,Mute,Unmute,SetVolume," - "Play,Playstate,PlayNext,PlayMediaSource" - ), - }) + if settings("remoteControl.bool"): + self.client.jellyfin.post_capabilities( + { + "PlayableMediaTypes": "Audio,Video", + "SupportsMediaControl": True, + "SupportedCommands": ( + "MoveUp,MoveDown,MoveLeft,MoveRight,Select," + "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," + "GoHome,PageUp,NextLetter,GoToSearch," + "GoToSettings,PageDown,PreviousLetter,TakeScreenshot," + "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage," + "SetAudioStreamIndex,SetSubtitleStreamIndex," + "SetRepeatMode,Mute,Unmute,SetVolume," + "Play,Playstate,PlayNext,PlayMediaSource" + ), + } + ) else: - self.client.jellyfin.post_capabilities({ - "PlayableMediaTypes": "Audio, Video", - "SupportsMediaControl": False - }) + self.client.jellyfin.post_capabilities( + {"PlayableMediaTypes": "Audio, Video", "SupportsMediaControl": False} + ) def on_message(self, ws, message): message = json.loads(message) - data = message.get('Data', {}) + data = message.get("Data", {}) - if message['MessageType'] in ('RefreshProgress',): + if message["MessageType"] in ("RefreshProgress",): LOG.debug("Ignoring %s", message) return - if not self.client.config.data['app.default']: - data['ServerId'] = self.client.auth.server_id + if not self.client.config.data["app.default"]: + data["ServerId"] = self.client.auth.server_id - self.client.callback(message['MessageType'], data) + self.client.callback(message["MessageType"], data) def stop_client(self): diff --git a/jellyfin_kodi/library.py b/jellyfin_kodi/library.py index c3127664..a200f37d 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -24,8 +24,8 @@ from .jellyfin import Jellyfin ################################################################################################## LOG = LazyLogger(__name__) -LIMIT = int(settings('limitIndex') or 15) -DTHREADS = int(settings('limitThreads') or 3) +LIMIT = int(settings("limitIndex") or 15) +DTHREADS = int(settings("limitThreads") or 3) TARGET_DB_VERSION = 1 ################################################################################################## @@ -43,8 +43,8 @@ class Library(threading.Thread): def __init__(self, monitor): - self.direct_path = settings('useDirectPaths') == "1" - self.progress_display = int(settings('syncProgress') or 50) + self.direct_path = settings("useDirectPaths") == "1" + self.progress_display = int(settings("syncProgress") or 50) self.monitor = monitor self.player = monitor.monitor.player self.server = Jellyfin().get_client() @@ -59,7 +59,7 @@ class Library(threading.Thread): self.jellyfin_threads = [] self.download_threads = [] self.notify_threads = [] - self.writer_threads = {'updated': [], 'userdata': [], 'removed': []} + self.writer_threads = {"updated": [], "userdata": [], "removed": []} self.database_lock = threading.Lock() self.music_database_lock = threading.Lock() @@ -67,16 +67,16 @@ class Library(threading.Thread): def __new_queues__(self): return { - 'Movie': Queue.Queue(), - 'BoxSet': Queue.Queue(), - 'MusicVideo': Queue.Queue(), - 'Series': Queue.Queue(), - 'Season': Queue.Queue(), - 'Episode': Queue.Queue(), - 'MusicAlbum': Queue.Queue(), - 'MusicArtist': Queue.Queue(), - 'AlbumArtist': Queue.Queue(), - 'Audio': Queue.Queue() + "Movie": Queue.Queue(), + "BoxSet": Queue.Queue(), + "MusicVideo": Queue.Queue(), + "Series": Queue.Queue(), + "Season": Queue.Queue(), + "Episode": Queue.Queue(), + "MusicAlbum": Queue.Queue(), + "MusicArtist": Queue.Queue(), + "AlbumArtist": Queue.Queue(), + "Audio": Queue.Queue(), } def run(self): @@ -86,7 +86,7 @@ class Library(threading.Thread): if not self.startup(): self.stop_client() - window('jellyfin_startup.bool', True) + window("jellyfin_startup.bool", True) while not self.stop_thread: @@ -105,17 +105,15 @@ class Library(threading.Thread): LOG.info("---<[ library ]") def test_databases(self): - - ''' Open the databases to test if the file exists. - ''' - with Database('video'), Database('music'): + """Open the databases to test if the file exists.""" + with Database("video"), Database("music"): pass def check_version(self): - ''' + """ Checks database version and triggers any required data migrations - ''' - with Database('jellyfin') as jellyfindb: + """ + with Database("jellyfin") as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) db_version = db.get_version() @@ -124,26 +122,37 @@ class Library(threading.Thread): db.add_version((TARGET_DB_VERSION)) # Video Database Migrations - with Database('video') as videodb: + with Database("video") as videodb: vid_db = KodiDb(videodb.cursor) if vid_db.migrations(): - LOG.info('changes detected, reloading skin') - xbmc.executebuiltin('UpdateLibrary(video)') - xbmc.executebuiltin('ReloadSkin()') + LOG.info("changes detected, reloading skin") + xbmc.executebuiltin("UpdateLibrary(video)") + xbmc.executebuiltin("ReloadSkin()") @stop def service(self): + """If error is encountered, it will rerun this function. + Start new "daemon threads" to process library updates. + (actual daemon thread is not supported in Kodi) + """ + self.download_threads = [ + thread for thread in self.download_threads if not thread.is_done + ] + self.writer_threads["updated"] = [ + thread for thread in self.writer_threads["updated"] if not thread.is_done + ] + self.writer_threads["userdata"] = [ + thread for thread in self.writer_threads["userdata"] if not thread.is_done + ] + self.writer_threads["removed"] = [ + thread for thread in self.writer_threads["removed"] if not thread.is_done + ] - ''' If error is encountered, it will rerun this function. - Start new "daemon threads" to process library updates. - (actual daemon thread is not supported in Kodi) - ''' - self.download_threads = [thread for thread in self.download_threads if not thread.is_done] - self.writer_threads['updated'] = [thread for thread in self.writer_threads['updated'] if not thread.is_done] - self.writer_threads['userdata'] = [thread for thread in self.writer_threads['userdata'] if not thread.is_done] - self.writer_threads['removed'] = [thread for thread in self.writer_threads['removed'] if not thread.is_done] - - if not self.player.isPlayingVideo() or settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + if ( + not self.player.isPlayingVideo() + or settings("syncDuringPlay.bool") + or xbmc.getCondVisibility("VideoPlayer.Content(livetv)") + ): self.worker_downloads() self.worker_sort() @@ -154,7 +163,7 @@ class Library(threading.Thread): self.worker_notify() if self.pending_refresh: - window('jellyfin_sync.bool', True) + window("jellyfin_sync.bool", True) if self.total_updates > self.progress_display: queue_size = self.worker_queue_size() @@ -162,58 +171,91 @@ class Library(threading.Thread): if self.progress_updates is None: self.progress_updates = xbmcgui.DialogProgressBG() - self.progress_updates.create(translate('addon_name'), translate(33178)) - self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size)) + self.progress_updates.create( + translate("addon_name"), translate(33178) + ) + self.progress_updates.update( + int( + ( + float(self.total_updates - queue_size) + / float(self.total_updates) + ) + * 100 + ), + message="%s: %s" % (translate(33178), queue_size), + ) elif queue_size: - self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size)) + self.progress_updates.update( + int( + ( + float(self.total_updates - queue_size) + / float(self.total_updates) + ) + * 100 + ), + message="%s: %s" % (translate(33178), queue_size), + ) else: - self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message=translate(33178)) + self.progress_updates.update( + int( + ( + float(self.total_updates - queue_size) + / float(self.total_updates) + ) + * 100 + ), + message=translate(33178), + ) - if not settings('dbSyncScreensaver.bool') and self.screensaver is None: + if not settings("dbSyncScreensaver.bool") and self.screensaver is None: - xbmc.executebuiltin('InhibitIdleShutdown(true)') + xbmc.executebuiltin("InhibitIdleShutdown(true)") self.screensaver = get_screensaver() set_screensaver(value="") - if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']): + if ( + self.pending_refresh + and not self.download_threads + and not self.writer_threads["updated"] + and not self.writer_threads["userdata"] + and not self.writer_threads["removed"] + ): self.pending_refresh = False self.save_last_sync() self.total_updates = 0 - window('jellyfin_sync', clear=True) + window("jellyfin_sync", clear=True) if self.progress_updates: self.progress_updates.close() self.progress_updates = None - if not settings('dbSyncScreensaver.bool') and self.screensaver is not None: + if not settings("dbSyncScreensaver.bool") and self.screensaver is not None: - xbmc.executebuiltin('InhibitIdleShutdown(false)') + xbmc.executebuiltin("InhibitIdleShutdown(false)") set_screensaver(value=self.screensaver) self.screensaver = None - if xbmc.getCondVisibility('Container.Content(musicvideos)'): # Prevent cursor from moving - xbmc.executebuiltin('Container.Refresh') + if xbmc.getCondVisibility( + "Container.Content(musicvideos)" + ): # Prevent cursor from moving + xbmc.executebuiltin("Container.Refresh") else: # Update widgets - xbmc.executebuiltin('UpdateLibrary(video)') + xbmc.executebuiltin("UpdateLibrary(video)") - if xbmc.getCondVisibility('Window.IsMedia'): - xbmc.executebuiltin('Container.Refresh') + if xbmc.getCondVisibility("Window.IsMedia"): + xbmc.executebuiltin("Container.Refresh") def stop_client(self): self.stop_thread = True def enable_pending_refresh(self): - - ''' When there's an active thread. Let the main thread know. - ''' + """When there's an active thread. Let the main thread know.""" self.pending_refresh = True - window('jellyfin_sync.bool', True) + window("jellyfin_sync.bool", True) def worker_queue_size(self): - - ''' Get how many items are queued up for worker threads. - ''' + """Get how many items are queued up for worker threads.""" total = 0 for queues in self.updated_output: @@ -228,10 +270,11 @@ class Library(threading.Thread): return total def worker_downloads(self): - - ''' Get items from jellyfin and place them in the appropriate queues. - ''' - for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)): + """Get items from jellyfin and place them in the appropriate queues.""" + for queue in ( + (self.updated_queue, self.updated_output), + (self.userdata_queue, self.userdata_output), + ): if queue[0].qsize() and len(self.download_threads) < DTHREADS: new_thread = GetItemWorker(self.server, queue[0], queue[1]) @@ -240,9 +283,7 @@ class Library(threading.Thread): self.download_threads.append(new_thread) def worker_sort(self): - - ''' Get items based on the local jellyfin database and place item in appropriate queues. - ''' + """Get items based on the local jellyfin database and place item in appropriate queues.""" if self.removed_queue.qsize() and len(self.jellyfin_threads) < 2: new_thread = SortWorker(self.removed_queue, self.removed_output) @@ -250,66 +291,96 @@ class Library(threading.Thread): LOG.info("-->[ q:sort/%s ]", id(new_thread)) def worker_updates(self): - - ''' Update items in the Kodi database. - ''' + """Update items in the Kodi database.""" for queues in self.updated_output: queue = self.updated_output[queues] - if queue.qsize() and not len(self.writer_threads['updated']): + if queue.qsize() and not len(self.writer_threads["updated"]): - if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = UpdateWorker(queue, self.notify_output, self.music_database_lock, "music", self.server, self.direct_path) + if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"): + new_thread = UpdateWorker( + queue, + self.notify_output, + self.music_database_lock, + "music", + self.server, + self.direct_path, + ) else: - new_thread = UpdateWorker(queue, self.notify_output, self.database_lock, "video", self.server, self.direct_path) + new_thread = UpdateWorker( + queue, + self.notify_output, + self.database_lock, + "video", + self.server, + self.direct_path, + ) new_thread.start() LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread)) - self.writer_threads['updated'].append(new_thread) + self.writer_threads["updated"].append(new_thread) self.enable_pending_refresh() def worker_userdata(self): - - ''' Update userdata in the Kodi database. - ''' + """Update userdata in the Kodi database.""" for queues in self.userdata_output: queue = self.userdata_output[queues] - if queue.qsize() and not len(self.writer_threads['userdata']): + if queue.qsize() and not len(self.writer_threads["userdata"]): - if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = UserDataWorker(queue, self.music_database_lock, "music", self.server, self.direct_path) + if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"): + new_thread = UserDataWorker( + queue, + self.music_database_lock, + "music", + self.server, + self.direct_path, + ) else: - new_thread = UserDataWorker(queue, self.database_lock, "video", self.server, self.direct_path) + new_thread = UserDataWorker( + queue, + self.database_lock, + "video", + self.server, + self.direct_path, + ) new_thread.start() LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread)) - self.writer_threads['userdata'].append(new_thread) + self.writer_threads["userdata"].append(new_thread) self.enable_pending_refresh() def worker_remove(self): - - ''' Remove items from the Kodi database. - ''' + """Remove items from the Kodi database.""" for queues in self.removed_output: queue = self.removed_output[queues] - if queue.qsize() and not len(self.writer_threads['removed']): + if queue.qsize() and not len(self.writer_threads["removed"]): - if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = RemovedWorker(queue, self.music_database_lock, "music", self.server, self.direct_path) + if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"): + new_thread = RemovedWorker( + queue, + self.music_database_lock, + "music", + self.server, + self.direct_path, + ) else: - new_thread = RemovedWorker(queue, self.database_lock, "video", self.server, self.direct_path) + new_thread = RemovedWorker( + queue, + self.database_lock, + "video", + self.server, + self.direct_path, + ) new_thread.start() LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread)) - self.writer_threads['removed'].append(new_thread) + self.writer_threads["removed"].append(new_thread) self.enable_pending_refresh() def worker_notify(self): - - ''' Notify the user of new additions. - ''' + """Notify the user of new additions.""" if self.notify_output.qsize() and not len(self.notify_threads): new_thread = NotifyWorker(self.notify_output, self.player) @@ -318,11 +389,10 @@ class Library(threading.Thread): self.notify_threads.append(new_thread) def startup(self): - - ''' Run at startup. - Check databases. - Check for the server plugin. - ''' + """Run at startup. + Check databases. + Check for the server plugin. + """ self.test_databases() self.check_version() @@ -330,7 +400,7 @@ class Library(threading.Thread): Views().get_nodes() try: - if get_sync()['Libraries']: + if get_sync()["Libraries"]: try: with FullSync(self, self.server) as sync: @@ -340,7 +410,7 @@ class Library(threading.Thread): except Exception as error: LOG.exception(error) - elif not settings('SyncInstallRunDone.bool'): + elif not settings("SyncInstallRunDone.bool"): with FullSync(self, self.server) as sync: sync.libraries() @@ -349,9 +419,7 @@ class Library(threading.Thread): return True - if settings('SyncInstallRunDone.bool') and settings( - 'kodiCompanion.bool' - ): + if settings("SyncInstallRunDone.bool") and settings("kodiCompanion.bool"): # None == Unknown if self.server.jellyfin.check_companion_enabled() is not False: @@ -372,12 +440,12 @@ class Library(threading.Thread): except LibraryException as error: LOG.error(error.status) - if error.status in 'SyncLibraryLater': + if error.status in "SyncLibraryLater": dialog("ok", "{jellyfin}", translate(33129)) - settings('SyncInstallRunDone.bool', True) + settings("SyncInstallRunDone.bool", True) sync = get_sync() - sync['Libraries'] = [] + sync["Libraries"] = [] save_sync(sync) return True @@ -388,33 +456,33 @@ class Library(threading.Thread): return False def fast_sync(self): - - ''' Movie and userdata not provided by server yet. - ''' - last_sync = settings('LastIncrementalSync') + """Movie and userdata not provided by server yet.""" + last_sync = settings("LastIncrementalSync") include = [] filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] sync = get_sync() - whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] + whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]] LOG.info("--[ retrieve changes ] %s", last_sync) # Get the item type of each synced library and build list of types to request for item_id in whitelist: library = self.server.jellyfin.get_item(item_id) - library_type = library.get('CollectionType') + library_type = library.get("CollectionType") if library_type in filters: include.append(library_type) # Include boxsets if movies are synced - if 'movies' in include: - include.append('boxsets') + if "movies" in include: + include.append("boxsets") # Filter down to the list of library types we want to exclude query_filter = list(set(filters) - set(include)) try: # Get list of updates from server for synced library types and populate work queues - result = self.server.jellyfin.get_sync_queue(last_sync, ",".join([x for x in query_filter])) + result = self.server.jellyfin.get_sync_queue( + last_sync, ",".join([x for x in query_filter]) + ) if result is None: return True @@ -423,18 +491,23 @@ class Library(threading.Thread): userdata = [] removed = [] - updated.extend(result['ItemsAdded']) - updated.extend(result['ItemsUpdated']) - userdata.extend(result['UserDataChanged']) - removed.extend(result['ItemsRemoved']) + updated.extend(result["ItemsAdded"]) + updated.extend(result["ItemsUpdated"]) + userdata.extend(result["UserDataChanged"]) + removed.extend(result["ItemsRemoved"]) total = len(updated) + len(userdata) - if total > int(settings('syncIndicator') or 99): + if total > int(settings("syncIndicator") or 99): - ''' Inverse yes no, in case the dialog is forced closed by Kodi. - ''' - if dialog("yesno", "{jellyfin}", translate(33172).replace('{number}', str(total)), nolabel=translate(107), yeslabel=translate(106)): + """Inverse yes no, in case the dialog is forced closed by Kodi.""" + if dialog( + "yesno", + "{jellyfin}", + translate(33172).replace("{number}", str(total)), + nolabel=translate(107), + yeslabel=translate(106), + ): LOG.warning("Large updates skipped.") return True @@ -453,56 +526,68 @@ class Library(threading.Thread): def save_last_sync(self): try: - time_now = datetime.strptime(self.server.config.data['server-time'].split(', ', 1)[1], '%d %b %Y %H:%M:%S GMT') - timedelta(minutes=2) + time_now = datetime.strptime( + self.server.config.data["server-time"].split(", ", 1)[1], + "%d %b %Y %H:%M:%S GMT", + ) - timedelta(minutes=2) except Exception as error: LOG.exception(error) time_now = datetime.utcnow() - timedelta(minutes=2) - last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') - settings('LastIncrementalSync', value=last_sync) + last_sync = time_now.strftime("%Y-%m-%dT%H:%M:%Sz") + settings("LastIncrementalSync", value=last_sync) LOG.info("--[ sync/%s ]", last_sync) def select_libraries(self, mode=None): - - ''' Select from libraries synced. Either update or repair libraries. - Send event back to service.py - ''' + """Select from libraries synced. Either update or repair libraries. + Send event back to service.py + """ modes = { - 'SyncLibrarySelection': 'SyncLibrary', - 'RepairLibrarySelection': 'RepairLibrary', - 'AddLibrarySelection': 'SyncLibrary', - 'RemoveLibrarySelection': 'RemoveLibrary' + "SyncLibrarySelection": "SyncLibrary", + "RepairLibrarySelection": "RepairLibrary", + "AddLibrarySelection": "SyncLibrary", + "RemoveLibrarySelection": "RemoveLibrary", } sync = get_sync() - whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] + whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]] libraries = [] - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'): - for library in sync['Whitelist']: + if mode in ( + "SyncLibrarySelection", + "RepairLibrarySelection", + "RemoveLibrarySelection", + ): + for library in sync["Whitelist"]: - name = db.get_view_name(library.replace('Mixed:', "")) - libraries.append({'Id': library, 'Name': name}) + name = db.get_view_name(library.replace("Mixed:", "")) + libraries.append({"Id": library, "Name": name}) else: - available = [x for x in sync['SortedViews'] if x not in whitelist] + available = [x for x in sync["SortedViews"] if x not in whitelist] for library in available: view = db.get_view(library) - if view.media_type in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): - libraries.append({'Id': view.view_id, 'Name': view.view_name}) + if view.media_type in ( + "movies", + "tvshows", + "musicvideos", + "mixed", + "music", + ): + libraries.append({"Id": view.view_id, "Name": view.view_name}) - choices = [x['Name'] for x in libraries] + choices = [x["Name"] for x in libraries] choices.insert(0, translate(33121)) titles = { "RepairLibrarySelection": 33199, "SyncLibrarySelection": 33198, "RemoveLibrarySelection": 33200, - "AddLibrarySelection": 33120 + "AddLibrarySelection": 33120, } title = titles.get(mode, "Failed to get title {}".format(mode)) @@ -519,9 +604,15 @@ class Library(threading.Thread): for x in selection: library = libraries[x - 1] - selected_libraries.append(library['Id']) + selected_libraries.append(library["Id"]) - event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'}) + event( + modes[mode], + { + "Id": ",".join([libraries[x - 1]["Id"] for x in selection]), + "Update": mode == "SyncLibrarySelection", + }, + ) def add_library(self, library_id, update=False): @@ -555,13 +646,11 @@ class Library(threading.Thread): return True def userdata(self, data): - - ''' Add item_id to userdata queue. - ''' + """Add item_id to userdata queue.""" if not data: return - items = [x['ItemId'] for x in data] + items = [x["ItemId"] for x in data] for item in split_list(items, LIMIT): self.userdata_queue.put(item) @@ -570,9 +659,7 @@ class Library(threading.Thread): LOG.info("---[ userdata:%s ]", len(items)) def updated(self, data): - - ''' Add item_id to updated queue. - ''' + """Add item_id to updated queue.""" if not data: return @@ -583,9 +670,7 @@ class Library(threading.Thread): LOG.info("---[ updated:%s ]", len(data)) def removed(self, data): - - ''' Add item_id to removed queue. - ''' + """Add item_id to removed queue.""" if not data: return @@ -604,10 +689,12 @@ class UpdateWorker(threading.Thread): is_done = False - def __init__(self, queue, notify, lock, database, server=None, direct_path=None, *args): + def __init__( + self, queue, notify, lock, database, server=None, direct_path=None, *args + ): self.queue = queue self.notify_output = notify - self.notify = settings('newContent.bool') + self.notify = settings("newContent.bool") self.lock = lock self.database = Database(database) self.args = args @@ -616,7 +703,7 @@ class UpdateWorker(threading.Thread): threading.Thread.__init__(self) def run(self): - with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb: + with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb: default_args = (self.server, jellyfindb, kodidb, self.direct_path) if kodidb.db_file == "video": movies = Movies(*default_args) @@ -626,7 +713,9 @@ class UpdateWorker(threading.Thread): music = Music(*default_args) else: # this should not happen - LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file)) + LOG.error( + '"{}" is not a valid Kodi library type.'.format(kodidb.db_file) + ) return while True: @@ -637,39 +726,41 @@ class UpdateWorker(threading.Thread): break try: - LOG.debug('{} - {}'.format(item['Type'], item['Name'])) - if item['Type'] == 'Movie': + LOG.debug("{} - {}".format(item["Type"], item["Name"])) + if item["Type"] == "Movie": movies.movie(item) - elif item['Type'] == 'BoxSet': + elif item["Type"] == "BoxSet": movies.boxset(item) - elif item['Type'] == 'Series': + elif item["Type"] == "Series": tvshows.tvshow(item) - elif item['Type'] == 'Season': + elif item["Type"] == "Season": tvshows.season(item) - elif item['Type'] == 'Episode': + elif item["Type"] == "Episode": tvshows.episode(item) - elif item['Type'] == 'MusicVideo': + elif item["Type"] == "MusicVideo": musicvideos.musicvideo(item) - elif item['Type'] == 'MusicAlbum': + elif item["Type"] == "MusicAlbum": music.album(item) - elif item['Type'] == 'MusicArtist': + elif item["Type"] == "MusicArtist": music.artist(item) - elif item['Type'] == 'AlbumArtist': + elif item["Type"] == "AlbumArtist": music.albumartist(item) - elif item['Type'] == 'Audio': + elif item["Type"] == "Audio": music.song(item) if self.notify: - self.notify_output.put((item['Type'], api.API(item).get_naming())) + self.notify_output.put( + (item["Type"], api.API(item).get_naming()) + ) except LibraryException as error: - if error.status == 'StopCalled': + if error.status == "StopCalled": break except Exception as error: LOG.exception(error) self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break LOG.info("--<[ q:updated/%s ]", id(self)) @@ -692,7 +783,7 @@ class UserDataWorker(threading.Thread): def run(self): - with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb: + with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb: default_args = (self.server, jellyfindb, kodidb, self.direct_path) if kodidb.db_file == "video": movies = Movies(*default_args) @@ -701,7 +792,9 @@ class UserDataWorker(threading.Thread): music = Music(*default_args) else: # this should not happen - LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file)) + LOG.error( + '"{}" is not a valid Kodi library type.'.format(kodidb.db_file) + ) return while True: @@ -712,27 +805,27 @@ class UserDataWorker(threading.Thread): break try: - if item['Type'] == 'Movie': + if item["Type"] == "Movie": movies.userdata(item) - elif item['Type'] in ['Series', 'Season', 'Episode']: + elif item["Type"] in ["Series", "Season", "Episode"]: tvshows.userdata(item) - elif item['Type'] == 'MusicAlbum': + elif item["Type"] == "MusicAlbum": music.album(item) - elif item['Type'] == 'MusicArtist': + elif item["Type"] == "MusicArtist": music.artist(item) - elif item['Type'] == 'AlbumArtist': + elif item["Type"] == "AlbumArtist": music.albumartist(item) - elif item['Type'] == 'Audio': + elif item["Type"] == "Audio": music.userdata(item) except LibraryException as error: - if error.status == 'StopCalled': + if error.status == "StopCalled": break except Exception as error: LOG.exception(error) self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break LOG.info("--<[ q:userdata/%s ]", id(self)) @@ -752,7 +845,7 @@ class SortWorker(threading.Thread): def run(self): - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: database = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) while True: @@ -765,21 +858,26 @@ class SortWorker(threading.Thread): try: media = database.get_media_by_id(item_id) if media: - self.output[media].put({'Id': item_id, 'Type': media}) + self.output[media].put({"Id": item_id, "Type": media}) else: items = database.get_media_by_parent_id(item_id) if not items: - LOG.info("Could not find media %s in the jellyfin database.", item_id) + LOG.info( + "Could not find media %s in the jellyfin database.", + item_id, + ) else: for item in items: - self.output[item[1]].put({'Id': item[0], 'Type': item[1]}) + self.output[item[1]].put( + {"Id": item[0], "Type": item[1]} + ) except Exception as error: LOG.exception(error) self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break LOG.info("--<[ q:sort/%s ]", id(self)) @@ -801,7 +899,7 @@ class RemovedWorker(threading.Thread): def run(self): - with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb: + with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb: default_args = (self.server, jellyfindb, kodidb, self.direct_path) if kodidb.db_file == "video": movies = Movies(*default_args) @@ -811,7 +909,9 @@ class RemovedWorker(threading.Thread): music = Music(*default_args) else: # this should not happen - LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file)) + LOG.error( + '"{}" is not a valid Kodi library type.'.format(kodidb.db_file) + ) return while True: @@ -821,26 +921,31 @@ class RemovedWorker(threading.Thread): except Queue.Empty: break - if item['Type'] == 'Movie': + if item["Type"] == "Movie": obj = movies.remove - elif item['Type'] in ['Series', 'Season', 'Episode']: + elif item["Type"] in ["Series", "Season", "Episode"]: obj = tvshows.remove - elif item['Type'] in ['MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio']: + elif item["Type"] in [ + "MusicAlbum", + "MusicArtist", + "AlbumArtist", + "Audio", + ]: obj = music.remove - elif item['Type'] == 'MusicVideo': + elif item["Type"] == "MusicVideo": obj = musicvideos.remove try: - obj(item['Id']) + obj(item["Id"]) except LibraryException as error: - if error.status == 'StopCalled': + if error.status == "StopCalled": break except Exception as error: LOG.exception(error) finally: self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break LOG.info("--<[ q:removed/%s ]", id(self)) @@ -854,8 +959,8 @@ class NotifyWorker(threading.Thread): def __init__(self, queue, player): self.queue = queue - self.video_time = int(settings('newvideotime')) * 1000 - self.music_time = int(settings('newmusictime')) * 1000 + self.video_time = int(settings("newvideotime")) * 1000 + self.music_time = int(settings("newmusictime")) * 1000 self.player = player threading.Thread.__init__(self) @@ -868,15 +973,24 @@ class NotifyWorker(threading.Thread): except Queue.Empty: break - time = self.music_time if item[0] == 'Audio' else self.video_time + time = self.music_time if item[0] == "Audio" else self.video_time - if time and (not self.player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): - dialog("notification", heading="%s %s" % (translate(33049), item[0]), message=item[1], - icon="{jellyfin}", time=time, sound=False) + if time and ( + not self.player.isPlayingVideo() + or xbmc.getCondVisibility("VideoPlayer.Content(livetv)") + ): + dialog( + "notification", + heading="%s %s" % (translate(33049), item[0]), + message=item[1], + icon="{jellyfin}", + time=time, + sound=False, + ) self.queue.task_done() - if window('jellyfin_should_stop.bool'): + if window("jellyfin_should_stop.bool"): break LOG.info("--<[ q:notify/%s ]", id(self)) diff --git a/jellyfin_kodi/monitor.py b/jellyfin_kodi/monitor.py index 31b9d67c..4f02226b 100644 --- a/jellyfin_kodi/monitor.py +++ b/jellyfin_kodi/monitor.py @@ -46,23 +46,35 @@ class Monitor(xbmc.Monitor): def onNotification(self, sender, method, data): - if sender.lower() not in ('plugin.video.jellyfin', 'xbmc', 'upnextprovider.signal'): + if sender.lower() not in ( + "plugin.video.jellyfin", + "xbmc", + "upnextprovider.signal", + ): return - if sender == 'plugin.video.jellyfin': - method = method.split('.')[1] + if sender == "plugin.video.jellyfin": + method = method.split(".")[1] - if method not in ('ReportProgressRequested', 'LoadServer', 'AddUser', 'PlayPlaylist', 'Play', 'Playstate', 'GeneralCommand'): + if method not in ( + "ReportProgressRequested", + "LoadServer", + "AddUser", + "PlayPlaylist", + "Play", + "Playstate", + "GeneralCommand", + ): return data = json.loads(data)[0] - elif sender.startswith('upnextprovider'): - LOG.info('Attempting to play the next episode via upnext') - method = method.split('.', 1)[1] + elif sender.startswith("upnextprovider"): + LOG.info("Attempting to play the next episode via upnext") + method = method.split(".", 1)[1] - if method not in ('plugin.video.jellyfin_play_action',): - LOG.info('Received invalid upnext method: %s', method) + if method not in ("plugin.video.jellyfin_play_action",): + LOG.info("Received invalid upnext method: %s", method) return data = json.loads(data) @@ -71,15 +83,23 @@ class Monitor(xbmc.Monitor): if data: data = json.loads(binascii.unhexlify(data[0])) else: - if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'): + if method not in ( + "Player.OnPlay", + "VideoLibrary.OnUpdate", + "Player.OnAVChange", + ): - ''' We have to clear the playlist if it was stopped before it has been played completely. - Otherwise the next played item will be added the previous queue. - ''' + """We have to clear the playlist if it was stopped before it has been played completely. + Otherwise the next played item will be added the previous queue. + """ if method == "Player.OnStop": - xbmc.sleep(3000) # let's wait for the player, so we don't clear the canceled playlist by mistake. + xbmc.sleep( + 3000 + ) # let's wait for the player, so we don't clear the canceled playlist by mistake. - if xbmc.getCondVisibility("!Player.HasMedia + !Window.IsVisible(busydialog)"): + if xbmc.getCondVisibility( + "!Player.HasMedia + !Window.IsVisible(busydialog)" + ): xbmc.executebuiltin("Playlist.Clear") LOG.info("[ playlist ] cleared") @@ -96,14 +116,14 @@ class Monitor(xbmc.Monitor): return try: - if not data.get('ServerId'): + if not data.get("ServerId"): server = Jellyfin() else: - if method != 'LoadServer' and data['ServerId'] not in self.servers: + if method != "LoadServer" and data["ServerId"] not in self.servers: try: - connect.Connect().register(data['ServerId']) - self.server_instance(data['ServerId']) + connect.Connect().register(data["ServerId"]) + self.server_instance(data["ServerId"]) except Exception as error: LOG.exception(error) @@ -111,80 +131,90 @@ class Monitor(xbmc.Monitor): return - server = Jellyfin(data['ServerId']) + server = Jellyfin(data["ServerId"]) except Exception as error: LOG.exception(error) server = Jellyfin() server = server.get_client() - if method == 'Play': + if method == "Play": - items = server.jellyfin.get_items(data['ItemIds']) + items = server.jellyfin.get_items(data["ItemIds"]) - PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow', - data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), - data.get('SubtitleStreamIndex')).start() + PlaylistWorker( + data.get("ServerId"), + items, + data["PlayCommand"] == "PlayNow", + data.get("StartPositionTicks", 0), + data.get("AudioStreamIndex"), + data.get("SubtitleStreamIndex"), + ).start() # TODO no clue if this is called by anything - elif method == 'PlayPlaylist': + elif method == "PlayPlaylist": - server.jellyfin.post_session(server.config.data['app.session'], "Playing", { - 'PlayCommand': "PlayNow", - 'ItemIds': data['Id'], - 'StartPositionTicks': 0 - }) + server.jellyfin.post_session( + server.config.data["app.session"], + "Playing", + { + "PlayCommand": "PlayNow", + "ItemIds": data["Id"], + "StartPositionTicks": 0, + }, + ) - elif method in ('ReportProgressRequested', 'Player.OnAVChange'): - self.player.report_playback(data.get('Report', True)) + elif method in ("ReportProgressRequested", "Player.OnAVChange"): + self.player.report_playback(data.get("Report", True)) - elif method == 'Playstate': + elif method == "Playstate": self.playstate(data) - elif method == 'GeneralCommand': + elif method == "GeneralCommand": self.general_commands(data) - elif method == 'LoadServer': - self.server_instance(data['ServerId']) + elif method == "LoadServer": + self.server_instance(data["ServerId"]) - elif method == 'AddUser': - server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add']) + elif method == "AddUser": + server.jellyfin.session_add_user( + server.config.data["app.session"], data["Id"], data["Add"] + ) self.additional_users(server) - elif method == 'Player.OnPlay': + elif method == "Player.OnPlay": on_play(data, server) - elif method == 'VideoLibrary.OnUpdate': + elif method == "VideoLibrary.OnUpdate": on_update(data, server) def server_instance(self, server_id=None): server = Jellyfin(server_id).get_client() session = server.jellyfin.get_device(self.device_id) - server.config.data['app.session'] = session[0]['Id'] + server.config.data["app.session"] = session[0]["Id"] if server_id is not None: self.servers.append(server_id) - elif settings('additionalUsers'): + elif settings("additionalUsers"): - users = settings('additionalUsers').split(',') + users = settings("additionalUsers").split(",") all_users = server.jellyfin.get_users() for additional in users: for user in all_users: - if user['Name'].lower() in additional.lower(): - server.jellyfin.session_add_user(server.config.data['app.session'], user['Id'], True) + if user["Name"].lower() in additional.lower(): + server.jellyfin.session_add_user( + server.config.data["app.session"], user["Id"], True + ) self.additional_users(server) - def additional_users(self, server): - - ''' Setup additional users images. - ''' + """Setup additional users images.""" for i in range(10): - window('JellyfinAdditionalUserImage.%s' % i, clear=True) + window("JellyfinAdditionalUserImage.%s" % i, clear=True) try: session = server.jellyfin.get_device(self.device_id) @@ -193,31 +223,31 @@ class Monitor(xbmc.Monitor): return - for index, user in enumerate(session[0]['AdditionalUsers']): + for index, user in enumerate(session[0]["AdditionalUsers"]): - info = server.jellyfin.get_user(user['UserId']) - image = api.API(info, server.config.data['auth.server']).get_user_artwork(user['UserId']) - window('JellyfinAdditionalUserImage.%s' % index, image) - window('JellyfinAdditionalUserPosition.%s' % user['UserId'], str(index)) + info = server.jellyfin.get_user(user["UserId"]) + image = api.API(info, server.config.data["auth.server"]).get_user_artwork( + user["UserId"] + ) + window("JellyfinAdditionalUserImage.%s" % index, image) + window("JellyfinAdditionalUserPosition.%s" % user["UserId"], str(index)) def playstate(self, data): - - ''' Jellyfin playstate updates. - ''' - command = data['Command'] + """Jellyfin playstate updates.""" + command = data["Command"] actions = { - 'Stop': self.player.stop, - 'Unpause': self.player.pause, - 'Pause': self.player.pause, - 'PlayPause': self.player.pause, - 'NextTrack': self.player.playnext, - 'PreviousTrack': self.player.playprevious + "Stop": self.player.stop, + "Unpause": self.player.pause, + "Pause": self.player.pause, + "PlayPause": self.player.pause, + "NextTrack": self.player.playnext, + "PreviousTrack": self.player.playprevious, } - if command == 'Seek': + if command == "Seek": if self.player.isPlaying(): - seektime = data['SeekPositionTicks'] / 10000000.0 + seektime = data["SeekPositionTicks"] / 10000000.0 self.player.seekTime(seektime) LOG.info("[ seek/%s ]", seektime) @@ -227,69 +257,78 @@ class Monitor(xbmc.Monitor): LOG.info("[ command/%s ]", command) def general_commands(self, data): + """General commands from Jellyfin to control the Kodi interface.""" + command = data["Name"] + args = data["Arguments"] - ''' General commands from Jellyfin to control the Kodi interface. - ''' - command = data['Name'] - args = data['Arguments'] + if command in ( + "Mute", + "Unmute", + "SetVolume", + "SetSubtitleStreamIndex", + "SetAudioStreamIndex", + "SetRepeatMode", + ): - if command in ('Mute', 'Unmute', 'SetVolume', - 'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'): + if command in ["Mute", "Unmute"]: + xbmc.executebuiltin("Mute") + elif command == "SetAudioStreamIndex": + self.player.set_audio_subs(args["Index"]) + elif command == "SetRepeatMode": + xbmc.executebuiltin("xbmc.PlayerControl(%s)" % args["RepeatMode"]) + elif command == "SetSubtitleStreamIndex": + self.player.set_audio_subs(None, args["Index"]) - if command in ['Mute', 'Unmute']: - xbmc.executebuiltin('Mute') - elif command == 'SetAudioStreamIndex': - self.player.set_audio_subs(args['Index']) - elif command == 'SetRepeatMode': - xbmc.executebuiltin('xbmc.PlayerControl(%s)' % args['RepeatMode']) - elif command == 'SetSubtitleStreamIndex': - self.player.set_audio_subs(None, args['Index']) - - elif command == 'SetVolume': - xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume']) + elif command == "SetVolume": + xbmc.executebuiltin("SetVolume(%s[,showvolumebar])" % args["Volume"]) # Kodi needs a bit of time to update its current status xbmc.sleep(500) self.player.report_playback() - elif command == 'DisplayMessage': - dialog("notification", heading=args['Header'], message=args['Text'], - icon="{jellyfin}", time=int(settings('displayMessage')) * 1000) + elif command == "DisplayMessage": + dialog( + "notification", + heading=args["Header"], + message=args["Text"], + icon="{jellyfin}", + time=int(settings("displayMessage")) * 1000, + ) - elif command == 'SendString': - JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) + elif command == "SendString": + JSONRPC("Input.SendText").execute({"text": args["String"], "done": False}) - elif command == 'GoHome': - JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) + elif command == "GoHome": + JSONRPC("GUI.ActivateWindow").execute({"window": "home"}) - elif command == 'Guide': - JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) + elif command == "Guide": + JSONRPC("GUI.ActivateWindow").execute({"window": "tvguide"}) - elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): + elif command in ("MoveUp", "MoveDown", "MoveRight", "MoveLeft"): actions = { - 'MoveUp': "Input.Up", - 'MoveDown': "Input.Down", - 'MoveRight': "Input.Right", - 'MoveLeft': "Input.Left" + "MoveUp": "Input.Up", + "MoveDown": "Input.Down", + "MoveRight": "Input.Right", + "MoveLeft": "Input.Left", } JSONRPC(actions[command]).execute() else: builtin = { - 'ToggleFullscreen': 'Action(FullScreen)', - 'ToggleOsdMenu': 'Action(OSD)', - 'ToggleContextMenu': 'Action(ContextMenu)', - 'Select': 'Action(Select)', - 'Back': 'Action(back)', - 'PageUp': 'Action(PageUp)', - 'NextLetter': 'Action(NextLetter)', - 'GoToSearch': 'VideoLibrary.Search', - 'GoToSettings': 'ActivateWindow(Settings)', - 'PageDown': 'Action(PageDown)', - 'PreviousLetter': 'Action(PrevLetter)', - 'TakeScreenshot': 'TakeScreenshot', - 'ToggleMute': 'Mute', - 'VolumeUp': 'Action(VolumeUp)', - 'VolumeDown': 'Action(VolumeDown)', + "ToggleFullscreen": "Action(FullScreen)", + "ToggleOsdMenu": "Action(OSD)", + "ToggleContextMenu": "Action(ContextMenu)", + "Select": "Action(Select)", + "Back": "Action(back)", + "PageUp": "Action(PageUp)", + "NextLetter": "Action(NextLetter)", + "GoToSearch": "VideoLibrary.Search", + "GoToSettings": "ActivateWindow(Settings)", + "PageDown": "Action(PageDown)", + "PreviousLetter": "Action(PrevLetter)", + "TakeScreenshot": "TakeScreenshot", + "ToggleMute": "Mute", + "VolumeUp": "Action(VolumeUp)", + "VolumeDown": "Action(VolumeDown)", } if command in builtin: xbmc.executebuiltin(builtin[command]) @@ -305,10 +344,9 @@ class Listener(threading.Thread): threading.Thread.__init__(self) def run(self): - - ''' Detect the resume dialog for widgets. - Detect external players. - ''' + """Detect the resume dialog for widgets. + Detect external players. + """ LOG.info("--->[ listener ]") while not self.stop_thread: diff --git a/jellyfin_kodi/objects/actions.py b/jellyfin_kodi/objects/actions.py index dbac009e..4711e1e1 100644 --- a/jellyfin_kodi/objects/actions.py +++ b/jellyfin_kodi/objects/actions.py @@ -31,43 +31,47 @@ class Actions(object): self.server_id = server_id or None if not api_client: - LOG.debug('No api client provided, attempting to use config file') + LOG.debug("No api client provided, attempting to use config file") jellyfin_client = Jellyfin(server_id).get_client() api_client = jellyfin_client.jellyfin - addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json") + addon_data = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/data.json" + ) try: - with open(addon_data, 'rb') as infile: + with open(addon_data, "rb") as infile: data = json.load(infile) - server_data = data['Servers'][0] - api_client.config.data['auth.server'] = server_data.get('address') - api_client.config.data['auth.server-name'] = server_data.get('Name') - api_client.config.data['auth.user_id'] = server_data.get('UserId') - api_client.config.data['auth.token'] = server_data.get('AccessToken') + server_data = data["Servers"][0] + api_client.config.data["auth.server"] = server_data.get("address") + api_client.config.data["auth.server-name"] = server_data.get("Name") + api_client.config.data["auth.user_id"] = server_data.get("UserId") + api_client.config.data["auth.token"] = server_data.get( + "AccessToken" + ) except Exception as e: - LOG.warning('Addon appears to not be configured yet: {}'.format(e)) + LOG.warning("Addon appears to not be configured yet: {}".format(e)) self.api_client = api_client - self.server = self.api_client.config.data['auth.server'] + self.server = self.api_client.config.data["auth.server"] self.stack = [] def get_playlist(self, item): - if item['Type'] == 'Audio': + if item["Type"] == "Audio": return xbmc.PlayList(xbmc.PLAYLIST_MUSIC) return xbmc.PlayList(xbmc.PLAYLIST_VIDEO) def play(self, item, db_id=None, transcode=False, playlist=False): - - ''' Play requested item - ''' + """Play requested item""" listitem = xbmcgui.ListItem() - LOG.info("[ play/%s ] %s", item['Id'], item['Name']) + LOG.info("[ play/%s ] %s", item["Id"], item["Name"]) - transcode = transcode or settings('playFromTranscode.bool') - play = playutils.PlayUtils(item, transcode, self.server_id, self.server, self.api_client) + transcode = transcode or settings("playFromTranscode.bool") + play = playutils.PlayUtils( + item, transcode, self.server_id, self.server, self.api_client + ) source = play.select_source(play.get_sources()) play.set_external_subs(source, listitem) @@ -79,43 +83,42 @@ class Actions(object): xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1]) def set_playlist(self, item, listitem, db_id=None, transcode=False): + """Verify seektime, set intros, set main item and set additional parts. + Detect the seektime for video type content. + Verify the default video action set in Kodi for accurate resume behavior. + """ - ''' Verify seektime, set intros, set main item and set additional parts. - Detect the seektime for video type content. - Verify the default video action set in Kodi for accurate resume behavior. - ''' - - if item['MediaType'] in ('Video', 'Audio'): - resume = item['UserData'].get('PlaybackPositionTicks') + if item["MediaType"] in ("Video", "Audio"): + resume = item["UserData"].get("PlaybackPositionTicks") if resume and transcode: - choice = self.resume_dialog(api.API(item, self.server).adjust_resume((resume or 0) / 10000000.0)) + choice = self.resume_dialog( + api.API(item, self.server).adjust_resume((resume or 0) / 10000000.0) + ) if choice is None: raise Exception("User backed out of resume dialog.") item["resumePlayback"] = bool(choice) - if settings('enableCinema.bool') and not item["resumePlayback"]: + if settings("enableCinema.bool") and not item["resumePlayback"]: self._set_intros(item) self.set_listitem(item, listitem, db_id, None) - playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) - self.stack.append([item['PlaybackInfo']['Path'], listitem]) + playutils.set_properties(item, item["PlaybackInfo"]["Method"], self.server_id) + self.stack.append([item["PlaybackInfo"]["Path"], listitem]) - if item.get('PartCount'): - self._set_additional_parts(item['Id']) + if item.get("PartCount"): + self._set_additional_parts(item["Id"]) def _set_intros(self, item): + """if we have any play them when the movie/show is not being resumed.""" + intros = self.api_client.get_intros(item["Id"]) - ''' if we have any play them when the movie/show is not being resumed. - ''' - intros = self.api_client.get_intros(item['Id']) - - if intros['Items']: + if intros["Items"]: enabled = True - if settings('askCinema') == "true": + if settings("askCinema") == "true": resp = dialog("yesno", "{jellyfin}", translate(33016)) if not resp: @@ -124,45 +127,51 @@ class Actions(object): LOG.info("Skip trailers.") if enabled: - for intro in intros['Items']: + for intro in intros["Items"]: listitem = xbmcgui.ListItem() - LOG.info("[ intro/%s ] %s", intro['Id'], intro['Name']) + LOG.info("[ intro/%s ] %s", intro["Id"], intro["Name"]) - play = playutils.PlayUtils(intro, False, self.server_id, self.server, self.api_client) + play = playutils.PlayUtils( + intro, False, self.server_id, self.server, self.api_client + ) play.select_source(play.get_sources()) self.set_listitem(intro, listitem, intro=True) - listitem.setPath(intro['PlaybackInfo']['Path']) - playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) + listitem.setPath(intro["PlaybackInfo"]["Path"]) + playutils.set_properties( + intro, intro["PlaybackInfo"]["Method"], self.server_id + ) - self.stack.append([intro['PlaybackInfo']['Path'], listitem]) + self.stack.append([intro["PlaybackInfo"]["Path"], listitem]) - window('jellyfin.skip.%s' % intro['Id'], value="true") + window("jellyfin.skip.%s" % intro["Id"], value="true") def _set_additional_parts(self, item_id): - - ''' Create listitems and add them to the stack of playlist. - ''' + """Create listitems and add them to the stack of playlist.""" parts = self.api_client.get_additional_parts(item_id) - for part in parts['Items']: + for part in parts["Items"]: listitem = xbmcgui.ListItem() - LOG.info("[ part/%s ] %s", part['Id'], part['Name']) + LOG.info("[ part/%s ] %s", part["Id"], part["Name"]) - play = playutils.PlayUtils(part, False, self.server_id, self.server, self.api_client) + play = playutils.PlayUtils( + part, False, self.server_id, self.server, self.api_client + ) source = play.select_source(play.get_sources()) play.set_external_subs(source, listitem) self.set_listitem(part, listitem) - listitem.setPath(part['PlaybackInfo']['Path']) - playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) + listitem.setPath(part["PlaybackInfo"]["Path"]) + playutils.set_properties( + part, part["PlaybackInfo"]["Method"], self.server_id + ) - self.stack.append([part['PlaybackInfo']['Path'], listitem]) + self.stack.append([part["PlaybackInfo"]["Path"], listitem]) - def play_playlist(self, items, clear=True, seektime=None, audio=None, subtitle=None): - - ''' Play a list of items. Creates a new playlist. Add additional items as plugin listing. - ''' - item = items['Items'][0] + def play_playlist( + self, items, clear=True, seektime=None, audio=None, subtitle=None + ): + """Play a list of items. Creates a new playlist. Add additional items as plugin listing.""" + item = items["Items"][0] playlist = self.get_playlist(item) player = xbmc.Player() @@ -170,55 +179,66 @@ class Actions(object): if player.isPlaying(): player.stop() - xbmc.executebuiltin('ActivateWindow(busydialognocancel)') + xbmc.executebuiltin("ActivateWindow(busydialognocancel)") playlist.clear() index = 0 else: index = max(playlist.getposition(), 0) + 1 # Can return -1 listitem = xbmcgui.ListItem() - LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + LOG.info("[ playlist/%s ] %s", item["Id"], item["Name"]) # Automatically resume if the item is in progress (casting from server) - resume = item['UserData'].get('PlaybackPositionTicks') + resume = item["UserData"].get("PlaybackPositionTicks") item["resumePlayback"] = bool(resume) - play = playutils.PlayUtils(item, False, self.server_id, self.server, self.api_client) + play = playutils.PlayUtils( + item, False, self.server_id, self.server, self.api_client + ) source = play.select_source(play.get_sources()) play.set_external_subs(source, listitem) - item['PlaybackInfo']['AudioStreamIndex'] = audio or item['PlaybackInfo']['AudioStreamIndex'] - item['PlaybackInfo']['SubtitleStreamIndex'] = subtitle or item['PlaybackInfo'].get('SubtitleStreamIndex') + item["PlaybackInfo"]["AudioStreamIndex"] = ( + audio or item["PlaybackInfo"]["AudioStreamIndex"] + ) + item["PlaybackInfo"]["SubtitleStreamIndex"] = subtitle or item[ + "PlaybackInfo" + ].get("SubtitleStreamIndex") self.set_listitem(item, listitem, None, True if seektime else False) - listitem.setPath(item['PlaybackInfo']['Path']) - playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) + listitem.setPath(item["PlaybackInfo"]["Path"]) + playutils.set_properties(item, item["PlaybackInfo"]["Method"], self.server_id) - playlist.add(item['PlaybackInfo']['Path'], listitem, index) + playlist.add(item["PlaybackInfo"]["Path"], listitem, index) if clear: - xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + xbmc.executebuiltin("Dialog.Close(busydialognocancel)") player.play(playlist, startpos=index) index += 1 - server_address = item['PlaybackInfo']['ServerAddress'] - token = item['PlaybackInfo']['Token'] + server_address = item["PlaybackInfo"]["ServerAddress"] + token = item["PlaybackInfo"]["Token"] - for item in items['Items'][1:]: + for item in items["Items"][1:]: listitem = xbmcgui.ListItem() - LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + LOG.info("[ playlist/%s ] %s", item["Id"], item["Name"]) self.set_listitem(item, listitem, None, False) - path = '{}/Audio/{}/stream.mp3?static=true&api_key={}'.format( - server_address, item['Id'], token) + path = "{}/Audio/{}/stream.mp3?static=true&api_key={}".format( + server_address, item["Id"], token + ) listitem.setPath(path) - play = playutils.PlayUtils(item, False, self.server_id, self.server, self.api_client) + play = playutils.PlayUtils( + item, False, self.server_id, self.server, self.api_client + ) source = play.select_source(play.get_sources()) play.set_external_subs(source, listitem) - playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) + playutils.set_properties( + item, item["PlaybackInfo"]["Method"], self.server_id + ) playlist.add(path, listitem, index) index += 1 @@ -228,463 +248,540 @@ class Actions(object): objects = Objects() API = api.API(item, self.server) - if item['Type'] in ('MusicArtist', 'MusicAlbum', 'Audio'): + if item["Type"] in ("MusicArtist", "MusicAlbum", "Audio"): - obj = objects.map(item, 'BrowseAudio') - obj['DbId'] = db_id - obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkMusic'), True) + obj = objects.map(item, "BrowseAudio") + obj["DbId"] = db_id + obj["Artwork"] = API.get_all_artwork( + objects.map(item, "ArtworkMusic"), True + ) self.listitem_music(obj, listitem, item) - elif item['Type'] in ('Photo', 'PhotoAlbum'): + elif item["Type"] in ("Photo", "PhotoAlbum"): - obj = objects.map(item, 'BrowsePhoto') - obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork')) + obj = objects.map(item, "BrowsePhoto") + obj["Artwork"] = API.get_all_artwork(objects.map(item, "Artwork")) self.listitem_photo(obj, listitem, item) - elif item['Type'] in ('TvChannel',): + elif item["Type"] in ("TvChannel",): - obj = objects.map(item, 'BrowseChannel') - obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork')) + obj = objects.map(item, "BrowseChannel") + obj["Artwork"] = API.get_all_artwork(objects.map(item, "Artwork")) self.listitem_channel(obj, listitem, item) else: - obj = objects.map(item, 'BrowseVideo') - obj['DbId'] = db_id - obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkParent'), True) + obj = objects.map(item, "BrowseVideo") + obj["DbId"] = db_id + obj["Artwork"] = API.get_all_artwork( + objects.map(item, "ArtworkParent"), True + ) if intro: - obj['Artwork']['Primary'] = "&KodiCinemaMode=true" + obj["Artwork"]["Primary"] = "&KodiCinemaMode=true" self.listitem_video(obj, listitem, item, seektime, intro) - if 'PlaybackInfo' in item: + if "PlaybackInfo" in item: if seektime: - item['PlaybackInfo']['CurrentPosition'] = obj['Resume'] + item["PlaybackInfo"]["CurrentPosition"] = obj["Resume"] - if 'SubtitleUrl' in item['PlaybackInfo']: + if "SubtitleUrl" in item["PlaybackInfo"]: - LOG.info("[ subtitles ] %s", item['PlaybackInfo']['SubtitleUrl']) - listitem.setSubtitles([item['PlaybackInfo']['SubtitleUrl']]) + LOG.info("[ subtitles ] %s", item["PlaybackInfo"]["SubtitleUrl"]) + listitem.setSubtitles([item["PlaybackInfo"]["SubtitleUrl"]]) - if item['Type'] == 'Episode': + if item["Type"] == "Episode": - item['PlaybackInfo']['CurrentEpisode'] = objects.map(item, "UpNext") - item['PlaybackInfo']['CurrentEpisode']['art'] = { - 'tvshow.poster': obj['Artwork'].get('Series.Primary'), - 'thumb': obj['Artwork'].get('Primary'), - 'tvshow.fanart': None + item["PlaybackInfo"]["CurrentEpisode"] = objects.map(item, "UpNext") + item["PlaybackInfo"]["CurrentEpisode"]["art"] = { + "tvshow.poster": obj["Artwork"].get("Series.Primary"), + "thumb": obj["Artwork"].get("Primary"), + "tvshow.fanart": None, } - if obj['Artwork']['Backdrop']: - item['PlaybackInfo']['CurrentEpisode']['art']['tvshow.fanart'] = obj['Artwork']['Backdrop'][0] + if obj["Artwork"]["Backdrop"]: + item["PlaybackInfo"]["CurrentEpisode"]["art"][ + "tvshow.fanart" + ] = obj["Artwork"]["Backdrop"][0] listitem.setContentLookup(False) def listitem_video(self, obj, listitem, item, seektime=None, intro=False): - - ''' Set listitem for video content. That also include streams. - ''' + """Set listitem for video content. That also include streams.""" API = api.API(item, self.server) - is_video = obj['MediaType'] in ('Video', 'Audio') # audiobook + is_video = obj["MediaType"] in ("Video", "Audio") # audiobook - obj['Genres'] = " / ".join(obj['Genres'] or []) - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] - obj['Studios'] = " / ".join(obj['Studios']) - obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) - obj['People'] = obj['People'] or [] - obj['Countries'] = " / ".join(obj['Countries'] or []) - obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Writers'] = " / ".join(obj['Writers'] or []) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['ShortPlot'] = API.get_overview(obj['ShortPlot']) - obj['DateAdded'] = obj['DateAdded'].split('.')[0].replace('T', " ") - obj['Rating'] = obj['Rating'] or 0 - obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['DateAdded'].split('T')[0].split('-'))) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 - obj['Overlay'] = 7 if obj['Played'] else 6 - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - obj['ChildCount'] = obj['ChildCount'] or 0 - obj['RecursiveCount'] = obj['RecursiveCount'] or 0 - obj['Unwatched'] = obj['Unwatched'] or 0 - obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or [] - obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or "" + obj["Genres"] = " / ".join(obj["Genres"] or []) + obj["Studios"] = [ + API.validate_studio(studio) for studio in (obj["Studios"] or []) + ] + obj["Studios"] = " / ".join(obj["Studios"]) + obj["Mpaa"] = API.get_mpaa(obj["Mpaa"]) + obj["People"] = obj["People"] or [] + obj["Countries"] = " / ".join(obj["Countries"] or []) + obj["Directors"] = " / ".join(obj["Directors"] or []) + obj["Writers"] = " / ".join(obj["Writers"] or []) + obj["Plot"] = API.get_overview(obj["Plot"]) + obj["ShortPlot"] = API.get_overview(obj["ShortPlot"]) + obj["DateAdded"] = obj["DateAdded"].split(".")[0].replace("T", " ") + obj["Rating"] = obj["Rating"] or 0 + obj["FileDate"] = "%s.%s.%s" % tuple( + reversed(obj["DateAdded"].split("T")[0].split("-")) + ) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) or 0 + obj["Overlay"] = 7 if obj["Played"] else 6 + obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"]) + obj["Audio"] = API.audio_streams(obj["Audio"] or []) + obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"]) + obj["ChildCount"] = obj["ChildCount"] or 0 + obj["RecursiveCount"] = obj["RecursiveCount"] or 0 + obj["Unwatched"] = obj["Unwatched"] or 0 + obj["Artwork"]["Backdrop"] = obj["Artwork"]["Backdrop"] or [] + obj["Artwork"]["Thumb"] = obj["Artwork"]["Thumb"] or "" - if not intro and obj['Type'] != 'Trailer': - obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \ + if not intro and obj["Type"] != "Trailer": + obj["Artwork"]["Primary"] = ( + obj["Artwork"]["Primary"] or "special://home/addons/plugin.video.jellyfin/resources/icon.png" + ) else: - obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \ - or obj['Artwork']['Thumb'] \ - or (obj['Artwork']['Backdrop'][0] - if len(obj['Artwork']['Backdrop']) - else "special://home/addons/plugin.video.jellyfin/resources/fanart.png") - obj['Artwork']['Primary'] += "&KodiTrailer=true" \ - if obj['Type'] == 'Trailer' else "&KodiCinemaMode=true" - obj['Artwork']['Backdrop'] = [obj['Artwork']['Primary']] + obj["Artwork"]["Primary"] = ( + obj["Artwork"]["Primary"] + or obj["Artwork"]["Thumb"] + or ( + obj["Artwork"]["Backdrop"][0] + if len(obj["Artwork"]["Backdrop"]) + else "special://home/addons/plugin.video.jellyfin/resources/fanart.png" + ) + ) + obj["Artwork"]["Primary"] += ( + "&KodiTrailer=true" + if obj["Type"] == "Trailer" + else "&KodiCinemaMode=true" + ) + obj["Artwork"]["Backdrop"] = [obj["Artwork"]["Primary"]] - self.set_artwork(obj['Artwork'], listitem, obj['Type']) + self.set_artwork(obj["Artwork"], listitem, obj["Type"]) - if intro or obj['Type'] == 'Trailer': - listitem.setArt({'poster': ""}) # Clear the poster value for intros / trailers to prevent issues in skins + if intro or obj["Type"] == "Trailer": + listitem.setArt( + {"poster": ""} + ) # Clear the poster value for intros / trailers to prevent issues in skins - listitem.setArt({ - 'icon': 'DefaultVideo.png', - 'thumb': obj['Artwork']['Primary'], - }) + listitem.setArt( + { + "icon": "DefaultVideo.png", + "thumb": obj["Artwork"]["Primary"], + } + ) - if obj['Premiere']: - obj['Premiere'] = obj['Premiere'].split('T')[0] + if obj["Premiere"]: + obj["Premiere"] = obj["Premiere"].split("T")[0] - if obj['DatePlayed']: - obj['DatePlayed'] = obj['DatePlayed'].split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = obj["DatePlayed"].split(".")[0].replace("T", " ") metadata = { - 'title': obj['Title'], - 'originaltitle': obj['Title'], - 'sorttitle': obj['SortTitle'], - 'country': obj['Countries'], - 'genre': obj['Genres'], - 'year': obj['Year'], - 'rating': obj['Rating'], - 'playcount': obj['PlayCount'], - 'overlay': obj['Overlay'], - 'director': obj['Directors'], - 'mpaa': obj['Mpaa'], - 'plot': obj['Plot'], - 'plotoutline': obj['ShortPlot'], - 'studio': obj['Studios'], - 'tagline': obj['Tagline'], - 'writer': obj['Writers'], - 'premiered': obj['Premiere'], - 'votes': obj['Votes'], - 'dateadded': obj['DateAdded'], - 'aired': obj['Year'], - 'date': obj['FileDate'], - 'dbid': obj['DbId'] + "title": obj["Title"], + "originaltitle": obj["Title"], + "sorttitle": obj["SortTitle"], + "country": obj["Countries"], + "genre": obj["Genres"], + "year": obj["Year"], + "rating": obj["Rating"], + "playcount": obj["PlayCount"], + "overlay": obj["Overlay"], + "director": obj["Directors"], + "mpaa": obj["Mpaa"], + "plot": obj["Plot"], + "plotoutline": obj["ShortPlot"], + "studio": obj["Studios"], + "tagline": obj["Tagline"], + "writer": obj["Writers"], + "premiered": obj["Premiere"], + "votes": obj["Votes"], + "dateadded": obj["DateAdded"], + "aired": obj["Year"], + "date": obj["FileDate"], + "dbid": obj["DbId"], } listitem.setCast(API.get_actors()) - if obj['Premiere']: - metadata['date'] = obj['Premiere'] + if obj["Premiere"]: + metadata["date"] = obj["Premiere"] - if obj['Type'] == 'Episode': - metadata.update({ - 'mediatype': "episode", - 'tvshowtitle': obj['SeriesName'], - 'season': obj['Season'] or 0, - 'sortseason': obj['Season'] or 0, - 'episode': obj['Index'] or 0, - 'sortepisode': obj['Index'] or 0, - 'lastplayed': obj['DatePlayed'], - 'duration': obj['Runtime'], - 'aired': obj['Premiere'], - }) + if obj["Type"] == "Episode": + metadata.update( + { + "mediatype": "episode", + "tvshowtitle": obj["SeriesName"], + "season": obj["Season"] or 0, + "sortseason": obj["Season"] or 0, + "episode": obj["Index"] or 0, + "sortepisode": obj["Index"] or 0, + "lastplayed": obj["DatePlayed"], + "duration": obj["Runtime"], + "aired": obj["Premiere"], + } + ) - elif obj['Type'] == 'Season': - metadata.update({ - 'mediatype': "season", - 'tvshowtitle': obj['SeriesName'], - 'season': obj['Index'] or 0, - 'sortseason': obj['Index'] or 0 - }) - listitem.setProperty('NumEpisodes', str(obj['RecursiveCount'])) - listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched'])) - listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched'])) - listitem.setProperty('IsFolder', 'true') + elif obj["Type"] == "Season": + metadata.update( + { + "mediatype": "season", + "tvshowtitle": obj["SeriesName"], + "season": obj["Index"] or 0, + "sortseason": obj["Index"] or 0, + } + ) + listitem.setProperty("NumEpisodes", str(obj["RecursiveCount"])) + listitem.setProperty( + "WatchedEpisodes", str(obj["RecursiveCount"] - obj["Unwatched"]) + ) + listitem.setProperty("UnWatchedEpisodes", str(obj["Unwatched"])) + listitem.setProperty("IsFolder", "true") - elif obj['Type'] == 'Series': + elif obj["Type"] == "Series": - if obj['Status'] != 'Ended': - obj['Status'] = None + if obj["Status"] != "Ended": + obj["Status"] = None - metadata.update({ - 'mediatype': "tvshow", - 'tvshowtitle': obj['Title'], - 'status': obj['Status'] - }) - listitem.setProperty('TotalSeasons', str(obj['ChildCount'])) - listitem.setProperty('TotalEpisodes', str(obj['RecursiveCount'])) - listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched'])) - listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched'])) - listitem.setProperty('IsFolder', 'true') + metadata.update( + { + "mediatype": "tvshow", + "tvshowtitle": obj["Title"], + "status": obj["Status"], + } + ) + listitem.setProperty("TotalSeasons", str(obj["ChildCount"])) + listitem.setProperty("TotalEpisodes", str(obj["RecursiveCount"])) + listitem.setProperty( + "WatchedEpisodes", str(obj["RecursiveCount"] - obj["Unwatched"]) + ) + listitem.setProperty("UnWatchedEpisodes", str(obj["Unwatched"])) + listitem.setProperty("IsFolder", "true") - elif obj['Type'] == 'Movie': - metadata.update({ - 'mediatype': "movie", - 'imdbnumber': obj['UniqueId'], - 'lastplayed': obj['DatePlayed'], - 'duration': obj['Runtime'], - }) + elif obj["Type"] == "Movie": + metadata.update( + { + "mediatype": "movie", + "imdbnumber": obj["UniqueId"], + "lastplayed": obj["DatePlayed"], + "duration": obj["Runtime"], + } + ) - elif obj['Type'] == 'MusicVideo': - metadata.update({ - 'mediatype': "musicvideo", - 'album': obj['Album'], - 'artist': obj['Artists'] or [], - 'lastplayed': obj['DatePlayed'], - 'duration': obj['Runtime'] - }) + elif obj["Type"] == "MusicVideo": + metadata.update( + { + "mediatype": "musicvideo", + "album": obj["Album"], + "artist": obj["Artists"] or [], + "lastplayed": obj["DatePlayed"], + "duration": obj["Runtime"], + } + ) - elif obj['Type'] == 'BoxSet': - metadata['mediatype'] = "set" - listitem.setProperty('IsFolder', 'true') + elif obj["Type"] == "BoxSet": + metadata["mediatype"] = "set" + listitem.setProperty("IsFolder", "true") else: - metadata.update({ - 'mediatype': "video", - 'lastplayed': obj['DatePlayed'], - 'year': obj['Year'], - 'duration': obj['Runtime'] - }) + metadata.update( + { + "mediatype": "video", + "lastplayed": obj["DatePlayed"], + "year": obj["Year"], + "duration": obj["Runtime"], + } + ) if is_video: - listitem.setProperty('totaltime', str(obj['Runtime'])) - listitem.setProperty('IsPlayable', 'true') - listitem.setProperty('IsFolder', 'false') + listitem.setProperty("totaltime", str(obj["Runtime"])) + listitem.setProperty("IsPlayable", "true") + listitem.setProperty("IsFolder", "false") - if obj['Resume'] and item.get("resumePlayback"): - listitem.setProperty('resumetime', str(obj['Resume'])) - listitem.setProperty('StartPercent', str(((obj['Resume'] / obj['Runtime']) * 100) - 0.40)) + if obj["Resume"] and item.get("resumePlayback"): + listitem.setProperty("resumetime", str(obj["Resume"])) + listitem.setProperty( + "StartPercent", str(((obj["Resume"] / obj["Runtime"]) * 100) - 0.40) + ) else: - listitem.setProperty('resumetime', '0') - listitem.setProperty('StartPercent', '0') + listitem.setProperty("resumetime", "0") + listitem.setProperty("StartPercent", "0") - for track in obj['Streams']['video']: - listitem.addStreamInfo('video', { - 'hdrtype': track['hdrtype'], - 'duration': obj['Runtime'], - 'aspect': track['aspect'], - 'codec': track['codec'], - 'width': track['width'], - 'height': track['height'] - }) + for track in obj["Streams"]["video"]: + listitem.addStreamInfo( + "video", + { + "hdrtype": track["hdrtype"], + "duration": obj["Runtime"], + "aspect": track["aspect"], + "codec": track["codec"], + "width": track["width"], + "height": track["height"], + }, + ) - for track in obj['Streams']['audio']: - listitem.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']}) + for track in obj["Streams"]["audio"]: + listitem.addStreamInfo( + "audio", {"codec": track["codec"], "channels": track["channels"]} + ) - for track in obj['Streams']['subtitle']: - listitem.addStreamInfo('subtitle', {'language': track}) + for track in obj["Streams"]["subtitle"]: + listitem.addStreamInfo("subtitle", {"language": track}) - listitem.setLabel(obj['Title']) - listitem.setInfo('video', metadata) + listitem.setLabel(obj["Title"]) + listitem.setInfo("video", metadata) listitem.setContentLookup(False) def listitem_channel(self, obj, listitem, item): - - ''' Set listitem for channel content. - ''' + """Set listitem for channel content.""" API = api.API(item, self.server) - obj['Title'] = "%s - %s" % (obj['Title'], obj['ProgramName']) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 - obj['Overlay'] = 7 if obj['Played'] else 6 - obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \ + obj["Title"] = "%s - %s" % (obj["Title"], obj["ProgramName"]) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) or 0 + obj["Overlay"] = 7 if obj["Played"] else 6 + obj["Artwork"]["Primary"] = ( + obj["Artwork"]["Primary"] or "special://home/addons/plugin.video.jellyfin/resources/icon.png" - obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] \ + ) + obj["Artwork"]["Thumb"] = ( + obj["Artwork"]["Thumb"] or "special://home/addons/plugin.video.jellyfin/resources/fanart.png" - obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] \ - or ["special://home/addons/plugin.video.jellyfin/resources/fanart.png"] + ) + obj["Artwork"]["Backdrop"] = obj["Artwork"]["Backdrop"] or [ + "special://home/addons/plugin.video.jellyfin/resources/fanart.png" + ] metadata = { - 'title': obj['Title'], - 'originaltitle': obj['Title'], - 'playcount': obj['PlayCount'], - 'overlay': obj['Overlay'] + "title": obj["Title"], + "originaltitle": obj["Title"], + "playcount": obj["PlayCount"], + "overlay": obj["Overlay"], } - listitem.setArt({ - 'icon': obj['Artwork']['Thumb'], - 'thumb': obj['Artwork']['Primary'], - }) - self.set_artwork(obj['Artwork'], listitem, obj['Type']) + listitem.setArt( + { + "icon": obj["Artwork"]["Thumb"], + "thumb": obj["Artwork"]["Primary"], + } + ) + self.set_artwork(obj["Artwork"], listitem, obj["Type"]) - if obj['Artwork']['Primary']: - listitem.setArt({ - 'thumb': obj['Artwork']['Primary'], - }) + if obj["Artwork"]["Primary"]: + listitem.setArt( + { + "thumb": obj["Artwork"]["Primary"], + } + ) - if not obj['Artwork']['Backdrop']: - listitem.setArt({'fanart': obj['Artwork']['Primary']}) + if not obj["Artwork"]["Backdrop"]: + listitem.setArt({"fanart": obj["Artwork"]["Primary"]}) - listitem.setProperty('totaltime', str(obj['Runtime'])) - listitem.setProperty('IsPlayable', 'true') - listitem.setProperty('IsFolder', 'false') + listitem.setProperty("totaltime", str(obj["Runtime"])) + listitem.setProperty("IsPlayable", "true") + listitem.setProperty("IsFolder", "false") - listitem.setLabel(obj['Title']) - listitem.setInfo('video', metadata) + listitem.setLabel(obj["Title"]) + listitem.setInfo("video", metadata) listitem.setContentLookup(False) def listitem_music(self, obj, listitem, item): API = api.API(item, self.server) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 - obj['Rating'] = obj['Rating'] or 0 + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) or 0 + obj["Rating"] = obj["Rating"] or 0 - if not obj['Played']: - obj['DatePlayed'] = None - elif obj['FileDate'] or obj['DatePlayed']: - obj['DatePlayed'] = (obj['DatePlayed'] or obj['FileDate']).split('.')[0].replace('T', " ") + if not obj["Played"]: + obj["DatePlayed"] = None + elif obj["FileDate"] or obj["DatePlayed"]: + obj["DatePlayed"] = ( + (obj["DatePlayed"] or obj["FileDate"]).split(".")[0].replace("T", " ") + ) - obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-'))) + obj["FileDate"] = "%s.%s.%s" % tuple( + reversed(obj["FileDate"].split("T")[0].split("-")) + ) metadata = { - 'title': obj['Title'], - 'genre': obj['Genre'], - 'year': obj['Year'], - 'album': obj['Album'], - 'artist': obj['Artists'], - 'rating': obj['Rating'], - 'comment': obj['Comment'], - 'date': obj['FileDate'] + "title": obj["Title"], + "genre": obj["Genre"], + "year": obj["Year"], + "album": obj["Album"], + "artist": obj["Artists"], + "rating": obj["Rating"], + "comment": obj["Comment"], + "date": obj["FileDate"], } - self.set_artwork(obj['Artwork'], listitem, obj['Type']) + self.set_artwork(obj["Artwork"], listitem, obj["Type"]) - if obj['Type'] == 'Audio': - metadata.update({ - 'mediatype': "song", - 'tracknumber': obj['Index'], - 'discnumber': obj['Disc'], - 'duration': obj['Runtime'], - 'playcount': obj['PlayCount'], - 'lastplayed': obj['DatePlayed'], - 'musicbrainztrackid': obj['UniqueId'] - }) - listitem.setProperty('IsPlayable', 'true') - listitem.setProperty('IsFolder', 'false') + if obj["Type"] == "Audio": + metadata.update( + { + "mediatype": "song", + "tracknumber": obj["Index"], + "discnumber": obj["Disc"], + "duration": obj["Runtime"], + "playcount": obj["PlayCount"], + "lastplayed": obj["DatePlayed"], + "musicbrainztrackid": obj["UniqueId"], + } + ) + listitem.setProperty("IsPlayable", "true") + listitem.setProperty("IsFolder", "false") - elif obj['Type'] == 'Album': - metadata.update({ - 'mediatype': "album", - 'musicbrainzalbumid': obj['UniqueId'] - }) + elif obj["Type"] == "Album": + metadata.update( + {"mediatype": "album", "musicbrainzalbumid": obj["UniqueId"]} + ) - elif obj['Type'] in ('Artist', 'MusicArtist'): - metadata.update({ - 'mediatype': "artist", - 'musicbrainzartistid': obj['UniqueId'] - }) + elif obj["Type"] in ("Artist", "MusicArtist"): + metadata.update( + {"mediatype": "artist", "musicbrainzartistid": obj["UniqueId"]} + ) else: - metadata['mediatype'] = "music" + metadata["mediatype"] = "music" - listitem.setLabel(obj['Title']) - listitem.setInfo('music', metadata) + listitem.setLabel(obj["Title"]) + listitem.setInfo("music", metadata) listitem.setContentLookup(False) def listitem_photo(self, obj, listitem, item): API = api.API(item, self.server) - obj['Overview'] = API.get_overview(obj['Overview']) - obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-'))) + obj["Overview"] = API.get_overview(obj["Overview"]) + obj["FileDate"] = "%s.%s.%s" % tuple( + reversed(obj["FileDate"].split("T")[0].split("-")) + ) - metadata = { - 'title': obj['Title'] - } - listitem.setProperty('path', obj['Artwork']['Primary']) - listitem.setArt({ - 'thumb': obj['Artwork']['Primary'], - }) + metadata = {"title": obj["Title"]} + listitem.setProperty("path", obj["Artwork"]["Primary"]) + listitem.setArt( + { + "thumb": obj["Artwork"]["Primary"], + } + ) - if obj['Type'] == 'Photo': - metadata.update({ - 'picturepath': obj['Artwork']['Primary'], - 'date': obj['FileDate'], - 'exif:width': str(obj.get('Width', 0)), - 'exif:height': str(obj.get('Height', 0)), - 'size': obj['Size'], - 'exif:cameramake': obj['CameraMake'], - 'exif:cameramodel': obj['CameraModel'], - 'exif:exposuretime': str(obj['ExposureTime']), - 'exif:focallength': str(obj['FocalLength']) - }) - listitem.setProperty('plot', obj['Overview']) - listitem.setProperty('IsFolder', 'false') - listitem.setArt({ - 'icon': 'DefaultPicture.png', - }) + if obj["Type"] == "Photo": + metadata.update( + { + "picturepath": obj["Artwork"]["Primary"], + "date": obj["FileDate"], + "exif:width": str(obj.get("Width", 0)), + "exif:height": str(obj.get("Height", 0)), + "size": obj["Size"], + "exif:cameramake": obj["CameraMake"], + "exif:cameramodel": obj["CameraModel"], + "exif:exposuretime": str(obj["ExposureTime"]), + "exif:focallength": str(obj["FocalLength"]), + } + ) + listitem.setProperty("plot", obj["Overview"]) + listitem.setProperty("IsFolder", "false") + listitem.setArt( + { + "icon": "DefaultPicture.png", + } + ) else: - listitem.setProperty('IsFolder', 'true') - listitem.setArt({ - 'icon': 'DefaultFolder.png', - }) + listitem.setProperty("IsFolder", "true") + listitem.setArt( + { + "icon": "DefaultFolder.png", + } + ) - listitem.setProperty('IsPlayable', 'false') - listitem.setLabel(obj['Title']) - listitem.setInfo('pictures', metadata) + listitem.setProperty("IsPlayable", "false") + listitem.setLabel(obj["Title"]) + listitem.setInfo("pictures", metadata) listitem.setContentLookup(False) def set_artwork(self, artwork, listitem, media): - if media == 'Episode': + if media == "Episode": art = { - 'poster': "Series.Primary", - 'tvshow.poster': "Series.Primary", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb", - 'tvshow.landscape': "Thumb", - 'thumb': "Primary", - 'fanart': "Backdrop" + "poster": "Series.Primary", + "tvshow.poster": "Series.Primary", + "clearart": "Art", + "tvshow.clearart": "Art", + "clearlogo": "Logo", + "tvshow.clearlogo": "Logo", + "discart": "Disc", + "fanart_image": "Backdrop", + "landscape": "Thumb", + "tvshow.landscape": "Thumb", + "thumb": "Primary", + "fanart": "Backdrop", } - elif media in ('Artist', 'Audio', 'MusicAlbum'): + elif media in ("Artist", "Audio", "MusicAlbum"): art = { - 'clearlogo': "Logo", - 'discart': "Disc", - 'fanart': "Backdrop", - 'fanart_image': "Backdrop", # in case - 'thumb': "Primary" + "clearlogo": "Logo", + "discart": "Disc", + "fanart": "Backdrop", + "fanart_image": "Backdrop", # in case + "thumb": "Primary", } else: art = { - 'poster': "Primary", - 'clearart': "Art", - 'clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb", - 'thumb': "Primary", - 'fanart': "Backdrop" + "poster": "Primary", + "clearart": "Art", + "clearlogo": "Logo", + "discart": "Disc", + "fanart_image": "Backdrop", + "landscape": "Thumb", + "thumb": "Primary", + "fanart": "Backdrop", } for k_art, e_art in art.items(): if e_art == "Backdrop": - self._set_art(listitem, k_art, artwork[e_art][0] if artwork[e_art] else " ") + self._set_art( + listitem, k_art, artwork[e_art][0] if artwork[e_art] else " " + ) else: self._set_art(listitem, k_art, artwork.get(e_art, " ")) def _set_art(self, listitem, art, path): LOG.debug(" [ art/%s ] %s", art, path) - if art in ('fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators', 'discart', - 'tvshow.poster'): + if art in ( + "fanart_image", + "small_poster", + "tiny_poster", + "medium_landscape", + "medium_poster", + "small_fanartimage", + "medium_fanartimage", + "fanart_noindicators", + "discart", + "tvshow.poster", + ): listitem.setProperty(art, path) else: listitem.setArt({art: path}) def resume_dialog(self, seektime): - - ''' Base resume dialog based on Kodi settings. - ''' + """Base resume dialog based on Kodi settings.""" LOG.info("Resume dialog called.") - XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i") + XML_PATH = ( + xbmcaddon.Addon("plugin.video.jellyfin").getAddonInfo("path"), + "default", + "1080i", + ) dialog = resume.ResumeDialog("script-jellyfin-resume.xml", *XML_PATH) - dialog.set_resume_point("Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0]) + dialog.set_resume_point( + "Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0] + ) dialog.doModal() if dialog.is_selected(): @@ -711,13 +808,11 @@ class PlaylistWorker(threading.Thread): def on_update(data, server): - - ''' Only for manually marking as watched/unwatched - ''' + """Only for manually marking as watched/unwatched""" try: - kodi_id = data['item']['id'] - media = data['item']['type'] - playcount = int(data['playcount']) + kodi_id = data["item"]["id"] + media = data["item"]["type"] + playcount = int(data["playcount"]) LOG.info(" [ update/%s ] kodi_id: %s media: %s", playcount, kodi_id, media) except (KeyError, TypeError): LOG.debug("Invalid playstate update") @@ -725,20 +820,19 @@ def on_update(data, server): return from .. import database + item = database.get_item(kodi_id, media) if item: - if not window('jellyfin.skip.%s.bool' % item[0]): + if not window("jellyfin.skip.%s.bool" % item[0]): server.jellyfin.item_played(item[0], playcount) - window('jellyfin.skip.%s' % item[0], clear=True) + window("jellyfin.skip.%s" % item[0], clear=True) def on_play(data, server): - - ''' Setup progress for jellyfin playback. - ''' + """Setup progress for jellyfin playback.""" player = xbmc.Player() try: @@ -746,19 +840,25 @@ def on_play(data, server): if player.isPlayingVideo(): - ''' Seems to misbehave when playback is not terminated prior to playing new content. - The kodi id remains that of the previous title. Maybe onPlay happens before - this information is updated. Added a failsafe further below. - ''' + """Seems to misbehave when playback is not terminated prior to playing new content. + The kodi id remains that of the previous title. Maybe onPlay happens before + this information is updated. Added a failsafe further below. + """ item = player.getVideoInfoTag() kodi_id = item.getDbId() media = item.getMediaType() - if kodi_id is None or int(kodi_id) == -1 or 'item' in data and 'id' in data['item'] and data['item']['id'] != kodi_id: + if ( + kodi_id is None + or int(kodi_id) == -1 + or "item" in data + and "id" in data["item"] + and data["item"]["id"] != kodi_id + ): - item = data['item'] - kodi_id = item['id'] - media = item['type'] + item = data["item"] + kodi_id = item["id"] + media = item["type"] LOG.info(" [ play ] kodi_id: %s media: %s", kodi_id, media) @@ -767,8 +867,9 @@ def on_play(data, server): return - if settings('useDirectPaths') == '1' or media == 'song': + if settings("useDirectPaths") == "1" or media == "song": from .. import database + item = database.get_item(kodi_id, media) if item: @@ -781,32 +882,34 @@ def on_play(data, server): return item = server.jellyfin.get_item(item[0]) - item['PlaybackInfo'] = {'Path': file} - playutils.set_properties(item, 'DirectStream' if settings('useDirectPaths') == '0' else 'DirectPlay') + item["PlaybackInfo"] = {"Path": file} + playutils.set_properties( + item, + "DirectStream" if settings("useDirectPaths") == "0" else "DirectPlay", + ) def special_listener(): - - ''' Corner cases that needs to be listened to. - This is run in a loop within monitor.py - ''' + """Corner cases that needs to be listened to. + This is run in a loop within monitor.py + """ player = xbmc.Player() is_playing = player.isPlaying() - count = int(window('jellyfin.external_count') or 0) + count = int(window("jellyfin.external_count") or 0) - if is_playing and not window('jellyfin.external_check'): + if is_playing and not window("jellyfin.external_check"): time = player.getTime() if time > 1: # Not external player. - window('jellyfin.external_check', value="true") - window('jellyfin.external_count', value="0") + window("jellyfin.external_check", value="true") + window("jellyfin.external_count", value="0") elif count == 120: LOG.info("External player detected.") - window('jellyfin.external.bool', True) - window('jellyfin.external_check.bool', True) - window('jellyfin.external_count', value="0") + window("jellyfin.external.bool", True) + window("jellyfin.external_check.bool", True) + window("jellyfin.external_count", value="0") elif time == 0: - window('jellyfin.external_count', value=str(count + 1)) + window("jellyfin.external_count", value=str(count + 1)) diff --git a/jellyfin_kodi/objects/kodi/artwork.py b/jellyfin_kodi/objects/kodi/artwork.py index a37320a8..7162c050 100644 --- a/jellyfin_kodi/objects/kodi/artwork.py +++ b/jellyfin_kodi/objects/kodi/artwork.py @@ -21,15 +21,21 @@ class Artwork(object): self.cursor = cursor def update(self, image_url, kodi_id, media, image): - - ''' Update artwork in the video database. - Delete current entry before updating with the new one. - ''' - if not image_url or image == 'poster' and media in ('song', 'artist', 'album'): + """Update artwork in the video database. + Delete current entry before updating with the new one. + """ + if not image_url or image == "poster" and media in ("song", "artist", "album"): return try: - self.cursor.execute(QU.get_art, (kodi_id, media, image,)) + self.cursor.execute( + QU.get_art, + ( + kodi_id, + media, + image, + ), + ) url = self.cursor.fetchone()[0] except TypeError: @@ -41,43 +47,39 @@ class Artwork(object): self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image)) def add(self, artwork, *args): - - ''' Add all artworks. - ''' + """Add all artworks.""" KODI = { - 'Primary': ['thumb', 'poster'], - 'Banner': "banner", - 'Logo': "clearlogo", - 'Art': "clearart", - 'Thumb': "landscape", - 'Disc': "discart", - 'Backdrop': "fanart" + "Primary": ["thumb", "poster"], + "Banner": "banner", + "Logo": "clearlogo", + "Art": "clearart", + "Thumb": "landscape", + "Disc": "discart", + "Backdrop": "fanart", } for art in KODI: - if art == 'Backdrop': + if art == "Backdrop": self.cursor.execute(QU.get_backdrops, args + ("fanart%",)) - if len(self.cursor.fetchall()) > len(artwork['Backdrop']): + if len(self.cursor.fetchall()) > len(artwork["Backdrop"]): self.cursor.execute(QU.delete_backdrops, args + ("fanart_",)) - for index, backdrop in enumerate(artwork['Backdrop']): + for index, backdrop in enumerate(artwork["Backdrop"]): if index: self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),)) else: self.update(*(backdrop,) + args + ("fanart",)) - elif art == 'Primary': - for kodi_image in KODI['Primary']: - self.update(*(artwork['Primary'],) + args + (kodi_image,)) + elif art == "Primary": + for kodi_image in KODI["Primary"]: + self.update(*(artwork["Primary"],) + args + (kodi_image,)) elif artwork.get(art): self.update(*(artwork[art],) + args + (KODI[art],)) def delete(self, *args): - - ''' Delete artwork from kodi database - ''' + """Delete artwork from kodi database""" self.cursor.execute(QU.delete_art, args) diff --git a/jellyfin_kodi/objects/kodi/kodi.py b/jellyfin_kodi/objects/kodi/kodi.py index 74b93e33..2edb53b8 100644 --- a/jellyfin_kodi/objects/kodi/kodi.py +++ b/jellyfin_kodi/objects/kodi/kodi.py @@ -89,7 +89,13 @@ class Kodi(object): def add_file(self, filename, path_id): try: - self.cursor.execute(QU.get_file, (filename, path_id,)) + self.cursor.execute( + QU.get_file, + ( + filename, + path_id, + ), + ) file_id = self.cursor.fetchone()[0] except TypeError: @@ -120,40 +126,49 @@ class Kodi(object): def add_thumbnail(person_id, person, person_type): - if person['imageurl']: + if person["imageurl"]: art = person_type.lower() if "writing" in art: art = "writer" - self.artwork.update(person['imageurl'], person_id, art, "thumb") + self.artwork.update(person["imageurl"], person_id, art, "thumb") cast_order = 1 bulk_updates = {} for person in people: - person_id = self.get_person(person['Name']) + person_id = self.get_person(person["Name"]) - if person['Type'] == 'Actor': + if person["Type"] == "Actor": sql = QU.update_actor - role = person.get('Role') - bulk_updates.setdefault(sql, []).append((person_id,) + args + (role, cast_order,)) + role = person.get("Role") + bulk_updates.setdefault(sql, []).append( + (person_id,) + + args + + ( + role, + cast_order, + ) + ) cast_order += 1 - elif person['Type'] == 'Director': - sql = QU.update_link.replace("{LinkType}", 'director_link') + elif person["Type"] == "Director": + sql = QU.update_link.replace("{LinkType}", "director_link") bulk_updates.setdefault(sql, []).append((person_id,) + args) - elif person['Type'] == 'Writer': - sql = QU.update_link.replace("{LinkType}", 'writer_link') + elif person["Type"] == "Writer": + sql = QU.update_link.replace("{LinkType}", "writer_link") bulk_updates.setdefault(sql, []).append((person_id,) + args) - elif person['Type'] == 'Artist': - sql = QU.insert_link_if_not_exists.replace("{LinkType}", 'actor_link') - bulk_updates.setdefault(sql, []).append((person_id,) + args + (person_id,) + args) + elif person["Type"] == "Artist": + sql = QU.insert_link_if_not_exists.replace("{LinkType}", "actor_link") + bulk_updates.setdefault(sql, []).append( + (person_id,) + args + (person_id,) + args + ) - add_thumbnail(person_id, person, person['Type']) + add_thumbnail(person_id, person, person["Type"]) for sql, parameters in bulk_updates.items(): self.cursor.executemany(sql, parameters) @@ -163,8 +178,7 @@ class Kodi(object): return self.cursor.lastrowid def _get_person(self, name): - '''Retrieve person from the database, or add them if they don't exist - ''' + """Retrieve person from the database, or add them if they don't exist""" resp = self.cursor.execute(QU.get_person, (name,)).fetchone() if resp is not None: return resp[0] @@ -172,8 +186,7 @@ class Kodi(object): return self.add_person(name) def get_person(self, name): - '''Retrieve person from cache, else forward to db query - ''' + """Retrieve person from cache, else forward to db query""" if name in self._people_cache: return self._people_cache[name] else: @@ -182,9 +195,7 @@ class Kodi(object): return person_id def add_genres(self, genres, *args): - - ''' Delete current genres first for clean slate. - ''' + """Delete current genres first for clean slate.""" self.cursor.execute(QU.delete_genres, args) for genre in genres: @@ -230,29 +241,32 @@ class Kodi(object): return self.add_studio(*args) def add_streams(self, file_id, streams, runtime): - - ''' First remove any existing entries - Then re-add video, audio and subtitles. - ''' + """First remove any existing entries + Then re-add video, audio and subtitles. + """ self.cursor.execute(QU.delete_streams, (file_id,)) if streams: - for track in streams['video']: + for track in streams["video"]: - track['FileId'] = file_id - track['Runtime'] = runtime + track["FileId"] = file_id + track["Runtime"] = runtime if kodi_version() < 20: self.add_stream_video(*values(track, QU.add_stream_video_obj_19)) else: self.add_stream_video(*values(track, QU.add_stream_video_obj)) - for track in streams['audio']: + for track in streams["audio"]: - track['FileId'] = file_id + track["FileId"] = file_id self.add_stream_audio(*values(track, QU.add_stream_audio_obj)) - for track in streams['subtitle']: - self.add_stream_sub(*values({'language': track, 'FileId': file_id}, QU.add_stream_sub_obj)) + for track in streams["subtitle"]: + self.add_stream_sub( + *values( + {"language": track, "FileId": file_id}, QU.add_stream_sub_obj + ) + ) def add_stream_video(self, *args): if kodi_version() < 20: @@ -267,17 +281,24 @@ class Kodi(object): self.cursor.execute(QU.add_stream_sub, args) def add_playstate(self, file_id, playcount, date_played, resume, *args): - - ''' Delete the existing resume point. - Set the watched count. - ''' + """Delete the existing resume point. + Set the watched count. + """ self.cursor.execute(QU.delete_bookmark, (file_id,)) self.set_playcount(playcount, date_played, file_id) if resume: bookmark_id = self.create_entry_bookmark() - self.cursor.execute(QU.add_bookmark, (bookmark_id, file_id, resume,) + args) + self.cursor.execute( + QU.add_bookmark, + ( + bookmark_id, + file_id, + resume, + ) + + args, + ) def set_playcount(self, *args): self.cursor.execute(QU.update_playcount, args) diff --git a/jellyfin_kodi/objects/kodi/movies.py b/jellyfin_kodi/objects/kodi/movies.py index 10172c02..14fba843 100644 --- a/jellyfin_kodi/objects/kodi/movies.py +++ b/jellyfin_kodi/objects/kodi/movies.py @@ -74,15 +74,11 @@ class Movies(Kodi): return None def add_ratings(self, *args): - - ''' Add ratings, rating type and votes. - ''' + """Add ratings, rating type and votes.""" self.cursor.execute(QU.add_rating, args) def update_ratings(self, *args): - - ''' Update rating by rating_id. - ''' + """Update rating by rating_id.""" self.cursor.execute(QU.update_rating, args) def get_unique_id(self, *args): @@ -95,15 +91,11 @@ class Movies(Kodi): return def add_unique_id(self, *args): - - ''' Add the provider id, imdb, tvdb. - ''' + """Add the provider id, imdb, tvdb.""" self.cursor.execute(QU.add_unique_id, args) def update_unique_id(self, *args): - - ''' Update the provider id, imdb, tvdb. - ''' + """Update the provider id, imdb, tvdb.""" self.cursor.execute(QU.update_unique_id, args) def add_countries(self, countries, *args): @@ -141,9 +133,9 @@ class Movies(Kodi): self.cursor.execute(QU.delete_set, args) def migrations(self): - ''' + """ Used to trigger required database migrations for new versions - ''' + """ self.cursor.execute(QU.get_version) version_id = self.cursor.fetchone()[0] changes = False @@ -156,10 +148,10 @@ class Movies(Kodi): return changes def omega_migration(self): - ''' + """ Adds a video version for all existing movies - ''' - LOG.info('Starting migration for Omega database changes') + """ + LOG.info("Starting migration for Omega database changes") # Tracks if this migration made any changes changes = False self.cursor.execute(QU.get_missing_versions) @@ -169,5 +161,5 @@ class Movies(Kodi): self.add_videoversion(entry[0], entry[1], "movie", "0", 40400) changes = True - LOG.info('Omega database migration is complete') + LOG.info("Omega database migration is complete") return changes diff --git a/jellyfin_kodi/objects/kodi/music.py b/jellyfin_kodi/objects/kodi/music.py index 84d7a1a4..cad294a1 100644 --- a/jellyfin_kodi/objects/kodi/music.py +++ b/jellyfin_kodi/objects/kodi/music.py @@ -25,10 +25,9 @@ class Music(Kodi): Kodi.__init__(self) def create_entry(self): - - ''' Krypton has a dummy first entry - idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing - ''' + """Krypton has a dummy first entry + idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing + """ self.cursor.execute(QU.create_artist) return self.cursor.fetchone()[0] + 1 @@ -55,9 +54,7 @@ class Music(Kodi): self.cursor.execute(QU.update_role, args) def get(self, artist_id, name, musicbrainz): - - ''' Get artist or create the entry. - ''' + """Get artist or create the entry.""" try: self.cursor.execute(QU.get_artist, (musicbrainz,)) result = self.cursor.fetchone() @@ -72,15 +69,20 @@ class Music(Kodi): return artist_id_res def add_artist(self, artist_id, name, *args): - - ''' Safety check, when musicbrainz does not exist - ''' + """Safety check, when musicbrainz does not exist""" try: self.cursor.execute(QU.get_artist_by_name, (name,)) artist_id_res = self.cursor.fetchone()[0] except TypeError: artist_id_res = artist_id or self.create_entry() - self.cursor.execute(QU.add_artist, (artist_id, name,) + args) + self.cursor.execute( + QU.add_artist, + ( + artist_id, + name, + ) + + args, + ) return artist_id_res @@ -141,7 +143,7 @@ class Music(Kodi): self.cursor.execute(QU.get_album_by_name72, (name,)) album = self.cursor.fetchone() - if album[1] and album[1].split(' / ')[0] not in artists.split(' / '): + if album[1] and album[1].split(" / ")[0] not in artists.split(" / "): LOG.info("Album found, but artist doesn't match?") LOG.info("Album [ %s/%s ] %s", name, album[1], artists) @@ -149,7 +151,14 @@ class Music(Kodi): album_id = (album or self.cursor.fetchone())[0] except TypeError: - album_id = self.add_album(*(album_id, name, musicbrainz,) + args) + album_id = self.add_album( + *( + album_id, + name, + musicbrainz, + ) + + args + ) return album_id @@ -225,11 +234,10 @@ class Music(Kodi): self.cursor.execute(QU.update_song_rating, args) def add_genres(self, kodi_id, genres, media): - - ''' Add genres, but delete current genres first. - Album_genres was removed in kodi 18 - ''' - if media == 'album' and self.version_id < 72: + """Add genres, but delete current genres first. + Album_genres was removed in kodi 18 + """ + if media == "album" and self.version_id < 72: self.cursor.execute(QU.delete_genres_album, (kodi_id,)) for genre in genres: @@ -237,7 +245,7 @@ class Music(Kodi): genre_id = self.get_genre(genre) self.cursor.execute(QU.update_genre_album, (genre_id, kodi_id)) - if media == 'song': + if media == "song": self.cursor.execute(QU.delete_genres_song, (kodi_id,)) for genre in genres: diff --git a/jellyfin_kodi/objects/kodi/queries.py b/jellyfin_kodi/objects/kodi/queries.py index 5bca095d..d02ef832 100644 --- a/jellyfin_kodi/objects/kodi/queries.py +++ b/jellyfin_kodi/objects/kodi/queries.py @@ -1,9 +1,9 @@ from __future__ import division, absolute_import, print_function, unicode_literals -''' Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items. +""" Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items. Some functions require additional information, therefore obj do not always reflect the Kodi database query values. -''' +""" create_path = """ SELECT coalesce(max(idPath), 0) FROM path @@ -254,21 +254,48 @@ add_bookmark = """ INSERT INTO bookmark(idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) VALUES (?, ?, ?, ?, ?, ?) """ -add_bookmark_obj = ["{FileId}", "{PlayCount}", "{DatePlayed}", "{Resume}", "{Runtime}", "DVDPlayer", 1] +add_bookmark_obj = [ + "{FileId}", + "{PlayCount}", + "{DatePlayed}", + "{Resume}", + "{Runtime}", + "DVDPlayer", + 1, +] add_streams_obj = ["{FileId}", "{Streams}", "{Runtime}"] add_stream_video = """ INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, iVideoHeight, iVideoDuration, strStereoMode, strHdrType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_stream_video_obj = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}", "{hdrtype}"] +add_stream_video_obj = [ + "{FileId}", + 0, + "{codec}", + "{aspect}", + "{width}", + "{height}", + "{Runtime}", + "{3d}", + "{hdrtype}", +] # strHdrType is new to Kodi 20 add_stream_video_19 = """ INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, iVideoHeight, iVideoDuration, strStereoMode) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """ -add_stream_video_obj_19 = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}"] +add_stream_video_obj_19 = [ + "{FileId}", + 0, + "{codec}", + "{aspect}", + "{width}", + "{height}", + "{Runtime}", + "{3d}", +] add_stream_audio = """ INSERT INTO streamdetails(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) VALUES (?, ?, ?, ?, ?) @@ -295,24 +322,82 @@ INSERT INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_movie_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", - "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", - "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}", - "{Trailer}", "{Country}", "{Premiere}"] +add_movie_obj = [ + "{MovieId}", + "{FileId}", + "{Title}", + "{Plot}", + "{ShortPlot}", + "{Tagline}", + "{Votes}", + "{RatingId}", + "{Writers}", + "{Year}", + "{Unique}", + "{SortTitle}", + "{Runtime}", + "{Mpaa}", + "{Genre}", + "{Directors}", + "{Title}", + "{Studio}", + "{Trailer}", + "{Country}", + "{Premiere}", +] add_rating = """ INSERT INTO rating(rating_id, media_id, media_type, rating_type, rating, votes) VALUES (?, ?, ?, ?, ?, ?) """ -add_rating_movie_obj = ["{RatingId}", "{MovieId}", "movie", "default", "{Rating}", "{Votes}"] -add_rating_tvshow_obj = ["{RatingId}", "{ShowId}", "tvshow", "default", "{Rating}", "{Votes}"] -add_rating_episode_obj = ["{RatingId}", "{EpisodeId}", "episode", "default", "{Rating}", "{Votes}"] +add_rating_movie_obj = [ + "{RatingId}", + "{MovieId}", + "movie", + "default", + "{Rating}", + "{Votes}", +] +add_rating_tvshow_obj = [ + "{RatingId}", + "{ShowId}", + "tvshow", + "default", + "{Rating}", + "{Votes}", +] +add_rating_episode_obj = [ + "{RatingId}", + "{EpisodeId}", + "episode", + "default", + "{Rating}", + "{Votes}", +] add_unique_id = """ INSERT INTO uniqueid(uniqueid_id, media_id, media_type, value, type) VALUES (?, ?, ?, ?, ?) """ -add_unique_id_movie_obj = ["{Unique}", "{MovieId}", "movie", "{UniqueId}", "{ProviderName}"] -add_unique_id_tvshow_obj = ["{Unique}", "{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}"] -add_unique_id_episode_obj = ["{Unique}", "{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}"] +add_unique_id_movie_obj = [ + "{Unique}", + "{MovieId}", + "movie", + "{UniqueId}", + "{ProviderName}", +] +add_unique_id_tvshow_obj = [ + "{Unique}", + "{ShowId}", + "tvshow", + "{UniqueId}", + "{ProviderName}", +] +add_unique_id_episode_obj = [ + "{Unique}", + "{EpisodeId}", + "episode", + "{UniqueId}", + "{ProviderName}", +] add_country = """ INSERT INTO country(name) VALUES (?) @@ -335,14 +420,40 @@ INSERT INTO musicvideo(idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c11, c12, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_musicvideo_obj = ["{MvideoId}", "{FileId}", "{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", - "{Plot}", "{Album}", "{Artists}", "{Genre}", "{Index}", "{Premiere}"] +add_musicvideo_obj = [ + "{MvideoId}", + "{FileId}", + "{Title}", + "{Runtime}", + "{Directors}", + "{Studio}", + "{Year}", + "{Plot}", + "{Album}", + "{Artists}", + "{Genre}", + "{Index}", + "{Premiere}", +] add_tvshow = """ INSERT INTO tvshow(idShow, c00, c01, c02, c04, c05, c08, c09, c10, c12, c13, c14, c15) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_tvshow_obj = ["{ShowId}", "{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}", - "disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}"] +add_tvshow_obj = [ + "{ShowId}", + "{Title}", + "{Plot}", + "{Status}", + "{RatingId}", + "{Premiere}", + "{Genre}", + "{Title}", + "disintegrate browse bug", + "{Unique}", + "{Mpaa}", + "{Studio}", + "{SortTitle}", +] add_season = """ INSERT INTO seasons(idSeason, idShow, season) VALUES (?, ?, ?) @@ -352,9 +463,27 @@ INSERT INTO episode(idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c1 idShow, c15, c16, idSeason, c18, c19, c20) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_episode_obj = ["{EpisodeId}", "{FileId}", "{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", - "{Directors}", "{Season}", "{Index}", "{Title}", "{ShowId}", "{AirsBeforeSeason}", - "{AirsBeforeEpisode}", "{SeasonId}", "{FullFilePath}", "{PathId}", "{Unique}"] +add_episode_obj = [ + "{EpisodeId}", + "{FileId}", + "{Title}", + "{Plot}", + "{RatingId}", + "{Writers}", + "{Premiere}", + "{Runtime}", + "{Directors}", + "{Season}", + "{Index}", + "{Title}", + "{ShowId}", + "{AirsBeforeSeason}", + "{AirsBeforeEpisode}", + "{SeasonId}", + "{FullFilePath}", + "{PathId}", + "{Unique}", +] add_art = """ INSERT INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?) @@ -372,7 +501,13 @@ SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ? WHERE idPath = ? """ update_path_movie_obj = ["{Path}", "movies", "metadata.local", 1, "{PathId}"] -update_path_toptvshow_obj = ["{TopLevel}", "tvshows", "metadata.local", 1, "{TopPathId}"] +update_path_toptvshow_obj = [ + "{TopLevel}", + "tvshows", + "metadata.local", + 1, + "{TopPathId}", +] update_path_toptvshow_addon_obj = ["{TopLevel}", None, None, 1, "{TopPathId}"] update_path_tvshow_obj = ["{Path}", None, None, 1, "{PathId}"] update_path_episode_obj = ["{Path}", None, None, 1, "{PathId}"] @@ -431,26 +566,83 @@ SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?, c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ? WHERE idMovie = ? """ -update_movie_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", - "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", - "{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}", "{Trailer}", - "{Country}", "{Premiere}", "{MovieId}"] +update_movie_obj = [ + "{Title}", + "{Plot}", + "{ShortPlot}", + "{Tagline}", + "{Votes}", + "{RatingId}", + "{Writers}", + "{Year}", + "{Unique}", + "{SortTitle}", + "{Runtime}", + "{Mpaa}", + "{Genre}", + "{Directors}", + "{Title}", + "{Studio}", + "{Trailer}", + "{Country}", + "{Premiere}", + "{MovieId}", +] update_rating = """ UPDATE rating SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ? WHERE rating_id = ? """ -update_rating_movie_obj = ["{MovieId}", "movie", "default", "{Rating}", "{Votes}", "{RatingId}"] -update_rating_tvshow_obj = ["{ShowId}", "tvshow", "default", "{Rating}", "{Votes}", "{RatingId}"] -update_rating_episode_obj = ["{EpisodeId}", "episode", "default", "{Rating}", "{Votes}", "{RatingId}"] +update_rating_movie_obj = [ + "{MovieId}", + "movie", + "default", + "{Rating}", + "{Votes}", + "{RatingId}", +] +update_rating_tvshow_obj = [ + "{ShowId}", + "tvshow", + "default", + "{Rating}", + "{Votes}", + "{RatingId}", +] +update_rating_episode_obj = [ + "{EpisodeId}", + "episode", + "default", + "{Rating}", + "{Votes}", + "{RatingId}", +] update_unique_id = """ UPDATE uniqueid SET media_id = ?, media_type = ?, value = ?, type = ? WHERE uniqueid_id = ? """ -update_unique_id_movie_obj = ["{MovieId}", "movie", "{UniqueId}", "{ProviderName}", "{Unique}"] -update_unique_id_tvshow_obj = ["{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}", "{Unique}"] -update_unique_id_episode_obj = ["{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}", "{Unique}"] +update_unique_id_movie_obj = [ + "{MovieId}", + "movie", + "{UniqueId}", + "{ProviderName}", + "{Unique}", +] +update_unique_id_tvshow_obj = [ + "{ShowId}", + "tvshow", + "{UniqueId}", + "{ProviderName}", + "{Unique}", +] +update_unique_id_episode_obj = [ + "{EpisodeId}", + "episode", + "{UniqueId}", + "{ProviderName}", + "{Unique}", +] update_country = """ INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) VALUES (?, ?, ?) @@ -474,16 +666,41 @@ SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = c11 = ?, c12 = ?, premiered = ? WHERE idMVideo = ? """ -update_musicvideo_obj = ["{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", "{Plot}", "{Album}", - "{Artists}", "{Genre}", "{Index}", "{Premiere}", "{MvideoId}"] +update_musicvideo_obj = [ + "{Title}", + "{Runtime}", + "{Directors}", + "{Studio}", + "{Year}", + "{Plot}", + "{Album}", + "{Artists}", + "{Genre}", + "{Index}", + "{Premiere}", + "{MvideoId}", +] update_tvshow = """ UPDATE tvshow SET c00 = ?, c01 = ?, c02 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?, c10 = ?, c12 = ?, c13 = ?, c14 = ?, c15 = ? WHERE idShow = ? """ -update_tvshow_obj = ["{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}", - "disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}", "{ShowId}"] +update_tvshow_obj = [ + "{Title}", + "{Plot}", + "{Status}", + "{RatingId}", + "{Premiere}", + "{Genre}", + "{Title}", + "disintegrate browse bug", + "{Unique}", + "{Mpaa}", + "{Studio}", + "{SortTitle}", + "{ShowId}", +] update_tvshow_link = """ INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) VALUES (?, ?) @@ -501,9 +718,26 @@ SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?, c18 = ?, c19 = ?, c20 = ? WHERE idEpisode = ? """ -update_episode_obj = ["{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", "{Directors}", - "{Season}", "{Index}", "{Title}", "{AirsBeforeSeason}", "{AirsBeforeEpisode}", "{SeasonId}", - "{ShowId}", "{FullFilePath}", "{PathId}", "{Unique}", "{EpisodeId}"] +update_episode_obj = [ + "{Title}", + "{Plot}", + "{RatingId}", + "{Writers}", + "{Premiere}", + "{Runtime}", + "{Directors}", + "{Season}", + "{Index}", + "{Title}", + "{AirsBeforeSeason}", + "{AirsBeforeEpisode}", + "{SeasonId}", + "{ShowId}", + "{FullFilePath}", + "{PathId}", + "{Unique}", + "{EpisodeId}", +] delete_path = """ diff --git a/jellyfin_kodi/objects/kodi/queries_music.py b/jellyfin_kodi/objects/kodi/queries_music.py index 88131dc3..ca6c7fa4 100644 --- a/jellyfin_kodi/objects/kodi/queries_music.py +++ b/jellyfin_kodi/objects/kodi/queries_music.py @@ -54,7 +54,14 @@ FROM album WHERE strMusicBrainzAlbumID = ? """ get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album"] -get_album_obj82 = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album", "{DateAdded}"] +get_album_obj82 = [ + "{AlbumId}", + "{Title}", + "{UniqueId}", + "{Artists}", + "album", + "{DateAdded}", +] get_album_by_name = """ SELECT idAlbum, strArtists FROM album @@ -132,9 +139,24 @@ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle rating, comment, dateAdded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ -add_song_obj = ["{SongId}", "{AlbumId}", "{PathId}", "{Artists}", "{Genre}", "{Title}", "{Index}", - "{Runtime}", "{Year}", "{Filename}", "{UniqueId}", "{PlayCount}", "{DatePlayed}", "{Rating}", - "{Comment}", "{DateAdded}"] +add_song_obj = [ + "{SongId}", + "{AlbumId}", + "{PathId}", + "{Artists}", + "{Genre}", + "{Title}", + "{Index}", + "{Runtime}", + "{Year}", + "{Filename}", + "{UniqueId}", + "{PlayCount}", + "{DatePlayed}", + "{Rating}", + "{Comment}", + "{DateAdded}", +] add_genre = """ INSERT INTO genre(idGenre, strGenre) VALUES (?, ?) @@ -197,7 +219,17 @@ SET strArtistDisp = ?, strReleaseDate = ?, strGenres = ?, strReview = ?, iUserrating = ?, lastScraped = ?, bScrapedMBID = 1, strReleaseType = ? WHERE idAlbum = ? """ -update_album_obj = ["{Artists}", "{Year}", "{Genre}", "{Bio}", "{Thumb}", "{Rating}", "{LastScraped}", "album", "{AlbumId}"] +update_album_obj = [ + "{Artists}", + "{Year}", + "{Genre}", + "{Bio}", + "{Thumb}", + "{Rating}", + "{LastScraped}", + "album", + "{AlbumId}", +] update_album_artist = """ UPDATE album SET strArtists = ? @@ -229,9 +261,22 @@ SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack rating = ?, comment = ?, dateAdded = ? WHERE idSong = ? """ -update_song_obj = ["{AlbumId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}", - "{Filename}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}", - "{DateAdded}", "{SongId}"] +update_song_obj = [ + "{AlbumId}", + "{Artists}", + "{Genre}", + "{Title}", + "{Index}", + "{Runtime}", + "{Year}", + "{Filename}", + "{PlayCount}", + "{DatePlayed}", + "{Rating}", + "{Comment}", + "{DateAdded}", + "{SongId}", +] update_song_artist = """ INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) VALUES (?, ?, ?, ?, ?) diff --git a/jellyfin_kodi/objects/movies.py b/jellyfin_kodi/objects/movies.py index 0971c578..44927b69 100644 --- a/jellyfin_kodi/objects/movies.py +++ b/jellyfin_kodi/objects/movies.py @@ -8,7 +8,16 @@ from kodi_six.utils import py2_encode from .. import downloader as server from ..database import jellyfin_db, queries as QUEM -from ..helper import api, stop, validate, validate_bluray_dir, validate_dvd_dir, jellyfin_item, values, Local +from ..helper import ( + api, + stop, + validate, + validate_bluray_dir, + validate_dvd_dir, + jellyfin_item, + values, + Local, +) from ..helper import LazyLogger from ..helper.utils import find_library from ..helper.exceptions import PathValidationException @@ -42,74 +51,83 @@ class Movies(KodiDb): @stop @jellyfin_item def movie(self, item, e_item): - - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """If item does not exist, entry will be added. + If item exists, entry will be updated. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Movie') + obj = self.objects.map(item, "Movie") update = True try: - obj['MovieId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['PathId'] = e_item[2] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["MovieId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["PathId"] = e_item[2] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False - LOG.debug("MovieId %s not found", obj['Id']) + LOG.debug("MovieId %s not found", obj["Id"]) library = self.library or find_library(self.server, item) if not library: # This item doesn't belong to a whitelisted library return - obj['MovieId'] = self.create_entry() - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] + obj["MovieId"] = self.create_entry() + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] else: if self.get(*values(obj, QU.get_movie_obj)) is None: update = False - LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId']) + LOG.info( + "MovieId %s missing from kodi. repairing the entry.", obj["MovieId"] + ) - obj['Path'] = API.get_file_path(obj['Path']) - obj['Genres'] = obj['Genres'] or [] - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] - obj['People'] = obj['People'] or [] - obj['Genre'] = " / ".join(obj['Genres']) - obj['Writers'] = " / ".join(obj['Writers'] or []) - obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['People'] = API.get_people_artwork(obj['People']) - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - if obj['Premiere'] is not None: - obj['Premiere'] = str(obj['Premiere']).split('T')[0] + obj["Path"] = API.get_file_path(obj["Path"]) + obj["Genres"] = obj["Genres"] or [] + obj["Studios"] = [ + API.validate_studio(studio) for studio in (obj["Studios"] or []) + ] + obj["People"] = obj["People"] or [] + obj["Genre"] = " / ".join(obj["Genres"]) + obj["Writers"] = " / ".join(obj["Writers"] or []) + obj["Directors"] = " / ".join(obj["Directors"] or []) + obj["Plot"] = API.get_overview(obj["Plot"]) + obj["Mpaa"] = API.get_mpaa(obj["Mpaa"]) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["People"] = API.get_people_artwork(obj["People"]) + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + obj["DatePlayed"] = ( + None + if not obj["DatePlayed"] + else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) + obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"]) + obj["Audio"] = API.audio_streams(obj["Audio"] or []) + obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"]) + if obj["Premiere"] is not None: + obj["Premiere"] = str(obj["Premiere"]).split("T")[0] self.get_path_filename(obj) self.trailer(obj) - if obj['Countries']: + if obj["Countries"]: self.add_countries(*values(obj, QU.update_country_obj)) - tags = list(obj['Tags'] or []) - tags.append(obj['LibraryName']) + tags = list(obj["Tags"] or []) + tags.append(obj["LibraryName"]) - if obj['Favorite']: - tags.append('Favorite movies') + if obj["Favorite"]: + tags.append("Favorite movies") - obj['Tags'] = tags + obj["Tags"] = tags if update: self.movie_update(obj) @@ -124,239 +142,289 @@ class Movies(KodiDb): self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.add_people(*values(obj, QU.add_people_movie_obj)) self.add_streams(*values(obj, QU.add_streams_obj)) - self.artwork.add(obj['Artwork'], obj['MovieId'], "movie") - self.item_ids.append(obj['Id']) + self.artwork.add(obj["Artwork"], obj["MovieId"], "movie") + self.item_ids.append(obj["Id"]) return not update def movie_add(self, obj): - - ''' Add object to kodi. - ''' - obj['RatingId'] = self.create_entry_rating() + """Add object to kodi.""" + obj["RatingId"] = self.create_entry_rating() self.add_ratings(*values(obj, QU.add_rating_movie_obj)) - obj['Unique'] = self.create_entry_unique_id() + obj["Unique"] = self.create_entry_unique_id() self.add_unique_id(*values(obj, QU.add_unique_id_movie_obj)) - obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) - obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj)) + obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj)) self.add(*values(obj, QU.add_movie_obj)) self.add_videoversion(*values(obj, QU.add_video_version_obj)) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_movie_obj)) - LOG.debug("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + LOG.debug( + "ADD movie [%s/%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["MovieId"], + obj["Id"], + obj["Title"], + ) def movie_update(self, obj): - - ''' Update object to kodi. - ''' - obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj)) + """Update object to kodi.""" + obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj)) self.update_ratings(*values(obj, QU.update_rating_movie_obj)) - obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj)) + obj["Unique"] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj)) self.update_unique_id(*values(obj, QU.update_unique_id_movie_obj)) self.update(*values(obj, QU.update_movie_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + LOG.debug( + "UPDATE movie [%s/%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["MovieId"], + obj["Id"], + obj["Title"], + ) def trailer(self, obj): try: - if obj['LocalTrailer']: + if obj["LocalTrailer"]: - trailer = self.server.jellyfin.get_local_trailers(obj['Id']) - obj['Trailer'] = "plugin://plugin.video.jellyfin/trailer?id=%s&mode=play" % trailer[0]['Id'] + trailer = self.server.jellyfin.get_local_trailers(obj["Id"]) + obj["Trailer"] = ( + "plugin://plugin.video.jellyfin/trailer?id=%s&mode=play" + % trailer[0]["Id"] + ) - elif obj['Trailer']: - obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] + elif obj["Trailer"]: + obj["Trailer"] = ( + "plugin://plugin.video.youtube/play/?video_id=%s" + % obj["Trailer"].rsplit("=", 1)[1] + ) except Exception as error: LOG.exception("Failed to get trailer: %s", error) - obj['Trailer'] = None + obj["Trailer"] = None def get_path_filename(self, obj): - - ''' Get the path and filename and build it into protocol://path - ''' - obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] + """Get the path and filename and build it into protocol://path""" + obj["Filename"] = ( + obj["Path"].rsplit("\\", 1)[1] + if "\\" in obj["Path"] + else obj["Path"].rsplit("/", 1)[1] + ) if self.direct_path: - if not validate(obj['Path']): + if not validate(obj["Path"]): raise PathValidationException("Failed to validate path. User stopped.") - obj['Path'] = obj['Path'].replace(obj['Filename'], "") + obj["Path"] = obj["Path"].replace(obj["Filename"], "") - '''check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO''' - if validate_dvd_dir(obj['Path'] + obj['Filename']): - obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/' - obj['Filename'] = 'VIDEO_TS.IFO' - LOG.debug("DVD directory %s", obj['Path']) + """check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO""" + if validate_dvd_dir(obj["Path"] + obj["Filename"]): + obj["Path"] = obj["Path"] + obj["Filename"] + "/VIDEO_TS/" + obj["Filename"] = "VIDEO_TS.IFO" + LOG.debug("DVD directory %s", obj["Path"]) - '''check bluray directories and point it to ./BDMV/index.bdmv''' - if validate_bluray_dir(obj['Path'] + obj['Filename']): - obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/' - obj['Filename'] = 'index.bdmv' - LOG.debug("Bluray directory %s", obj['Path']) + """check bluray directories and point it to ./BDMV/index.bdmv""" + if validate_bluray_dir(obj["Path"] + obj["Filename"]): + obj["Path"] = obj["Path"] + obj["Filename"] + "/BDMV/" + obj["Filename"] = "index.bdmv" + LOG.debug("Bluray directory %s", obj["Path"]) else: - obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] + obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"] params = { - 'filename': py2_encode(obj['Filename'], 'utf-8'), - 'id': obj['Id'], - 'dbid': obj['MovieId'], - 'mode': "play" + "filename": py2_encode(obj["Filename"], "utf-8"), + "id": obj["Id"], + "dbid": obj["MovieId"], + "mode": "play", } - obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params)) + obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params)) @stop @jellyfin_item def boxset(self, item, e_item): + """If item does not exist, entry will be added. + If item exists, entry will be updated. - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - Process movies inside boxset. - Process removals from boxset. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + Process movies inside boxset. + Process removals from boxset. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Boxset') + obj = self.objects.map(item, "Boxset") - obj['Overview'] = API.get_overview(obj['Overview']) + obj["Overview"] = API.get_overview(obj["Overview"]) try: - obj['SetId'] = e_item[0] + obj["SetId"] = e_item[0] self.update_boxset(*values(obj, QU.update_set_obj)) except TypeError: - LOG.debug("SetId %s not found", obj['Id']) - obj['SetId'] = self.add_boxset(*values(obj, QU.add_set_obj)) + LOG.debug("SetId %s not found", obj["Id"]) + obj["SetId"] = self.add_boxset(*values(obj, QU.add_set_obj)) self.boxset_current(obj) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) - for movie in obj['Current']: + for movie in obj["Current"]: temp_obj = dict(obj) - temp_obj['Movie'] = movie - temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']] + temp_obj["Movie"] = movie + temp_obj["MovieId"] = obj["Current"][temp_obj["Movie"]] self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) - self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj)) - LOG.debug("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId']) + self.jellyfin_db.update_parent_id( + *values(temp_obj, QUEM.delete_parent_boxset_obj) + ) + LOG.debug( + "DELETE from boxset [%s] %s: %s", + temp_obj["SetId"], + temp_obj["Title"], + temp_obj["MovieId"], + ) - self.artwork.add(obj['Artwork'], obj['SetId'], "set") + self.artwork.add(obj["Artwork"], obj["SetId"], "set") self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_boxset_obj)) - LOG.debug("UPDATE boxset [%s] %s", obj['SetId'], obj['Title']) + LOG.debug("UPDATE boxset [%s] %s", obj["SetId"], obj["Title"]) def boxset_current(self, obj): - - ''' Add or removes movies based on the current movies found in the boxset. - ''' + """Add or removes movies based on the current movies found in the boxset.""" try: - current = self.jellyfin_db.get_item_id_by_parent_id(*values(obj, QUEM.get_item_id_by_parent_boxset_obj)) + current = self.jellyfin_db.get_item_id_by_parent_id( + *values(obj, QUEM.get_item_id_by_parent_boxset_obj) + ) movies = dict(current) except ValueError: movies = {} - obj['Current'] = movies + obj["Current"] = movies - for all_movies in server.get_movies_by_boxset(obj['Id']): - for movie in all_movies['Items']: + for all_movies in server.get_movies_by_boxset(obj["Id"]): + for movie in all_movies["Items"]: temp_obj = dict(obj) - temp_obj['Title'] = movie['Name'] - temp_obj['Id'] = movie['Id'] + temp_obj["Title"] = movie["Name"] + temp_obj["Id"] = movie["Id"] try: - temp_obj['MovieId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + temp_obj["MovieId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except TypeError: - LOG.info("Failed to process %s to boxset.", temp_obj['Title']) + LOG.info("Failed to process %s to boxset.", temp_obj["Title"]) continue - if temp_obj['Id'] not in obj['Current']: + if temp_obj["Id"] not in obj["Current"]: self.set_boxset(*values(temp_obj, QU.update_movie_set_obj)) - self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_movie_obj)) - LOG.debug("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id']) + self.jellyfin_db.update_parent_id( + *values(temp_obj, QUEM.update_parent_movie_obj) + ) + LOG.debug( + "ADD to boxset [%s/%s] %s: %s to boxset", + temp_obj["SetId"], + temp_obj["MovieId"], + temp_obj["Title"], + temp_obj["Id"], + ) else: - obj['Current'].pop(temp_obj['Id']) + obj["Current"].pop(temp_obj["Id"]) def boxsets_reset(self): - - ''' Special function to remove all existing boxsets. - ''' - boxsets = self.jellyfin_db.get_items_by_media('set') + """Special function to remove all existing boxsets.""" + boxsets = self.jellyfin_db.get_items_by_media("set") for boxset in boxsets: self.remove(boxset[0]) @stop @jellyfin_item def userdata(self, item, e_item): - - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'MovieUserData') + obj = self.objects.map(item, "MovieUserData") try: - obj['MovieId'] = e_item[0] - obj['FileId'] = e_item[1] + obj["MovieId"] = e_item[0] + obj["FileId"] = e_item[1] except TypeError: return - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) - if obj['DatePlayed']: - obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") - if obj['Favorite']: + if obj["Favorite"]: self.get_tag(*values(obj, QU.get_tag_movie_obj)) else: self.remove_tag(*values(obj, QU.delete_tag_movie_obj)) - LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume']) + LOG.debug("New resume point %s: %s", obj["Id"], obj["Resume"]) self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + LOG.debug( + "USERDATA movie [%s/%s] %s: %s", + obj["FileId"], + obj["MovieId"], + obj["Id"], + obj["Title"], + ) @stop @jellyfin_item def remove(self, item_id, e_item): - - ''' Remove movieid, fileid, jellyfin reference. - Remove artwork, boxset - ''' - obj = {'Id': item_id} + """Remove movieid, fileid, jellyfin reference. + Remove artwork, boxset + """ + obj = {"Id": item_id} try: - obj['KodiId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["Media"] = e_item[4] except TypeError: return - self.artwork.delete(obj['KodiId'], obj['Media']) + self.artwork.delete(obj["KodiId"], obj["Media"]) - if obj['Media'] == 'movie': + if obj["Media"] == "movie": self.delete(*values(obj, QU.delete_movie_obj)) - elif obj['Media'] == 'set': + elif obj["Media"] == "set": - for movie in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_movie_obj)): + for movie in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_movie_obj) + ): temp_obj = dict(obj) - temp_obj['MovieId'] = movie[1] - temp_obj['Movie'] = movie[0] + temp_obj["MovieId"] = movie[1] + temp_obj["Movie"] = movie[0] self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) - self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj)) + self.jellyfin_db.update_parent_id( + *values(temp_obj, QUEM.delete_parent_boxset_obj) + ) self.delete_boxset(*values(obj, QU.delete_set_obj)) self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) - LOG.debug("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id']) + LOG.debug( + "DELETE %s [%s/%s] %s", + obj["Media"], + obj["FileId"], + obj["KodiId"], + obj["Id"], + ) diff --git a/jellyfin_kodi/objects/music.py b/jellyfin_kodi/objects/music.py index 1ecf6877..310c2052 100644 --- a/jellyfin_kodi/objects/music.py +++ b/jellyfin_kodi/objects/music.py @@ -39,19 +39,20 @@ class Music(KodiDb): @stop @jellyfin_item def artist(self, item, e_item): - - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """If item does not exist, entry will be added. + If item exists, entry will be updated. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Artist') + obj = self.objects.map(item, "Artist") update = True try: - obj['ArtistId'] = e_item[0] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["ArtistId"] = e_item[0] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False @@ -60,72 +61,81 @@ class Music(KodiDb): # This item doesn't belong to a whitelisted library return - obj['ArtistId'] = None - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] - LOG.debug("ArtistId %s not found", obj['Id']) + obj["ArtistId"] = None + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] + LOG.debug("ArtistId %s not found", obj["Id"]) else: if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None: update = False - LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId']) + LOG.info( + "ArtistId %s missing from kodi. repairing the entry.", + obj["ArtistId"], + ) - obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - obj['ArtistType'] = "MusicArtist" - obj['Genre'] = " / ".join(obj['Genres'] or []) - obj['Bio'] = API.get_overview(obj['Bio']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) - obj['Thumb'] = obj['Artwork']['Primary'] - obj['Backdrops'] = obj['Artwork']['Backdrop'] or "" + obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + obj["ArtistType"] = "MusicArtist" + obj["Genre"] = " / ".join(obj["Genres"] or []) + obj["Bio"] = API.get_overview(obj["Bio"]) + obj["Artwork"] = API.get_all_artwork( + self.objects.map(item, "ArtworkMusic"), True + ) + obj["Thumb"] = obj["Artwork"]["Primary"] + obj["Backdrops"] = obj["Artwork"]["Backdrop"] or "" - if obj['Thumb']: - obj['Thumb'] = "%s" % obj['Thumb'] + if obj["Thumb"]: + obj["Thumb"] = "%s" % obj["Thumb"] - if obj['Backdrops']: - obj['Backdrops'] = "%s" % obj['Backdrops'][0] + if obj["Backdrops"]: + obj["Backdrops"] = "%s" % obj["Backdrops"][0] if update: self.artist_update(obj) else: self.artist_add(obj) - self.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['ArtistId']) - self.artwork.add(obj['Artwork'], obj['ArtistId'], "artist") - self.item_ids.append(obj['Id']) + self.update( + obj["Genre"], + obj["Bio"], + obj["Thumb"], + obj["Backdrops"], + obj["LastScraped"], + obj["ArtistId"], + ) + self.artwork.add(obj["Artwork"], obj["ArtistId"], "artist") + self.item_ids.append(obj["Id"]) def artist_add(self, obj): + """Add object to kodi. - ''' Add object to kodi. - - safety checks: It looks like Jellyfin supports the same artist multiple times. - Kodi doesn't allow that. In case that happens we just merge the artist entries. - ''' - obj['ArtistId'] = self.get(*values(obj, QU.get_artist_obj)) + safety checks: It looks like Jellyfin supports the same artist multiple times. + Kodi doesn't allow that. In case that happens we just merge the artist entries. + """ + obj["ArtistId"] = self.get(*values(obj, QU.get_artist_obj)) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_artist_obj)) - LOG.debug("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + LOG.debug("ADD artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"]) def artist_update(self, obj): - - ''' Update object to kodi. - ''' + """Update object to kodi.""" self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + LOG.debug("UPDATE artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"]) @stop @jellyfin_item def album(self, item, e_item): - - ''' Update object to kodi. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """Update object to kodi.""" + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Album') + obj = self.objects.map(item, "Album") update = True try: - obj['AlbumId'] = e_item[0] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["AlbumId"] = e_item[0] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False @@ -134,31 +144,35 @@ class Music(KodiDb): # This item doesn't belong to a whitelisted library return - obj['AlbumId'] = None - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] - LOG.debug("AlbumId %s not found", obj['Id']) + obj["AlbumId"] = None + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] + LOG.debug("AlbumId %s not found", obj["Id"]) else: if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None: update = False - LOG.info("AlbumId %s missing from kodi. repairing the entry.", obj['AlbumId']) + LOG.info( + "AlbumId %s missing from kodi. repairing the entry.", obj["AlbumId"] + ) - obj['Rating'] = 0 - obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - obj['Genres'] = obj['Genres'] or [] - obj['Genre'] = " / ".join(obj['Genres']) - obj['Bio'] = API.get_overview(obj['Bio']) - obj['Artists'] = " / ".join(obj['Artists'] or []) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) - obj['Thumb'] = obj['Artwork']['Primary'] - obj['DateAdded'] = item.get('DateCreated') + obj["Rating"] = 0 + obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + obj["Genres"] = obj["Genres"] or [] + obj["Genre"] = " / ".join(obj["Genres"]) + obj["Bio"] = API.get_overview(obj["Bio"]) + obj["Artists"] = " / ".join(obj["Artists"] or []) + obj["Artwork"] = API.get_all_artwork( + self.objects.map(item, "ArtworkMusic"), True + ) + obj["Thumb"] = obj["Artwork"]["Primary"] + obj["DateAdded"] = item.get("DateCreated") - if obj['DateAdded']: - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + if obj["DateAdded"]: + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") - if obj['Thumb']: - obj['Thumb'] = "%s" % obj['Thumb'] + if obj["Thumb"]: + obj["Thumb"] = "%s" % obj["Thumb"] if update: self.album_update(obj) @@ -169,89 +183,90 @@ class Music(KodiDb): self.artist_discography(obj) self.update_album(*values(obj, QU.update_album_obj)) self.add_genres(*values(obj, QU.add_genres_obj)) - self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") - self.item_ids.append(obj['Id']) + self.artwork.add(obj["Artwork"], obj["AlbumId"], "album") + self.item_ids.append(obj["Id"]) def album_add(self, obj): - - ''' Add object to kodi. - ''' + """Add object to kodi.""" if self.version_id >= 82: obj_values = values(obj, QU.get_album_obj82) else: obj_values = values(obj, QU.get_album_obj) - obj['AlbumId'] = self.get_album(*obj_values) + obj["AlbumId"] = self.get_album(*obj_values) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_album_obj)) - LOG.debug("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + LOG.debug("ADD album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"]) def album_update(self, obj): - - ''' Update object to kodi. - ''' + """Update object to kodi.""" self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + LOG.debug("UPDATE album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"]) def artist_discography(self, obj): - - ''' Update the artist's discography. - ''' - for artist in (obj['ArtistItems'] or []): + """Update the artist's discography.""" + for artist in obj["ArtistItems"] or []: temp_obj = dict(obj) - temp_obj['Id'] = artist['Id'] - temp_obj['AlbumId'] = obj['Id'] + temp_obj["Id"] = artist["Id"] + temp_obj["AlbumId"] = obj["Id"] try: - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except TypeError: continue self.add_discography(*values(temp_obj, QU.update_discography_obj)) - self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_album_obj)) + self.jellyfin_db.update_parent_id( + *values(temp_obj, QUEM.update_parent_album_obj) + ) def artist_link(self, obj): - - ''' Assign main artists to album. - Artist does not exist in jellyfin database, create the reference. - ''' - for artist in (obj['AlbumArtists'] or []): + """Assign main artists to album. + Artist does not exist in jellyfin database, create the reference. + """ + for artist in obj["AlbumArtists"] or []: temp_obj = dict(obj) - temp_obj['Name'] = artist['Name'] - temp_obj['Id'] = artist['Id'] + temp_obj["Name"] = artist["Name"] + temp_obj["Id"] = artist["Id"] try: - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except TypeError: try: - self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + self.artist(self.server.jellyfin.get_item(temp_obj["Id"])) + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except Exception as error: LOG.exception(error) continue self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj)) self.link(*values(temp_obj, QU.update_link_obj)) - self.item_ids.append(temp_obj['Id']) + self.item_ids.append(temp_obj["Id"]) @stop @jellyfin_item def song(self, item, e_item): - - ''' Update object to kodi. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """Update object to kodi.""" + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Song') + obj = self.objects.map(item, "Song") update = True try: - obj['SongId'] = e_item[0] - obj['PathId'] = e_item[2] - obj['AlbumId'] = e_item[3] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["SongId"] = e_item[0] + obj["PathId"] = e_item[2] + obj["AlbumId"] = e_item[3] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False @@ -260,38 +275,42 @@ class Music(KodiDb): # This item doesn't belong to a whitelisted library return - obj['SongId'] = self.create_entry_song() - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] - LOG.debug("SongId %s not found", obj['Id']) + obj["SongId"] = self.create_entry_song() + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] + LOG.debug("SongId %s not found", obj["Id"]) else: if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None: update = False - LOG.info("SongId %s missing from kodi. repairing the entry.", obj['SongId']) + LOG.info( + "SongId %s missing from kodi. repairing the entry.", obj["SongId"] + ) self.get_song_path_filename(obj, API) - obj['Rating'] = 0 - obj['Genres'] = obj['Genres'] or [] - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0 - obj['Genre'] = " / ".join(obj['Genres']) - obj['Artists'] = " / ".join(obj['Artists'] or []) - obj['AlbumArtists'] = obj['AlbumArtists'] or [] - obj['Index'] = obj['Index'] or 0 - obj['Disc'] = obj['Disc'] or 1 - obj['EmbedCover'] = False - obj['Comment'] = API.get_overview(obj['Comment']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj["Rating"] = 0 + obj["Genres"] = obj["Genres"] or [] + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) + obj["Runtime"] = (obj["Runtime"] or 0) / 10000000.0 + obj["Genre"] = " / ".join(obj["Genres"]) + obj["Artists"] = " / ".join(obj["Artists"] or []) + obj["AlbumArtists"] = obj["AlbumArtists"] or [] + obj["Index"] = obj["Index"] or 0 + obj["Disc"] = obj["Disc"] or 1 + obj["EmbedCover"] = False + obj["Comment"] = API.get_overview(obj["Comment"]) + obj["Artwork"] = API.get_all_artwork( + self.objects.map(item, "ArtworkMusic"), True + ) - if obj['DateAdded']: - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + if obj["DateAdded"]: + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") - if obj['DatePlayed']: - obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") - obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index'] + obj["Index"] = obj["Disc"] * 2**16 + obj["Index"] if update: self.song_update(obj) @@ -303,227 +322,275 @@ class Music(KodiDb): self.song_artist_link(obj) self.song_artist_discography(obj) - obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists']) + obj["strAlbumArtists"] = " / ".join(obj["AlbumArtists"]) self.get_album_artist(*values(obj, QU.get_album_artist_obj)) self.add_genres(*values(obj, QU.update_genre_song_obj)) - self.artwork.add(obj['Artwork'], obj['SongId'], "song") - self.item_ids.append(obj['Id']) + self.artwork.add(obj["Artwork"], obj["SongId"], "song") + self.item_ids.append(obj["Id"]) - if obj['SongAlbumId'] is None: - self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") + if obj["SongAlbumId"] is None: + self.artwork.add(obj["Artwork"], obj["AlbumId"], "album") return not update def song_add(self, obj): + """Add object to kodi. - ''' Add object to kodi. - - Verify if there's an album associated. - If no album found, create a single's album - ''' - obj['PathId'] = self.add_path(obj['Path']) + Verify if there's an album associated. + If no album found, create a single's album + """ + obj["PathId"] = self.add_path(obj["Path"]) try: - obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0] + obj["AlbumId"] = self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_song_obj) + )[0] except TypeError: try: - if obj['SongAlbumId'] is None: + if obj["SongAlbumId"] is None: raise TypeError("No album id found associated?") - self.album(self.server.jellyfin.get_item(obj['SongAlbumId'])) - obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0] + self.album(self.server.jellyfin.get_item(obj["SongAlbumId"])) + obj["AlbumId"] = self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_song_obj) + )[0] except TypeError: self.single(obj) self.add_song(*values(obj, QU.add_song_obj)) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_song_obj)) - LOG.debug("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + LOG.debug( + "ADD song [%s/%s/%s] %s: %s", + obj["PathId"], + obj["AlbumId"], + obj["SongId"], + obj["Id"], + obj["Title"], + ) def song_update(self, obj): - - ''' Update object to kodi. - ''' + """Update object to kodi.""" self.update_path(*values(obj, QU.update_path_obj)) self.update_song(*values(obj, QU.update_song_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + LOG.debug( + "UPDATE song [%s/%s/%s] %s: %s", + obj["PathId"], + obj["AlbumId"], + obj["SongId"], + obj["Id"], + obj["Title"], + ) def get_song_path_filename(self, obj, api): - - ''' Get the path and filename and build it into protocol://path - ''' - obj['Path'] = api.get_file_path(obj['Path']) - obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] + """Get the path and filename and build it into protocol://path""" + obj["Path"] = api.get_file_path(obj["Path"]) + obj["Filename"] = ( + obj["Path"].rsplit("\\", 1)[1] + if "\\" in obj["Path"] + else obj["Path"].rsplit("/", 1)[1] + ) if self.direct_path: - if not validate(obj['Path']): + if not validate(obj["Path"]): raise PathValidationException("Failed to validate path. User stopped.") - obj['Path'] = obj['Path'].replace(obj['Filename'], "") + obj["Path"] = obj["Path"].replace(obj["Filename"], "") else: - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] - obj['Path'] = "%s/Audio/%s/" % (server_address, obj['Id']) - obj['Filename'] = "stream.%s?static=true" % obj['Container'] + server_address = self.server.auth.get_server_info( + self.server.auth.server_id + )["address"] + obj["Path"] = "%s/Audio/%s/" % (server_address, obj["Id"]) + obj["Filename"] = "stream.%s?static=true" % obj["Container"] def song_artist_discography(self, obj): - - ''' Update the artist's discography. - ''' + """Update the artist's discography.""" artists = [] - for artist in (obj['AlbumArtists'] or []): + for artist in obj["AlbumArtists"] or []: temp_obj = dict(obj) - temp_obj['Name'] = artist['Name'] - temp_obj['Id'] = artist['Id'] + temp_obj["Name"] = artist["Name"] + temp_obj["Id"] = artist["Id"] - artists.append(temp_obj['Name']) + artists.append(temp_obj["Name"]) try: - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except TypeError: try: - self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + self.artist(self.server.jellyfin.get_item(temp_obj["Id"])) + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except Exception as error: LOG.exception(error) continue self.link(*values(temp_obj, QU.update_link_obj)) - self.item_ids.append(temp_obj['Id']) + self.item_ids.append(temp_obj["Id"]) - if obj['Album']: + if obj["Album"]: - temp_obj['Title'] = obj['Album'] - temp_obj['Year'] = 0 + temp_obj["Title"] = obj["Album"] + temp_obj["Year"] = 0 self.add_discography(*values(temp_obj, QU.update_discography_obj)) - obj['AlbumArtists'] = artists + obj["AlbumArtists"] = artists def song_artist_link(self, obj): - - ''' Assign main artists to song. - Artist does not exist in jellyfin database, create the reference. - ''' - for index, artist in enumerate(obj['ArtistItems'] or []): + """Assign main artists to song. + Artist does not exist in jellyfin database, create the reference. + """ + for index, artist in enumerate(obj["ArtistItems"] or []): temp_obj = dict(obj) - temp_obj['Name'] = artist['Name'] - temp_obj['Id'] = artist['Id'] - temp_obj['Index'] = index + temp_obj["Name"] = artist["Name"] + temp_obj["Id"] = artist["Id"] + temp_obj["Index"] = index try: - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except TypeError: try: - self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) - temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + self.artist(self.server.jellyfin.get_item(temp_obj["Id"])) + temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] except Exception as error: LOG.exception(error) continue self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj)) - self.item_ids.append(temp_obj['Id']) + self.item_ids.append(temp_obj["Id"]) def single(self, obj): - obj['AlbumId'] = self.create_entry_album() + obj["AlbumId"] = self.create_entry_album() self.add_single(*values(obj, QU.add_single_obj)) @stop @jellyfin_item def userdata(self, item, e_item): + """This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + """ - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - ''' - - obj = self.objects.map(item, 'SongUserData') + obj = self.objects.map(item, "SongUserData") try: - obj['KodiId'] = e_item[0] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["Media"] = e_item[4] except TypeError: return - obj['Rating'] = 0 + obj["Rating"] = 0 - if obj['Media'] == 'song': + if obj["Media"] == "song": - if obj['DatePlayed']: - obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = ( + Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) self.rate_song(*values(obj, QU.update_song_rating_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title']) + LOG.debug( + "USERDATA %s [%s] %s: %s", + obj["Media"], + obj["KodiId"], + obj["Id"], + obj["Title"], + ) @stop @jellyfin_item def remove(self, item_id, e_item): + """This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - - This should address single song scenario, where server doesn't actually - create an album for the song. - ''' - obj = {'Id': item_id} + This should address single song scenario, where server doesn't actually + create an album for the song. + """ + obj = {"Id": item_id} try: - obj['KodiId'] = e_item[0] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["Media"] = e_item[4] except TypeError: return - if obj['Media'] == 'song': + if obj["Media"] == "song": - self.remove_song(obj['KodiId'], obj['Id']) - self.jellyfin_db.remove_wild_item(obj['Id']) + self.remove_song(obj["KodiId"], obj["Id"]) + self.jellyfin_db.remove_wild_item(obj["Id"]) - for item in self.jellyfin_db.get_item_by_wild_id(*values(obj, QUEM.get_item_by_wild_obj)): - if item[1] == 'album': + for item in self.jellyfin_db.get_item_by_wild_id( + *values(obj, QUEM.get_item_by_wild_obj) + ): + if item[1] == "album": temp_obj = dict(obj) - temp_obj['ParentId'] = item[0] + temp_obj["ParentId"] = item[0] - if not self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): - self.remove_album(temp_obj['ParentId'], obj['Id']) + if not self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_song_obj) + ): + self.remove_album(temp_obj["ParentId"], obj["Id"]) - elif obj['Media'] == 'album': - obj['ParentId'] = obj['KodiId'] + elif obj["Media"] == "album": + obj["ParentId"] = obj["KodiId"] - for song in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_song_obj)): - self.remove_song(song[1], obj['Id']) + for song in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_song_obj) + ): + self.remove_song(song[1], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_song_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(obj, QUEM.delete_item_by_parent_song_obj) + ) - self.remove_album(obj['KodiId'], obj['Id']) + self.remove_album(obj["KodiId"], obj["Id"]) - elif obj['Media'] == 'artist': - obj['ParentId'] = obj['KodiId'] + elif obj["Media"] == "artist": + obj["ParentId"] = obj["KodiId"] - for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)): + for album in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_album_obj) + ): temp_obj = dict(obj) - temp_obj['ParentId'] = album[1] + temp_obj["ParentId"] = album[1] - for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): - self.remove_song(song[1], obj['Id']) + for song in self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_song_obj) + ): + self.remove_song(song[1], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_song_obj)) - self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_artist_obj)) - self.remove_album(temp_obj['ParentId'], obj['Id']) + self.jellyfin_db.remove_items_by_parent_id( + *values(temp_obj, QUEM.delete_item_by_parent_song_obj) + ) + self.jellyfin_db.remove_items_by_parent_id( + *values(temp_obj, QUEM.delete_item_by_parent_artist_obj) + ) + self.remove_album(temp_obj["ParentId"], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_album_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(obj, QUEM.delete_item_by_parent_album_obj) + ) - self.remove_artist(obj['KodiId'], obj['Id']) + self.remove_artist(obj["KodiId"], obj["Id"]) self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) @@ -547,29 +614,31 @@ class Music(KodiDb): @jellyfin_item def get_child(self, item_id, e_item): - - ''' Get all child elements from tv show jellyfin id. - ''' - obj = {'Id': item_id} + """Get all child elements from tv show jellyfin id.""" + obj = {"Id": item_id} child = [] try: - obj['KodiId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['ParentId'] = e_item[3] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["ParentId"] = e_item[3] + obj["Media"] = e_item[4] except TypeError: return child - obj['ParentId'] = obj['KodiId'] + obj["ParentId"] = obj["KodiId"] - for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)): + for album in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_album_obj) + ): temp_obj = dict(obj) - temp_obj['ParentId'] = album[1] + temp_obj["ParentId"] = album[1] child.append((album[0],)) - for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): + for song in self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_song_obj) + ): child.append((song[0],)) return child diff --git a/jellyfin_kodi/objects/musicvideos.py b/jellyfin_kodi/objects/musicvideos.py index 93cc7b60..c2d280e4 100644 --- a/jellyfin_kodi/objects/musicvideos.py +++ b/jellyfin_kodi/objects/musicvideos.py @@ -43,24 +43,25 @@ class MusicVideos(KodiDb): @stop @jellyfin_item def musicvideo(self, item, e_item): + """If item does not exist, entry will be added. + If item exists, entry will be updated. - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - If we don't get the track number from Jellyfin, see if we can infer it - from the sortname attribute. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + If we don't get the track number from Jellyfin, see if we can infer it + from the sortname attribute. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'MusicVideo') + obj = self.objects.map(item, "MusicVideo") update = True try: - obj['MvideoId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['PathId'] = e_item[2] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["MvideoId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["PathId"] = e_item[2] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False @@ -69,67 +70,80 @@ class MusicVideos(KodiDb): # This item doesn't belong to a whitelisted library return - LOG.debug("MvideoId for %s not found", obj['Id']) - obj['MvideoId'] = self.create_entry() - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] + LOG.debug("MvideoId for %s not found", obj["Id"]) + obj["MvideoId"] = self.create_entry() + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] else: if self.get(*values(obj, QU.get_musicvideo_obj)) is None: update = False - LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId']) + LOG.info( + "MvideoId %s missing from kodi. repairing the entry.", + obj["MvideoId"], + ) - if (obj.get('ProductionYear') or 0) > 9999: - obj['ProductionYear'] = int(str(obj['ProductionYear'])[:4]) + if (obj.get("ProductionYear") or 0) > 9999: + obj["ProductionYear"] = int(str(obj["ProductionYear"])[:4]) - if (obj.get('Year') or 0) > 9999: - obj['Year'] = int(str(obj['Year'])[:4]) + if (obj.get("Year") or 0) > 9999: + obj["Year"] = int(str(obj["Year"])[:4]) - obj['Path'] = API.get_file_path(obj['Path']) - obj['Genres'] = obj['Genres'] or [] - obj['ArtistItems'] = obj['ArtistItems'] or [] - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] - obj['Plot'] = API.get_overview(obj['Plot']) - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['Premiere'] = Local(obj['Premiere']) if obj['Premiere'] else datetime.date(obj['Year'] or 1970, 1, 1) - obj['Genre'] = " / ".join(obj['Genres']) - obj['Studio'] = " / ".join(obj['Studios']) - obj['Artists'] = " / ".join(obj['Artists'] or []) - obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj["Path"] = API.get_file_path(obj["Path"]) + obj["Genres"] = obj["Genres"] or [] + obj["ArtistItems"] = obj["ArtistItems"] or [] + obj["Studios"] = [ + API.validate_studio(studio) for studio in (obj["Studios"] or []) + ] + obj["Plot"] = API.get_overview(obj["Plot"]) + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + obj["DatePlayed"] = ( + None + if not obj["DatePlayed"] + else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["Premiere"] = ( + Local(obj["Premiere"]) + if obj["Premiere"] + else datetime.date(obj["Year"] or 1970, 1, 1) + ) + obj["Genre"] = " / ".join(obj["Genres"]) + obj["Studio"] = " / ".join(obj["Studios"]) + obj["Artists"] = " / ".join(obj["Artists"] or []) + obj["Directors"] = " / ".join(obj["Directors"] or []) + obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"]) + obj["Audio"] = API.audio_streams(obj["Audio"] or []) + obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"]) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) self.get_path_filename(obj) - if obj['Premiere']: - obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ") + if obj["Premiere"]: + obj["Premiere"] = str(obj["Premiere"]).split(".")[0].replace("T", " ") - for artist in obj['ArtistItems']: - artist['Type'] = "Artist" + for artist in obj["ArtistItems"]: + artist["Type"] = "Artist" - obj['People'] = obj['People'] or [] + obj['ArtistItems'] - obj['People'] = API.get_people_artwork(obj['People']) + obj["People"] = obj["People"] or [] + obj["ArtistItems"] + obj["People"] = API.get_people_artwork(obj["People"]) - if obj['Index'] is None and obj['SortTitle'] is not None: - search = re.search(r'^\d+\s?', obj['SortTitle']) + if obj["Index"] is None and obj["SortTitle"] is not None: + search = re.search(r"^\d+\s?", obj["SortTitle"]) if search: - obj['Index'] = search.group() + obj["Index"] = search.group() tags = [] - tags.extend(obj['Tags'] or []) - tags.append(obj['LibraryName']) + tags.extend(obj["Tags"] or []) + tags.append(obj["LibraryName"]) - if obj['Favorite']: - tags.append('Favorite musicvideos') + if obj["Favorite"]: + tags.append("Favorite musicvideos") - obj['Tags'] = tags + obj["Tags"] = tags if update: self.musicvideo_update(obj) @@ -144,106 +158,129 @@ class MusicVideos(KodiDb): self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.add_people(*values(obj, QU.add_people_mvideo_obj)) self.add_streams(*values(obj, QU.add_streams_obj)) - self.artwork.add(obj['Artwork'], obj['MvideoId'], "musicvideo") - self.item_ids.append(obj['Id']) + self.artwork.add(obj["Artwork"], obj["MvideoId"], "musicvideo") + self.item_ids.append(obj["Id"]) return not update def musicvideo_add(self, obj): - - ''' Add object to kodi. - ''' - obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) - obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + """Add object to kodi.""" + obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj)) + obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj)) self.add(*values(obj, QU.add_musicvideo_obj)) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_mvideo_obj)) - LOG.debug("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + LOG.debug( + "ADD mvideo [%s/%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["MvideoId"], + obj["Id"], + obj["Title"], + ) def musicvideo_update(self, obj): - - ''' Update object to kodi. - ''' + """Update object to kodi.""" self.update(*values(obj, QU.update_musicvideo_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + LOG.debug( + "UPDATE mvideo [%s/%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["MvideoId"], + obj["Id"], + obj["Title"], + ) def get_path_filename(self, obj): - - ''' Get the path and filename and build it into protocol://path - ''' - obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] + """Get the path and filename and build it into protocol://path""" + obj["Filename"] = ( + obj["Path"].rsplit("\\", 1)[1] + if "\\" in obj["Path"] + else obj["Path"].rsplit("/", 1)[1] + ) if self.direct_path: - if not validate(obj['Path']): + if not validate(obj["Path"]): raise PathValidationException("Failed to validate path. User stopped.") - obj['Path'] = obj['Path'].replace(obj['Filename'], "") + obj["Path"] = obj["Path"].replace(obj["Filename"], "") else: - obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] + obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"] params = { - 'filename': py2_encode(obj['Filename'], 'utf-8'), - 'id': obj['Id'], - 'dbid': obj['MvideoId'], - 'mode': "play" + "filename": py2_encode(obj["Filename"], "utf-8"), + "id": obj["Id"], + "dbid": obj["MvideoId"], + "mode": "play", } - obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params)) + obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params)) @stop @jellyfin_item def userdata(self, item, e_item): - - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + """This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'MusicVideoUserData') + obj = self.objects.map(item, "MusicVideoUserData") try: - obj['MvideoId'] = e_item[0] - obj['FileId'] = e_item[1] + obj["MvideoId"] = e_item[0] + obj["FileId"] = e_item[1] except TypeError: return - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) - if obj['DatePlayed']: - obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") - if obj['Favorite']: + if obj["Favorite"]: self.get_tag(*values(obj, QU.get_tag_mvideo_obj)) else: self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj)) self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + LOG.debug( + "USERDATA mvideo [%s/%s] %s: %s", + obj["FileId"], + obj["MvideoId"], + obj["Id"], + obj["Title"], + ) @stop @jellyfin_item def remove(self, item_id, e_item): - - ''' Remove mvideoid, fileid, pathid, jellyfin reference. - ''' - obj = {'Id': item_id} + """Remove mvideoid, fileid, pathid, jellyfin reference.""" + obj = {"Id": item_id} try: - obj['MvideoId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['PathId'] = e_item[2] + obj["MvideoId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["PathId"] = e_item[2] except TypeError: return - self.artwork.delete(obj['MvideoId'], "musicvideo") + self.artwork.delete(obj["MvideoId"], "musicvideo") self.delete(*values(obj, QU.delete_musicvideo_obj)) if self.direct_path: self.remove_path(*values(obj, QU.delete_path_obj)) self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) - LOG.debug("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id']) + LOG.debug( + "DELETE musicvideo %s [%s/%s] %s", + obj["MvideoId"], + obj["PathId"], + obj["FileId"], + obj["Id"], + ) diff --git a/jellyfin_kodi/objects/obj.py b/jellyfin_kodi/objects/obj.py index f9015791..ace80e12 100644 --- a/jellyfin_kodi/objects/obj.py +++ b/jellyfin_kodi/objects/obj.py @@ -23,35 +23,30 @@ class Objects(object): _shared_state = {} def __init__(self): - - ''' Hold all persistent data here. - ''' + """Hold all persistent data here.""" self.__dict__ = self._shared_state def mapping(self): - - ''' Load objects mapping. - ''' + """Load objects mapping.""" file_dir = os.path.dirname(ensure_text(__file__, get_filesystem_encoding())) - with open(os.path.join(file_dir, 'obj_map.json')) as infile: + with open(os.path.join(file_dir, "obj_map.json")) as infile: self.objects = json.load(infile) def map(self, item, mapping_name): + """Syntax to traverse the item dictionary. + This of the query almost as a url. - ''' Syntax to traverse the item dictionary. - This of the query almost as a url. + Item is the Jellyfin item json object structure - Item is the Jellyfin item json object structure - - ",": each element will be used as a fallback until a value is found. - "?": split filters and key name from the query part, i.e. MediaSources/0?$Name - "$": lead the key name with $. Only one key value can be requested per element. - ":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name - MediaStreams is a list. - "/": indicates where to go directly - ''' + ",": each element will be used as a fallback until a value is found. + "?": split filters and key name from the query part, i.e. MediaSources/0?$Name + "$": lead the key name with $. Only one key value can be requested per element. + ":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name + MediaStreams is a list. + "/": indicates where to go directly + """ self.mapped_item = {} if not mapping_name: @@ -62,7 +57,7 @@ class Objects(object): for key, value in iteritems(mapping): self.mapped_item[key] = None - params = value.split(',') + params = value.split(",") for param in params: @@ -71,19 +66,19 @@ class Objects(object): obj_key = "" obj_filters = {} - if '?' in obj_param: + if "?" in obj_param: - if '$' in obj_param: - obj_param, obj_key = obj_param.rsplit('$', 1) + if "$" in obj_param: + obj_param, obj_key = obj_param.rsplit("$", 1) - obj_param, filters = obj_param.rsplit('?', 1) + obj_param, filters = obj_param.rsplit("?", 1) if filters: - for filter in filters.split('&'): - filter_key, filter_value = filter.split('=') + for filter in filters.split("&"): + filter_key, filter_value = filter.split("=") obj_filters[filter_key] = filter_value - if ':' in obj_param: + if ":" in obj_param: result = [] for d in self.__recursiveloop__(obj, obj_param): @@ -94,7 +89,7 @@ class Objects(object): obj = result obj_filters = {} - elif '/' in obj_param: + elif "/" in obj_param: obj = self.__recursive__(obj, obj_param) elif obj is item and obj is not None: @@ -107,21 +102,31 @@ class Objects(object): continue if obj_key: - obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key) + obj = ( + [d[obj_key] for d in obj if d.get(obj_key)] + if type(obj) == list + else obj.get(obj_key) + ) self.mapped_item[key] = obj break - if not mapping_name.startswith('Browse') and not mapping_name.startswith('Artwork') and not mapping_name.startswith('UpNext'): + if ( + not mapping_name.startswith("Browse") + and not mapping_name.startswith("Artwork") + and not mapping_name.startswith("UpNext") + ): - self.mapped_item['ProviderName'] = self.objects.get('%sProviderName' % mapping_name) - self.mapped_item['Checksum'] = json.dumps(item['UserData']) + self.mapped_item["ProviderName"] = self.objects.get( + "%sProviderName" % mapping_name + ) + self.mapped_item["Checksum"] = json.dumps(item["UserData"]) return self.mapped_item def __recursiveloop__(self, obj, keys): - first, rest = keys.split(':', 1) + first, rest = keys.split(":", 1) obj = self.__recursive__(obj, first) if obj: @@ -133,7 +138,7 @@ class Objects(object): def __recursive__(self, obj, keys): - for string in keys.split('/'): + for string in keys.split("/"): if not obj: return @@ -150,10 +155,10 @@ class Objects(object): inverse = False - if value.startswith('!'): + if value.startswith("!"): inverse = True - value = value.split('!', 1)[1] + value = value.split("!", 1)[1] if value.lower() == "null": value = None diff --git a/jellyfin_kodi/objects/tvshows.py b/jellyfin_kodi/objects/tvshows.py index 31b89ba4..09485509 100644 --- a/jellyfin_kodi/objects/tvshows.py +++ b/jellyfin_kodi/objects/tvshows.py @@ -11,7 +11,16 @@ from kodi_six.utils import py2_encode from .. import downloader as server from ..database import jellyfin_db, queries as QUEM -from ..helper import api, stop, validate, validate_bluray_dir, validate_dvd_dir, jellyfin_item, values, Local +from ..helper import ( + api, + stop, + validate, + validate_bluray_dir, + validate_dvd_dir, + jellyfin_item, + values, + Local, +) from ..helper import LazyLogger from ..helper.utils import find_library from ..helper.exceptions import PathValidationException @@ -28,7 +37,15 @@ LOG = LazyLogger(__name__) class TVShows(KodiDb): - def __init__(self, server, jellyfindb, videodb, direct_path, library=None, update_library=False): + def __init__( + self, + server, + jellyfindb, + videodb, + direct_path, + library=None, + update_library=False, + ): self.server = server self.jellyfin = jellyfindb @@ -46,69 +63,76 @@ class TVShows(KodiDb): @stop @jellyfin_item def tvshow(self, item, e_item): + """If item does not exist, entry will be added. + If item exists, entry will be updated. - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - If the show is empty, try to remove it. - Process seasons. - Apply series pooling. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + If the show is empty, try to remove it. + Process seasons. + Apply series pooling. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Series') + obj = self.objects.map(item, "Series") update = True try: - obj['ShowId'] = e_item[0] - obj['PathId'] = e_item[2] - obj['LibraryId'] = e_item[6] - obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) + obj["ShowId"] = e_item[0] + obj["PathId"] = e_item[2] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) except TypeError: update = False - LOG.debug("ShowId %s not found", obj['Id']) + LOG.debug("ShowId %s not found", obj["Id"]) library = self.library or find_library(self.server, item) if not library: # This item doesn't belong to a whitelisted library return - obj['ShowId'] = self.create_entry() - obj['LibraryId'] = library['Id'] - obj['LibraryName'] = library['Name'] + obj["ShowId"] = self.create_entry() + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] else: if self.get(*values(obj, QU.get_tvshow_obj)) is None: update = False - LOG.info("ShowId %s missing from kodi. repairing the entry.", obj['ShowId']) + LOG.info( + "ShowId %s missing from kodi. repairing the entry.", obj["ShowId"] + ) - obj['Path'] = API.get_file_path(obj['Path']) - obj['Genres'] = obj['Genres'] or [] - obj['People'] = obj['People'] or [] - obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] - obj['Genre'] = " / ".join(obj['Genres']) - obj['People'] = API.get_people_artwork(obj['People']) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['Studio'] = " / ".join(obj['Studios']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj["Path"] = API.get_file_path(obj["Path"]) + obj["Genres"] = obj["Genres"] or [] + obj["People"] = obj["People"] or [] + obj["Mpaa"] = API.get_mpaa(obj["Mpaa"]) + obj["Studios"] = [ + API.validate_studio(studio) for studio in (obj["Studios"] or []) + ] + obj["Genre"] = " / ".join(obj["Genres"]) + obj["People"] = API.get_people_artwork(obj["People"]) + obj["Plot"] = API.get_overview(obj["Plot"]) + obj["Studio"] = " / ".join(obj["Studios"]) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) - if obj['Status'] != 'Ended': - obj['Status'] = None + if obj["Status"] != "Ended": + obj["Status"] = None self.get_path_filename(obj) - if obj['Premiere']: - obj['Premiere'] = str(Local(obj['Premiere'])).split('.')[0].replace('T', " ") + if obj["Premiere"]: + obj["Premiere"] = ( + str(Local(obj["Premiere"])).split(".")[0].replace("T", " ") + ) tags = [] - tags.extend(obj['Tags'] or []) - tags.append(obj['LibraryName']) + tags.extend(obj["Tags"] or []) + tags.append(obj["LibraryName"]) - if obj['Favorite']: - tags.append('Favorite tvshows') + if obj["Favorite"]: + tags.append("Favorite tvshows") - obj['Tags'] = tags + obj["Tags"] = tags if update: self.tvshow_update(obj) @@ -121,54 +145,60 @@ class TVShows(KodiDb): self.add_people(*values(obj, QU.add_people_tvshow_obj)) self.add_genres(*values(obj, QU.add_genres_tvshow_obj)) self.add_studios(*values(obj, QU.add_studios_tvshow_obj)) - self.artwork.add(obj['Artwork'], obj['ShowId'], "tvshow") - self.item_ids.append(obj['Id']) + self.artwork.add(obj["Artwork"], obj["ShowId"], "tvshow") + self.item_ids.append(obj["Id"]) season_episodes = {} - for season in self.server.jellyfin.get_seasons(obj['Id'])['Items']: + for season in self.server.jellyfin.get_seasons(obj["Id"])["Items"]: - if season['SeriesId'] != obj['Id']: - obj['SeriesId'] = season['SeriesId'] - self.item_ids.append(season['SeriesId']) + if season["SeriesId"] != obj["Id"]: + obj["SeriesId"] = season["SeriesId"] + self.item_ids.append(season["SeriesId"]) try: - self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_series_obj) + )[0] if self.update_library: - season_episodes[season['Id']] = season['SeriesId'] + season_episodes[season["Id"]] = season["SeriesId"] except TypeError: - self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_pool_obj)) - LOG.info("POOL %s [%s/%s]", obj['Title'], obj['Id'], obj['SeriesId']) - season_episodes[season['Id']] = season['SeriesId'] + self.jellyfin_db.add_reference( + *values(obj, QUEM.add_reference_pool_obj) + ) + LOG.info( + "POOL %s [%s/%s]", obj["Title"], obj["Id"], obj["SeriesId"] + ) + season_episodes[season["Id"]] = season["SeriesId"] try: - self.jellyfin_db.get_item_by_id(season['Id'])[0] - self.item_ids.append(season['Id']) + self.jellyfin_db.get_item_by_id(season["Id"])[0] + self.item_ids.append(season["Id"]) except TypeError: - self.season(season, obj['ShowId']) + self.season(season, obj["ShowId"]) else: season_id = self.get_season(*values(obj, QU.get_season_special_obj)) - self.artwork.add(obj['Artwork'], season_id, "season") + self.artwork.add(obj["Artwork"], season_id, "season") for season in season_episodes: - for episodes in server.get_episode_by_season(season_episodes[season], season): + for episodes in server.get_episode_by_season( + season_episodes[season], season + ): - for episode in episodes['Items']: + for episode in episodes["Items"]: self.episode(episode) def tvshow_add(self, obj): - - ''' Add object to kodi. - ''' - obj['RatingId'] = self.create_entry_rating() + """Add object to kodi.""" + obj["RatingId"] = self.create_entry_rating() self.add_ratings(*values(obj, QU.add_rating_tvshow_obj)) - obj['Unique'] = self.create_entry_unique_id() + obj["Unique"] = self.create_entry_unique_id() self.add_unique_id(*values(obj, QU.add_unique_id_tvshow_obj)) - obj['TopPathId'] = self.add_path(obj['TopLevel']) + obj["TopPathId"] = self.add_path(obj["TopLevel"]) if self.direct_path: # Normal way, we use the actual top path @@ -178,179 +208,207 @@ class TVShows(KodiDb): # We create a path on top of all others that holds mediaType and scrapper self.update_path(*values(obj, QU.update_path_toptvshow_addon_obj)) temp_obj = dict() - temp_obj['TopLevel'] = 'plugin://plugin.video.jellyfin/' - temp_obj['TopPathId'] = self.add_path(temp_obj['TopLevel']) + temp_obj["TopLevel"] = "plugin://plugin.video.jellyfin/" + temp_obj["TopPathId"] = self.add_path(temp_obj["TopLevel"]) self.update_path(*values(temp_obj, QU.update_path_toptvshow_obj)) - self.update_path_parent_id(obj['TopPathId'], temp_obj['TopPathId']) + self.update_path_parent_id(obj["TopPathId"], temp_obj["TopPathId"]) - obj['PathId'] = self.add_path(*values(obj, QU.get_path_obj)) + obj["PathId"] = self.add_path(*values(obj, QU.get_path_obj)) self.add(*values(obj, QU.add_tvshow_obj)) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_tvshow_obj)) - LOG.debug("ADD tvshow [%s/%s/%s] %s: %s", obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Title'], obj['Id']) + LOG.debug( + "ADD tvshow [%s/%s/%s] %s: %s", + obj["TopPathId"], + obj["PathId"], + obj["ShowId"], + obj["Title"], + obj["Id"], + ) - self.update_path_parent_id(obj['PathId'], obj['TopPathId']) + self.update_path_parent_id(obj["PathId"], obj["TopPathId"]) def tvshow_update(self, obj): - - ''' Update object to kodi. - ''' - obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_unique_id_tvshow_obj)) + """Update object to kodi.""" + obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_unique_id_tvshow_obj)) self.update_ratings(*values(obj, QU.update_rating_tvshow_obj)) - obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_tvshow_obj)) + obj["Unique"] = self.get_unique_id(*values(obj, QU.get_unique_id_tvshow_obj)) self.update_unique_id(*values(obj, QU.update_unique_id_tvshow_obj)) - obj['TopPathId'] = self.get_path(obj['TopLevel']) + obj["TopPathId"] = self.get_path(obj["TopLevel"]) self.update(*values(obj, QU.update_tvshow_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("UPDATE tvshow [%s/%s] %s: %s", obj['PathId'], obj['ShowId'], obj['Title'], obj['Id']) + LOG.debug( + "UPDATE tvshow [%s/%s] %s: %s", + obj["PathId"], + obj["ShowId"], + obj["Title"], + obj["Id"], + ) - self.update_path_parent_id(obj['PathId'], obj['TopPathId']) + self.update_path_parent_id(obj["PathId"], obj["TopPathId"]) def get_path_filename(self, obj): - - ''' Get the path and build it into protocol://path - ''' + """Get the path and build it into protocol://path""" if self.direct_path: - if '\\' in obj['Path']: - obj['Path'] = "%s\\" % obj['Path'] - obj['TopLevel'] = "%s\\" % dirname(dirname(obj['Path'])) - elif 'smb://' in obj['Path'] or 'nfs://' in obj['Path']: - obj['Path'] = "%s/" % obj['Path'] - obj['TopLevel'] = "%s/" % dirname(dirname(obj['Path'])) + if "\\" in obj["Path"]: + obj["Path"] = "%s\\" % obj["Path"] + obj["TopLevel"] = "%s\\" % dirname(dirname(obj["Path"])) + elif "smb://" in obj["Path"] or "nfs://" in obj["Path"]: + obj["Path"] = "%s/" % obj["Path"] + obj["TopLevel"] = "%s/" % dirname(dirname(obj["Path"])) else: - obj['Path'] = "%s/" % obj['Path'] - obj['TopLevel'] = "plugin://plugin.video.jellyfin/" + obj["Path"] = "%s/" % obj["Path"] + obj["TopLevel"] = "plugin://plugin.video.jellyfin/" - if not validate(obj['Path']): + if not validate(obj["Path"]): raise PathValidationException("Failed to validate path. User stopped.") else: - obj['TopLevel'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] - obj['Path'] = "%s%s/" % (obj['TopLevel'], obj['Id']) + obj["TopLevel"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"] + obj["Path"] = "%s%s/" % (obj["TopLevel"], obj["Id"]) @stop def season(self, item, show_id=None): + """If item does not exist, entry will be added. + If item exists, entry will be updated. - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - If the show is empty, try to remove it. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + If the show is empty, try to remove it. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Season') + obj = self.objects.map(item, "Season") - obj['ShowId'] = show_id + obj["ShowId"] = show_id - if obj['ShowId'] is None: + if obj["ShowId"] is None: try: - obj['ShowId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + obj["ShowId"] = self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_series_obj) + )[0] except (KeyError, TypeError) as error: - LOG.error("Unable to add series %s", obj['SeriesId']) + LOG.error("Unable to add series %s", obj["SeriesId"]) LOG.exception(error) return False - obj['SeasonId'] = self.get_season(*values(obj, QU.get_season_obj)) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj["SeasonId"] = self.get_season(*values(obj, QU.get_season_obj)) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) - if obj['Location'] != "Virtual": + if obj["Location"] != "Virtual": self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_season_obj)) - self.item_ids.append(obj['Id']) + self.item_ids.append(obj["Id"]) - self.artwork.add(obj['Artwork'], obj['SeasonId'], "season") - LOG.debug("UPDATE season [%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['Title'] or obj['Index'], obj['Id']) + self.artwork.add(obj["Artwork"], obj["SeasonId"], "season") + LOG.debug( + "UPDATE season [%s/%s] %s: %s", + obj["ShowId"], + obj["SeasonId"], + obj["Title"] or obj["Index"], + obj["Id"], + ) @stop @jellyfin_item def episode(self, item, e_item): + """If item does not exist, entry will be added. + If item exists, entry will be updated. - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - Create additional entry for widgets. - This is only required for plugin/episode. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + Create additional entry for widgets. + This is only required for plugin/episode. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'Episode') + obj = self.objects.map(item, "Episode") update = True - if obj['Location'] == "Virtual": - LOG.info("Skipping virtual episode %s: %s", obj['Title'], obj['Id']) + if obj["Location"] == "Virtual": + LOG.info("Skipping virtual episode %s: %s", obj["Title"], obj["Id"]) return - elif obj['SeriesId'] is None: - LOG.info("Skipping episode %s with missing SeriesId", obj['Id']) + elif obj["SeriesId"] is None: + LOG.info("Skipping episode %s with missing SeriesId", obj["Id"]) return try: - obj['EpisodeId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['PathId'] = e_item[2] + obj["EpisodeId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["PathId"] = e_item[2] except TypeError: update = False - LOG.debug("EpisodeId %s not found", obj['Id']) + LOG.debug("EpisodeId %s not found", obj["Id"]) library = self.library or find_library(self.server, item) if not library: # This item doesn't belong to a whitelisted library return - obj['EpisodeId'] = self.create_entry_episode() + obj["EpisodeId"] = self.create_entry_episode() else: if self.get_episode(*values(obj, QU.get_episode_obj)) is None: update = False - LOG.info("EpisodeId %s missing from kodi. repairing the entry.", obj['EpisodeId']) + LOG.info( + "EpisodeId %s missing from kodi. repairing the entry.", + obj["EpisodeId"], + ) - obj['Path'] = API.get_file_path(obj['Path']) - obj['Index'] = obj['Index'] or -1 - obj['Writers'] = " / ".join(obj['Writers'] or []) - obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['People'] = API.get_people_artwork(obj['People'] or []) - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj["Path"] = API.get_file_path(obj["Path"]) + obj["Index"] = obj["Index"] or -1 + obj["Writers"] = " / ".join(obj["Writers"] or []) + obj["Directors"] = " / ".join(obj["Directors"] or []) + obj["Plot"] = API.get_overview(obj["Plot"]) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["People"] = API.get_people_artwork(obj["People"] or []) + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + obj["DatePlayed"] = ( + None + if not obj["DatePlayed"] + else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) + obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork")) + obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"]) + obj["Audio"] = API.audio_streams(obj["Audio"] or []) + obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"]) self.get_episode_path_filename(obj) - if obj['Premiere']: - obj['Premiere'] = Local(obj['Premiere']).split('.')[0].replace('T', " ") + if obj["Premiere"]: + obj["Premiere"] = Local(obj["Premiere"]).split(".")[0].replace("T", " ") - if obj['Season'] is None: - if obj['AbsoluteNumber']: + if obj["Season"] is None: + if obj["AbsoluteNumber"]: - obj['Season'] = 1 - obj['Index'] = obj['AbsoluteNumber'] + obj["Season"] = 1 + obj["Index"] = obj["AbsoluteNumber"] else: - obj['Season'] = 0 + obj["Season"] = 0 - if obj['AirsAfterSeason']: + if obj["AirsAfterSeason"]: - obj['AirsBeforeSeason'] = obj['AirsAfterSeason'] - obj['AirsBeforeEpisode'] = 4096 # Kodi default number for afterseason ordering + obj["AirsBeforeSeason"] = obj["AirsAfterSeason"] + obj["AirsBeforeEpisode"] = ( + 4096 # Kodi default number for afterseason ordering + ) - if obj['MultiEpisode']: - obj['Title'] = "| %02d | %s" % (obj['MultiEpisode'], obj['Title']) + if obj["MultiEpisode"]: + obj["Title"] = "| %02d | %s" % (obj["MultiEpisode"], obj["Title"]) if not self.get_show_id(obj): return False - obj['SeasonId'] = self.get_season(*values(obj, QU.get_season_episode_obj)) + obj["SeasonId"] = self.get_season(*values(obj, QU.get_season_episode_obj)) if update: self.episode_update(obj) @@ -362,273 +420,339 @@ class TVShows(KodiDb): self.add_people(*values(obj, QU.add_people_episode_obj)) self.add_streams(*values(obj, QU.add_streams_obj)) self.add_playstate(*values(obj, QU.add_bookmark_obj)) - self.artwork.update(obj['Artwork']['Primary'], obj['EpisodeId'], "episode", "thumb") - self.item_ids.append(obj['Id']) + self.artwork.update( + obj["Artwork"]["Primary"], obj["EpisodeId"], "episode", "thumb" + ) + self.item_ids.append(obj["Id"]) - if not self.direct_path and obj['Resume']: + if not self.direct_path and obj["Resume"]: temp_obj = dict(obj) - temp_obj['Path'] = "plugin://plugin.video.jellyfin/" - temp_obj['PathId'] = self.get_path(*values(temp_obj, QU.get_path_obj)) - temp_obj['FileId'] = self.add_file(*values(temp_obj, QU.add_file_obj)) + temp_obj["Path"] = "plugin://plugin.video.jellyfin/" + temp_obj["PathId"] = self.get_path(*values(temp_obj, QU.get_path_obj)) + temp_obj["FileId"] = self.add_file(*values(temp_obj, QU.add_file_obj)) self.update_file(*values(temp_obj, QU.update_file_obj)) self.add_playstate(*values(temp_obj, QU.add_bookmark_obj)) return not update def episode_add(self, obj): - - ''' Add object to kodi. - ''' - obj['RatingId'] = self.create_entry_rating() + """Add object to kodi.""" + obj["RatingId"] = self.create_entry_rating() self.add_ratings(*values(obj, QU.add_rating_episode_obj)) - obj['Unique'] = self.create_entry_unique_id() + obj["Unique"] = self.create_entry_unique_id() self.add_unique_id(*values(obj, QU.add_unique_id_episode_obj)) - obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) - obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj)) + obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj)) try: self.add_episode(*values(obj, QU.add_episode_obj)) except sqlite3.IntegrityError: LOG.error("IntegrityError for %s", obj) - obj['EpisodeId'] = self.create_entry_episode() + obj["EpisodeId"] = self.create_entry_episode() return self.episode_add(obj) self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_episode_obj)) - parentPathId = self.jellyfin_db.get_episode_kodi_parent_path_id(*values(obj, QUEM.get_episode_kodi_parent_path_id_obj)) - if obj['PathId'] != parentPathId: - LOG.debug("Setting episode pathParentId, episode %s, title %s, pathId %s, pathParentId %s", obj['Id'], obj['Title'], obj['PathId'], parentPathId) - self.update_path_parent_id(obj['PathId'], parentPathId) + parentPathId = self.jellyfin_db.get_episode_kodi_parent_path_id( + *values(obj, QUEM.get_episode_kodi_parent_path_id_obj) + ) + if obj["PathId"] != parentPathId: + LOG.debug( + "Setting episode pathParentId, episode %s, title %s, pathId %s, pathParentId %s", + obj["Id"], + obj["Title"], + obj["PathId"], + parentPathId, + ) + self.update_path_parent_id(obj["PathId"], parentPathId) - LOG.debug("ADD episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) + LOG.debug( + "ADD episode [%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["Id"], + obj["Title"], + ) def episode_update(self, obj): - - ''' Update object to kodi. - ''' - obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_episode_obj)) + """Update object to kodi.""" + obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_rating_episode_obj)) self.update_ratings(*values(obj, QU.update_rating_episode_obj)) - obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_episode_obj)) + obj["Unique"] = self.get_unique_id(*values(obj, QU.get_unique_id_episode_obj)) self.update_unique_id(*values(obj, QU.update_unique_id_episode_obj)) self.update_episode(*values(obj, QU.update_episode_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) self.jellyfin_db.update_parent_id(*values(obj, QUEM.update_parent_episode_obj)) - LOG.debug("UPDATE episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) + LOG.debug( + "UPDATE episode [%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["Id"], + obj["Title"], + ) def get_episode_path_filename(self, obj): - - ''' Get the path and build it into protocol://path - ''' - if '\\' in obj['Path']: - obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] + """Get the path and build it into protocol://path""" + if "\\" in obj["Path"]: + obj["Filename"] = obj["Path"].rsplit("\\", 1)[1] else: - obj['Filename'] = obj['Path'].rsplit('/', 1)[1] + obj["Filename"] = obj["Path"].rsplit("/", 1)[1] if self.direct_path: - if not validate(obj['Path']): + if not validate(obj["Path"]): raise PathValidationException("Failed to validate path. User stopped.") - obj['Path'] = obj['Path'].replace(obj['Filename'], "") + obj["Path"] = obj["Path"].replace(obj["Filename"], "") - '''check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO''' - if validate_dvd_dir(obj['Path'] + obj['Filename']): - obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/' - obj['Filename'] = 'VIDEO_TS.IFO' - LOG.debug("DVD directory %s", obj['Path']) + """check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO""" + if validate_dvd_dir(obj["Path"] + obj["Filename"]): + obj["Path"] = obj["Path"] + obj["Filename"] + "/VIDEO_TS/" + obj["Filename"] = "VIDEO_TS.IFO" + LOG.debug("DVD directory %s", obj["Path"]) - '''check bluray directories and point it to ./BDMV/index.bdmv''' - if validate_bluray_dir(obj['Path'] + obj['Filename']): - obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/' - obj['Filename'] = 'index.bdmv' - LOG.debug("Bluray directory %s", obj['Path']) + """check bluray directories and point it to ./BDMV/index.bdmv""" + if validate_bluray_dir(obj["Path"] + obj["Filename"]): + obj["Path"] = obj["Path"] + obj["Filename"] + "/BDMV/" + obj["Filename"] = "index.bdmv" + LOG.debug("Bluray directory %s", obj["Path"]) - obj['FullFilePath'] = obj['Path'] + obj['Filename'] + obj["FullFilePath"] = obj["Path"] + obj["Filename"] else: # We need LibraryId library = self.library or find_library(self.server, obj) - obj['LibraryId'] = library['Id'] - obj['Path'] = "plugin://plugin.video.jellyfin/%s/%s/" % (obj['LibraryId'], obj['SeriesId']) + obj["LibraryId"] = library["Id"] + obj["Path"] = "plugin://plugin.video.jellyfin/%s/%s/" % ( + obj["LibraryId"], + obj["SeriesId"], + ) params = { - 'filename': py2_encode(obj['Filename'], 'utf-8'), - 'id': obj['Id'], - 'dbid': obj['EpisodeId'], - 'mode': "play" + "filename": py2_encode(obj["Filename"], "utf-8"), + "id": obj["Id"], + "dbid": obj["EpisodeId"], + "mode": "play", } - obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params)) - obj['FullFilePath'] = obj['Filename'] + obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params)) + obj["FullFilePath"] = obj["Filename"] def get_show_id(self, obj): - obj['ShowId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj)) + obj["ShowId"] = self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_series_obj) + ) - if obj['ShowId'] is None: + if obj["ShowId"] is None: try: - self.tvshow(self.server.jellyfin.get_item(obj['SeriesId'])) - obj['ShowId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + self.tvshow(self.server.jellyfin.get_item(obj["SeriesId"])) + obj["ShowId"] = self.jellyfin_db.get_item_by_id( + *values(obj, QUEM.get_item_series_obj) + )[0] except (TypeError, KeyError) as error: - LOG.error("Unable to add series %s", obj['SeriesId']) + LOG.error("Unable to add series %s", obj["SeriesId"]) LOG.exception(error) return False else: - obj['ShowId'] = obj['ShowId'][0] + obj["ShowId"] = obj["ShowId"][0] - self.item_ids.append(obj['SeriesId']) + self.item_ids.append(obj["SeriesId"]) return True @stop @jellyfin_item def userdata(self, item, e_item): + """This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - - Make sure there's no other bookmarks created by widget. - Create additional entry for widgets. This is only required for plugin/episode. - ''' - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] + Make sure there's no other bookmarks created by widget. + Create additional entry for widgets. This is only required for plugin/episode. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] API = api.API(item, server_address) - obj = self.objects.map(item, 'EpisodeUserData') + obj = self.objects.map(item, "EpisodeUserData") try: - obj['KodiId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["Media"] = e_item[4] except TypeError: return - if obj['Media'] == "tvshow": + if obj["Media"] == "tvshow": - if obj['Favorite']: + if obj["Favorite"]: self.get_tag(*values(obj, QU.get_tag_episode_obj)) else: self.remove_tag(*values(obj, QU.delete_tag_episode_obj)) - elif obj['Media'] == "episode": + elif obj["Media"] == "episode": - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) - obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) - if obj['DatePlayed']: - obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + if obj["DatePlayed"]: + obj["DatePlayed"] = ( + Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) - if obj['DateAdded']: - obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + if obj["DateAdded"]: + obj["DateAdded"] = ( + Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + ) self.add_playstate(*values(obj, QU.add_bookmark_obj)) - if not self.direct_path and not obj['Resume']: + if not self.direct_path and not obj["Resume"]: temp_obj = dict(obj) - temp_obj['Filename'] = self.get_filename(*values(temp_obj, QU.get_file_obj)) - temp_obj['Path'] = "plugin://plugin.video.jellyfin/" + temp_obj["Filename"] = self.get_filename( + *values(temp_obj, QU.get_file_obj) + ) + temp_obj["Path"] = "plugin://plugin.video.jellyfin/" self.remove_file(*values(temp_obj, QU.delete_file_obj)) - elif not self.direct_path and obj['Resume']: + elif not self.direct_path and obj["Resume"]: temp_obj = dict(obj) - temp_obj['Filename'] = self.get_filename(*values(temp_obj, QU.get_file_obj)) - temp_obj['PathId'] = self.get_path("plugin://plugin.video.jellyfin/") - temp_obj['FileId'] = self.add_file(*values(temp_obj, QU.add_file_obj)) + temp_obj["Filename"] = self.get_filename( + *values(temp_obj, QU.get_file_obj) + ) + temp_obj["PathId"] = self.get_path("plugin://plugin.video.jellyfin/") + temp_obj["FileId"] = self.add_file(*values(temp_obj, QU.add_file_obj)) self.update_file(*values(temp_obj, QU.update_file_obj)) self.add_playstate(*values(temp_obj, QU.add_bookmark_obj)) self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) - LOG.debug("USERDATA %s [%s/%s] %s: %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'], obj['Title']) + LOG.debug( + "USERDATA %s [%s/%s] %s: %s", + obj["Media"], + obj["FileId"], + obj["KodiId"], + obj["Id"], + obj["Title"], + ) @stop @jellyfin_item def remove(self, item_id, e_item): - - ''' Remove showid, fileid, pathid, jellyfin reference. - There's no episodes left, delete show and any possible remaining seasons - ''' - obj = {'Id': item_id} + """Remove showid, fileid, pathid, jellyfin reference. + There's no episodes left, delete show and any possible remaining seasons + """ + obj = {"Id": item_id} try: - obj['KodiId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['ParentId'] = e_item[3] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["ParentId"] = e_item[3] + obj["Media"] = e_item[4] except TypeError: return - if obj['Media'] == 'episode': + if obj["Media"] == "episode": temp_obj = dict(obj) - self.remove_episode(obj['KodiId'], obj['FileId'], obj['Id']) - season = self.jellyfin_db.get_full_item_by_kodi_id(*values(obj, QUEM.delete_item_by_parent_season_obj)) + self.remove_episode(obj["KodiId"], obj["FileId"], obj["Id"]) + season = self.jellyfin_db.get_full_item_by_kodi_id( + *values(obj, QUEM.delete_item_by_parent_season_obj) + ) try: - temp_obj['Id'] = season[0] - temp_obj['ParentId'] = season[1] + temp_obj["Id"] = season[0] + temp_obj["ParentId"] = season[1] except TypeError: return - if not self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_episode_obj)): + if not self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_episode_obj) + ): - self.remove_season(obj['ParentId'], obj['Id']) + self.remove_season(obj["ParentId"], obj["Id"]) self.jellyfin_db.remove_item(*values(temp_obj, QUEM.delete_item_obj)) - temp_obj['Id'] = self.jellyfin_db.get_item_by_kodi_id(*values(temp_obj, QUEM.get_item_by_parent_tvshow_obj)) + temp_obj["Id"] = self.jellyfin_db.get_item_by_kodi_id( + *values(temp_obj, QUEM.get_item_by_parent_tvshow_obj) + ) - if not self.get_total_episodes(*values(temp_obj, QU.get_total_episodes_obj)): + if not self.get_total_episodes( + *values(temp_obj, QU.get_total_episodes_obj) + ): - for season in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_season_obj)): - self.remove_season(season[1], obj['Id']) + for season in self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_season_obj) + ): + self.remove_season(season[1], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_season_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(temp_obj, QUEM.delete_item_by_parent_season_obj) + ) - self.remove_tvshow(temp_obj['ParentId'], obj['Id']) + self.remove_tvshow(temp_obj["ParentId"], obj["Id"]) self.jellyfin_db.remove_item(*values(temp_obj, QUEM.delete_item_obj)) - elif obj['Media'] == 'tvshow': - obj['ParentId'] = obj['KodiId'] + elif obj["Media"] == "tvshow": + obj["ParentId"] = obj["KodiId"] - for season in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): + for season in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_season_obj) + ): temp_obj = dict(obj) - temp_obj['ParentId'] = season[1] + temp_obj["ParentId"] = season[1] - for episode in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_episode_obj)): - self.remove_episode(episode[1], episode[2], obj['Id']) + for episode in self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_episode_obj) + ): + self.remove_episode(episode[1], episode[2], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_episode_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(temp_obj, QUEM.delete_item_by_parent_episode_obj) + ) else: - self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_season_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(obj, QUEM.delete_item_by_parent_season_obj) + ) - self.remove_tvshow(obj['KodiId'], obj['Id']) + self.remove_tvshow(obj["KodiId"], obj["Id"]) - elif obj['Media'] == 'season': + elif obj["Media"] == "season": - for episode in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_episode_obj)): - self.remove_episode(episode[1], episode[2], obj['Id']) + for episode in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_episode_obj) + ): + self.remove_episode(episode[1], episode[2], obj["Id"]) else: - self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_episode_obj)) + self.jellyfin_db.remove_items_by_parent_id( + *values(obj, QUEM.delete_item_by_parent_episode_obj) + ) - self.remove_season(obj['KodiId'], obj['Id']) + self.remove_season(obj["KodiId"], obj["Id"]) - if not self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.delete_item_by_parent_season_obj)): + if not self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.delete_item_by_parent_season_obj) + ): - self.remove_tvshow(obj['ParentId'], obj['Id']) - self.jellyfin_db.remove_item_by_kodi_id(*values(obj, QUEM.delete_item_by_parent_tvshow_obj)) + self.remove_tvshow(obj["ParentId"], obj["Id"]) + self.jellyfin_db.remove_item_by_kodi_id( + *values(obj, QUEM.delete_item_by_parent_tvshow_obj) + ) # Remove any series pooling episodes - for episode in self.jellyfin_db.get_media_by_parent_id(obj['Id']): - self.remove_episode(episode[2], episode[3], obj['Id']) + for episode in self.jellyfin_db.get_media_by_parent_id(obj["Id"]): + self.remove_episode(episode[2], episode[3], obj["Id"]) else: - self.jellyfin_db.remove_media_by_parent_id(obj['Id']) + self.jellyfin_db.remove_media_by_parent_id(obj["Id"]) self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) @@ -652,32 +776,34 @@ class TVShows(KodiDb): @jellyfin_item def get_child(self, item_id, e_item): - - ''' Get all child elements from tv show jellyfin id. - ''' - obj = {'Id': item_id} + """Get all child elements from tv show jellyfin id.""" + obj = {"Id": item_id} child = [] try: - obj['KodiId'] = e_item[0] - obj['FileId'] = e_item[1] - obj['ParentId'] = e_item[3] - obj['Media'] = e_item[4] + obj["KodiId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["ParentId"] = e_item[3] + obj["Media"] = e_item[4] except TypeError: return child - obj['ParentId'] = obj['KodiId'] + obj["ParentId"] = obj["KodiId"] - for season in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): + for season in self.jellyfin_db.get_item_by_parent_id( + *values(obj, QUEM.get_item_by_parent_season_obj) + ): temp_obj = dict(obj) - temp_obj['ParentId'] = season[1] + temp_obj["ParentId"] = season[1] child.append(season[0]) - for episode in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_episode_obj)): + for episode in self.jellyfin_db.get_item_by_parent_id( + *values(temp_obj, QUEM.get_item_by_parent_episode_obj) + ): child.append(episode[0]) - for episode in self.jellyfin_db.get_media_by_parent_id(obj['Id']): + for episode in self.jellyfin_db.get_media_by_parent_id(obj["Id"]): child.append(episode[0]) return child diff --git a/jellyfin_kodi/objects/utils.py b/jellyfin_kodi/objects/utils.py index 31bbf7d1..1aec0141 100644 --- a/jellyfin_kodi/objects/utils.py +++ b/jellyfin_kodi/objects/utils.py @@ -14,8 +14,8 @@ LOG = LazyLogger(__name__) def get_grouped_set(): - - ''' Get if boxsets should be grouped - ''' - result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) - return result.get('result', {}).get('value', False) + """Get if boxsets should be grouped""" + result = JSONRPC("Settings.GetSettingValue").execute( + {"setting": "videolibrary.groupmoviesets"} + ) + return result.get("result", {}).get("value", False) diff --git a/jellyfin_kodi/player.py b/jellyfin_kodi/player.py index bb6a8bea..ac709208 100644 --- a/jellyfin_kodi/player.py +++ b/jellyfin_kodi/player.py @@ -44,11 +44,10 @@ class Player(xbmc.Player): return file in self.played def onPlayBackStarted(self): - - ''' We may need to wait for info to be set in kodi monitor. - Accounts for scenario where Kodi starts playback and exits immediately. - First, ensure previous playback terminated correctly in Jellyfin. - ''' + """We may need to wait for info to be set in kodi monitor. + Accounts for scenario where Kodi starts playback and exits immediately. + First, ensure previous playback terminated correctly in Jellyfin. + """ self.stop_playback() self.up_next = False count = 0 @@ -69,11 +68,11 @@ class Player(xbmc.Player): if monitor.waitForAbort(1): return else: - LOG.info('Cancel playback report') + LOG.info("Cancel playback report") return - items = window('jellyfin_play.json') + items = window("jellyfin_play.json") item = None while not items: @@ -81,7 +80,7 @@ class Player(xbmc.Player): if monitor.waitForAbort(2): return - items = window('jellyfin_play.json') + items = window("jellyfin_play.json") count += 1 if count == 20: @@ -90,51 +89,49 @@ class Player(xbmc.Player): return for item in items: - if item['Path'] == current_file: + if item["Path"] == current_file: items.pop(items.index(item)) break else: item = items.pop(0) - window('jellyfin_play.json', items) + window("jellyfin_play.json", items) self.set_item(current_file, item) data = { - 'QueueableMediaTypes': "Video,Audio", - 'CanSeek': True, - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PlayMethod': item['PlayMethod'], - 'VolumeLevel': item['Volume'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'IsPaused': item['Paused'], - 'IsMuted': item['Muted'], - 'PlaySessionId': item['PlaySessionId'], - 'AudioStreamIndex': item['AudioStreamIndex'], - 'SubtitleStreamIndex': item['SubtitleStreamIndex'] + "QueueableMediaTypes": "Video,Audio", + "CanSeek": True, + "ItemId": item["Id"], + "MediaSourceId": item["MediaSourceId"], + "PlayMethod": item["PlayMethod"], + "VolumeLevel": item["Volume"], + "PositionTicks": int(item["CurrentPosition"] * 10000000), + "IsPaused": item["Paused"], + "IsMuted": item["Muted"], + "PlaySessionId": item["PlaySessionId"], + "AudioStreamIndex": item["AudioStreamIndex"], + "SubtitleStreamIndex": item["SubtitleStreamIndex"], } - item['Server'].jellyfin.session_playing(data) - window('jellyfin.skip.%s.bool' % item['Id'], True) + item["Server"].jellyfin.session_playing(data) + window("jellyfin.skip.%s.bool" % item["Id"], True) if monitor.waitForAbort(2): return - if item['PlayOption'] == 'Addon': - self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex']) + if item["PlayOption"] == "Addon": + self.set_audio_subs(item["AudioStreamIndex"], item["SubtitleStreamIndex"]) def set_item(self, file, item): - - ''' Set playback information. - ''' + """Set playback information.""" try: - item['Runtime'] = int(item['Runtime']) + item["Runtime"] = int(item["Runtime"]) except (TypeError, ValueError): try: - item['Runtime'] = int(self.getTotalTime()) - LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime']) + item["Runtime"] = int(self.getTotalTime()) + LOG.info("Runtime is missing, Kodi runtime: %s" % item["Runtime"]) except Exception: - item['Runtime'] = 0 + item["Runtime"] = 0 LOG.info("Runtime is missing, Using Zero") try: @@ -142,22 +139,26 @@ class Player(xbmc.Player): except Exception: # at this point we should be playing and if not then bail out return - result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) - result = result.get('result', {}) - volume = result.get('volume') - muted = result.get('muted') + result = JSONRPC("Application.GetProperties").execute( + {"properties": ["volume", "muted"]} + ) + result = result.get("result", {}) + volume = result.get("volume") + muted = result.get("muted") - item.update({ - 'File': file, - 'CurrentPosition': item.get('CurrentPosition') or int(seektime), - 'Muted': muted, - 'Volume': volume, - 'Server': Jellyfin(item['ServerId']).get_client(), - 'Paused': False - }) + item.update( + { + "File": file, + "CurrentPosition": item.get("CurrentPosition") or int(seektime), + "Muted": muted, + "Volume": volume, + "Server": Jellyfin(item["ServerId"]).get_client(), + "Paused": False, + } + ) self.played[file] = item - LOG.info("-->[ play/%s ] %s", item['Id'], item) + LOG.info("-->[ play/%s ] %s", item["Id"], item) def set_audio_subs(self, audio=None, subtitle=None): if audio: @@ -165,15 +166,15 @@ class Player(xbmc.Player): if subtitle: subtitle = int(subtitle) - ''' Only for after playback started - ''' + """ Only for after playback started + """ LOG.info("Setting audio: %s subs: %s", audio, subtitle) current_file = self.get_playing_file() if self.is_playing_file(current_file): item = self.get_file_info(current_file) - mapping = item['SubsMapping'] + mapping = item["SubsMapping"] if audio and len(self.getAvailableAudioStreams()) > 1: self.setAudioStream(audio - 1) @@ -200,82 +201,90 @@ class Player(xbmc.Player): def detect_audio_subs(self, item): params = { - 'playerid': 1, - 'properties': ["currentsubtitle", "currentaudiostream", "subtitleenabled"] + "playerid": 1, + "properties": ["currentsubtitle", "currentaudiostream", "subtitleenabled"], } - result = JSONRPC('Player.GetProperties').execute(params) - result = result.get('result') + result = JSONRPC("Player.GetProperties").execute(params) + result = result.get("result") try: # Audio tracks - audio = result['currentaudiostream']['index'] + audio = result["currentaudiostream"]["index"] except (KeyError, TypeError): audio = 0 try: # Subtitles tracks - subs = result['currentsubtitle']['index'] + subs = result["currentsubtitle"]["index"] except (KeyError, TypeError): subs = 0 try: # If subtitles are enabled - subs_enabled = result['subtitleenabled'] + subs_enabled = result["subtitleenabled"] except (KeyError, TypeError): subs_enabled = False - item['AudioStreamIndex'] = audio + 1 + item["AudioStreamIndex"] = audio + 1 if not subs_enabled or not len(self.getAvailableSubtitleStreams()): - item['SubtitleStreamIndex'] = None + item["SubtitleStreamIndex"] = None return - mapping = item['SubsMapping'] + mapping = item["SubsMapping"] tracks = len(self.getAvailableAudioStreams()) if mapping: if str(subs) in mapping: - item['SubtitleStreamIndex'] = mapping[str(subs)] + item["SubtitleStreamIndex"] = mapping[str(subs)] else: - item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1 + item["SubtitleStreamIndex"] = subs - len(mapping) + tracks + 1 else: - item['SubtitleStreamIndex'] = subs + tracks + 1 + item["SubtitleStreamIndex"] = subs + tracks + 1 def next_up(self): item = self.get_file_info(self.get_playing_file()) objects = Objects() - if item['Type'] != 'Episode' or not item.get('CurrentEpisode'): + if item["Type"] != "Episode" or not item.get("CurrentEpisode"): return - next_items = item['Server'].jellyfin.get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id']) + next_items = item["Server"].jellyfin.get_adjacent_episodes( + item["CurrentEpisode"]["tvshowid"], item["Id"] + ) - for index, next_item in enumerate(next_items['Items']): - if next_item['Id'] == item['Id']: + for index, next_item in enumerate(next_items["Items"]): + if next_item["Id"] == item["Id"]: try: - next_item = next_items['Items'][index + 1] + next_item = next_items["Items"][index + 1] except IndexError: LOG.warning("No next up episode.") return break - server_address = item['Server'].auth.get_server_info(item['Server'].auth.server_id)['address'] + server_address = item["Server"].auth.get_server_info( + item["Server"].auth.server_id + )["address"] API = api.API(next_item, server_address) data = objects.map(next_item, "UpNext") - artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True) - data['art'] = { - 'tvshow.poster': artwork.get('Series.Primary'), - 'tvshow.fanart': None, - 'thumb': artwork.get('Primary') + artwork = API.get_all_artwork(objects.map(next_item, "ArtworkParent"), True) + data["art"] = { + "tvshow.poster": artwork.get("Series.Primary"), + "tvshow.fanart": None, + "thumb": artwork.get("Primary"), } - if artwork['Backdrop']: - data['art']['tvshow.fanart'] = artwork['Backdrop'][0] + if artwork["Backdrop"]: + data["art"]["tvshow.fanart"] = artwork["Backdrop"][0] next_info = { - 'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'}, - 'current_episode': item['CurrentEpisode'], - 'next_episode': data + "play_info": { + "ItemIds": [data["episodeid"]], + "ServerId": item["ServerId"], + "PlayCommand": "PlayNow", + }, + "current_episode": item["CurrentEpisode"], + "next_episode": data, } LOG.info("--[ next up ] %s", next_info) @@ -286,7 +295,7 @@ class Player(xbmc.Player): if self.is_playing_file(current_file): - self.get_file_info(current_file)['Paused'] = True + self.get_file_info(current_file)["Paused"] = True self.report_playback() LOG.debug("-->[ paused ]") @@ -295,24 +304,21 @@ class Player(xbmc.Player): if self.is_playing_file(current_file): - self.get_file_info(current_file)['Paused'] = False + self.get_file_info(current_file)["Paused"] = False self.report_playback() LOG.debug("--<[ paused ]") def onPlayBackSeek(self, time, seek_offset): - - ''' Does not seem to work in Leia?? - ''' + """Does not seem to work in Leia??""" if self.is_playing_file(self.get_playing_file()): self.report_playback() LOG.info("--[ seek ]") def report_playback(self, report=True): - - ''' Report playback progress to jellyfin server. - Check if the user seek. - ''' + """Report playback progress to jellyfin server. + Check if the user seek. + """ current_file = self.get_playing_file() if not self.is_playing_file(current_file): @@ -320,25 +326,29 @@ class Player(xbmc.Player): item = self.get_file_info(current_file) - if window('jellyfin.external.bool'): + if window("jellyfin.external.bool"): return if not report: - previous = item['CurrentPosition'] + previous = item["CurrentPosition"] try: - item['CurrentPosition'] = int(self.getTime()) + item["CurrentPosition"] = int(self.getTime()) except Exception as e: # getTime() raises RuntimeError if nothing is playing LOG.debug("Failed to get playback position: %s", e) return - if int(item['CurrentPosition']) == 1: + if int(item["CurrentPosition"]) == 1: return try: - played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100 + played = ( + float(item["CurrentPosition"] * 10000000) + / int(item["Runtime"]) + * 100 + ) except ZeroDivisionError: # Runtime is 0. played = 0 @@ -347,52 +357,48 @@ class Player(xbmc.Player): self.up_next = True self.next_up() - if (item['CurrentPosition'] - previous) < 30: + if (item["CurrentPosition"] - previous) < 30: return - result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) - result = result.get('result', {}) - item['Volume'] = result.get('volume') - item['Muted'] = result.get('muted') - item['CurrentPosition'] = int(self.getTime()) + result = JSONRPC("Application.GetProperties").execute( + {"properties": ["volume", "muted"]} + ) + result = result.get("result", {}) + item["Volume"] = result.get("volume") + item["Muted"] = result.get("muted") + item["CurrentPosition"] = int(self.getTime()) self.detect_audio_subs(item) data = { - 'QueueableMediaTypes': "Video,Audio", - 'CanSeek': True, - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PlayMethod': item['PlayMethod'], - 'VolumeLevel': item['Volume'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'IsPaused': item['Paused'], - 'IsMuted': item['Muted'], - 'PlaySessionId': item['PlaySessionId'], - 'AudioStreamIndex': item['AudioStreamIndex'], - 'SubtitleStreamIndex': item['SubtitleStreamIndex'] + "QueueableMediaTypes": "Video,Audio", + "CanSeek": True, + "ItemId": item["Id"], + "MediaSourceId": item["MediaSourceId"], + "PlayMethod": item["PlayMethod"], + "VolumeLevel": item["Volume"], + "PositionTicks": int(item["CurrentPosition"] * 10000000), + "IsPaused": item["Paused"], + "IsMuted": item["Muted"], + "PlaySessionId": item["PlaySessionId"], + "AudioStreamIndex": item["AudioStreamIndex"], + "SubtitleStreamIndex": item["SubtitleStreamIndex"], } - item['Server'].jellyfin.session_progress(data) + item["Server"].jellyfin.session_progress(data) def onPlayBackStopped(self): - - ''' Will be called when user stops playing a file. - ''' - window('jellyfin_play', clear=True) + """Will be called when user stops playing a file.""" + window("jellyfin_play", clear=True) self.stop_playback() LOG.info("--<[ playback ]") def onPlayBackEnded(self): - - ''' Will be called when kodi stops playing a file. - ''' + """Will be called when kodi stops playing a file.""" self.stop_playback() LOG.info("--<<[ playback ]") def stop_playback(self): - - ''' Stop all playback. Check for external player for positionticks. - ''' + """Stop all playback. Check for external player for positionticks.""" if not self.played: return @@ -401,61 +407,67 @@ class Player(xbmc.Player): for file in self.played: item = self.get_file_info(file) - window('jellyfin.skip.%s.bool' % item['Id'], True) + window("jellyfin.skip.%s.bool" % item["Id"], True) - if window('jellyfin.external.bool'): - window('jellyfin.external', clear=True) + if window("jellyfin.external.bool"): + window("jellyfin.external", clear=True) - if int(item['CurrentPosition']) == 1: - item['CurrentPosition'] = int(item['Runtime']) + if int(item["CurrentPosition"]) == 1: + item["CurrentPosition"] = int(item["Runtime"]) data = { - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'PlaySessionId': item['PlaySessionId'] + "ItemId": item["Id"], + "MediaSourceId": item["MediaSourceId"], + "PositionTicks": int(item["CurrentPosition"] * 10000000), + "PlaySessionId": item["PlaySessionId"], } - item['Server'].jellyfin.session_stop(data) + item["Server"].jellyfin.session_stop(data) - if item.get('LiveStreamId'): + if item.get("LiveStreamId"): - LOG.info("<[ livestream/%s ]", item['LiveStreamId']) - item['Server'].jellyfin.close_live_stream(item['LiveStreamId']) + LOG.info("<[ livestream/%s ]", item["LiveStreamId"]) + item["Server"].jellyfin.close_live_stream(item["LiveStreamId"]) - elif item['PlayMethod'] == 'Transcode': + elif item["PlayMethod"] == "Transcode": - LOG.info("<[ transcode/%s ]", item['Id']) - item['Server'].jellyfin.close_transcode(item['DeviceId'], item['PlaySessionId']) + LOG.info("<[ transcode/%s ]", item["Id"]) + item["Server"].jellyfin.close_transcode( + item["DeviceId"], item["PlaySessionId"] + ) - path = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/") + path = translate_path( + "special://profile/addon_data/plugin.video.jellyfin/temp/" + ) if xbmcvfs.exists(path): dirs, files = xbmcvfs.listdir(path) for file in files: # Only delete the cached files for the previous play session - if item['Id'] in file: + if item["Id"] in file: xbmcvfs.delete(os.path.join(path, file)) - result = item['Server'].jellyfin.get_item(item['Id']) or {} + result = item["Server"].jellyfin.get_item(item["Id"]) or {} - if 'UserData' in result and result['UserData']['Played']: + if "UserData" in result and result["UserData"]["Played"]: delete = False - if result['Type'] == 'Episode' and settings('deleteTV.bool'): + if result["Type"] == "Episode" and settings("deleteTV.bool"): delete = True - elif result['Type'] == 'Movie' and settings('deleteMovies.bool'): + elif result["Type"] == "Movie" and settings("deleteMovies.bool"): delete = True - if not settings('offerDelete.bool'): + if not settings("offerDelete.bool"): delete = False if delete: LOG.info("Offer delete option") - if dialog("yesno", translate(30091), translate(33015), autoclose=120000): - item['Server'].jellyfin.delete_item(item['Id']) + if dialog( + "yesno", translate(30091), translate(33015), autoclose=120000 + ): + item["Server"].jellyfin.delete_item(item["Id"]) - window('jellyfin.external_check', clear=True) + window("jellyfin.external_check", clear=True) self.played.clear() diff --git a/jellyfin_kodi/views.py b/jellyfin_kodi/views.py index 8569cb1e..00eea84f 100644 --- a/jellyfin_kodi/views.py +++ b/jellyfin_kodi/views.py @@ -19,86 +19,86 @@ from .helper.utils import translate_path LOG = LazyLogger(__name__) NODES = { - 'tvshows': [ - ('all', None), - ('recent', translate(30170)), - ('recentepisodes', translate(30175)), - ('inprogress', translate(30171)), - ('inprogressepisodes', translate(30178)), - ('nextepisodes', translate(30179)), - ('genres', 135), - ('random', translate(30229)), - ('recommended', translate(30230)) + "tvshows": [ + ("all", None), + ("recent", translate(30170)), + ("recentepisodes", translate(30175)), + ("inprogress", translate(30171)), + ("inprogressepisodes", translate(30178)), + ("nextepisodes", translate(30179)), + ("genres", 135), + ("random", translate(30229)), + ("recommended", translate(30230)), ], - 'movies': [ - ('all', None), - ('recent', translate(30174)), - ('inprogress', translate(30177)), - ('unwatched', translate(30189)), - ('sets', 20434), - ('genres', 135), - ('random', translate(30229)), - ('recommended', translate(30230)) + "movies": [ + ("all", None), + ("recent", translate(30174)), + ("inprogress", translate(30177)), + ("unwatched", translate(30189)), + ("sets", 20434), + ("genres", 135), + ("random", translate(30229)), + ("recommended", translate(30230)), + ], + "musicvideos": [ + ("all", None), + ("recent", translate(30256)), + ("inprogress", translate(30257)), + ("unwatched", translate(30258)), ], - 'musicvideos': [ - ('all', None), - ('recent', translate(30256)), - ('inprogress', translate(30257)), - ('unwatched', translate(30258)) - ] } DYNNODES = { - 'tvshows': [ - ('all', None), - ('RecentlyAdded', translate(30170)), - ('recentepisodes', translate(30175)), - ('InProgress', translate(30171)), - ('inprogressepisodes', translate(30178)), - ('nextepisodes', translate(30179)), - ('Genres', translate(135)), - ('Random', translate(30229)), - ('recommended', translate(30230)) + "tvshows": [ + ("all", None), + ("RecentlyAdded", translate(30170)), + ("recentepisodes", translate(30175)), + ("InProgress", translate(30171)), + ("inprogressepisodes", translate(30178)), + ("nextepisodes", translate(30179)), + ("Genres", translate(135)), + ("Random", translate(30229)), + ("recommended", translate(30230)), ], - 'movies': [ - ('all', None), - ('RecentlyAdded', translate(30174)), - ('InProgress', translate(30177)), - ('Boxsets', translate(20434)), - ('Favorite', translate(33168)), - ('FirstLetter', translate(33171)), - ('Genres', translate(135)), - ('Random', translate(30229)), + "movies": [ + ("all", None), + ("RecentlyAdded", translate(30174)), + ("InProgress", translate(30177)), + ("Boxsets", translate(20434)), + ("Favorite", translate(33168)), + ("FirstLetter", translate(33171)), + ("Genres", translate(135)), + ("Random", translate(30229)), # ('Recommended', translate(30230)) ], - 'musicvideos': [ - ('all', None), - ('RecentlyAdded', translate(30256)), - ('InProgress', translate(30257)), - ('Unwatched', translate(30258)) + "musicvideos": [ + ("all", None), + ("RecentlyAdded", translate(30256)), + ("InProgress", translate(30257)), + ("Unwatched", translate(30258)), ], - 'homevideos': [ - ('all', None), - ('RecentlyAdded', translate(33167)), - ('InProgress', translate(33169)), - ('Favorite', translate(33168)) + "homevideos": [ + ("all", None), + ("RecentlyAdded", translate(33167)), + ("InProgress", translate(33169)), + ("Favorite", translate(33168)), ], - 'books': [ - ('all', None), - ('RecentlyAdded', translate(33167)), - ('InProgress', translate(33169)), - ('Favorite', translate(33168)) + "books": [ + ("all", None), + ("RecentlyAdded", translate(33167)), + ("InProgress", translate(33169)), + ("Favorite", translate(33168)), ], - 'audiobooks': [ - ('all', None), - ('RecentlyAdded', translate(33167)), - ('InProgress', translate(33169)), - ('Favorite', translate(33168)) + "audiobooks": [ + ("all", None), + ("RecentlyAdded", translate(33167)), + ("InProgress", translate(33169)), + ("Favorite", translate(33168)), + ], + "music": [ + ("all", None), + ("RecentlyAdded", translate(33167)), + ("Favorite", translate(33168)), ], - 'music': [ - ('all', None), - ('RecentlyAdded', translate(33167)), - ('Favorite', translate(33168)) - ] } ################################################################################################# @@ -116,17 +116,15 @@ class Views(object): self.server = Jellyfin() def add_library(self, view): - - ''' Add entry to view table in jellyfin database. - ''' - with Database('jellyfin') as jellyfindb: - jellyfin_db.JellyfinDatabase(jellyfindb.cursor).add_view(view['Id'], view['Name'], view['Media']) + """Add entry to view table in jellyfin database.""" + with Database("jellyfin") as jellyfindb: + jellyfin_db.JellyfinDatabase(jellyfindb.cursor).add_view( + view["Id"], view["Name"], view["Media"] + ) def remove_library(self, view_id): - - ''' Remove entry from view table in jellyfin database. - ''' - with Database('jellyfin') as jellyfindb: + """Remove entry from view table in jellyfin database.""" + with Database("jellyfin") as jellyfindb: jellyfin_db.JellyfinDatabase(jellyfindb.cursor).remove_view(view_id) self.delete_playlist_by_id(view_id) @@ -135,10 +133,10 @@ class Views(object): def get_libraries(self): try: - libraries = self.server.jellyfin.get_media_folders()['Items'] - library_ids = [x['Id'] for x in libraries] - for view in self.server.jellyfin.get_views()['Items']: - if view['Id'] not in library_ids: + libraries = self.server.jellyfin.get_media_folders()["Items"] + library_ids = [x["Id"] for x in libraries] + for view in self.server.jellyfin.get_views()["Items"]: + if view["Id"] not in library_ids: libraries.append(view) except Exception as error: @@ -148,9 +146,7 @@ class Views(object): return libraries def get_views(self): - - ''' Get the media folders. Add or remove them. Do not proceed if issue getting libraries. - ''' + """Get the media folders. Add or remove them. Do not proceed if issue getting libraries.""" try: libraries = self.get_libraries() except IndexError as error: @@ -158,35 +154,35 @@ class Views(object): return - self.sync['SortedViews'] = [x['Id'] for x in libraries] + self.sync["SortedViews"] = [x["Id"] for x in libraries] for library in libraries: - if library['Type'] == 'Channel': - library['Media'] = "channels" + if library["Type"] == "Channel": + library["Media"] = "channels" else: - library['Media'] = library.get('OriginalCollectionType', library.get('CollectionType', "mixed")) + library["Media"] = library.get( + "OriginalCollectionType", library.get("CollectionType", "mixed") + ) self.add_library(library) - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: views = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() removed = [] for view in views: - if view.view_id not in self.sync['SortedViews']: + if view.view_id not in self.sync["SortedViews"]: removed.append(view.view_id) if removed: - event('RemoveLibrary', {'Id': ','.join(removed)}) + event("RemoveLibrary", {"Id": ",".join(removed)}) save_sync(self.sync) def get_nodes(self): - - ''' Set up playlists, video nodes, window prop. - ''' + """Set up playlists, video nodes, window prop.""" node_path = translate_path("special://profile/library/video") playlist_path = translate_path("special://profile/playlists/video") index = 0 @@ -195,38 +191,57 @@ class Views(object): if not os.path.isdir(node_path): os.makedirs(node_path) - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) - for library in self.sync['Whitelist']: + for library in self.sync["Whitelist"]: - library = library.replace('Mixed:', "") + library = library.replace("Mixed:", "") view = db.get_view(library) if view: - view = {'Id': library, 'Name': view.view_name, 'Tag': view.view_name, 'Media': view.media_type} + view = { + "Id": library, + "Name": view.view_name, + "Tag": view.view_name, + "Media": view.media_type, + } - if view['Media'] == 'mixed': - for media in ('movies', 'tvshows'): + if view["Media"] == "mixed": + for media in ("movies", "tvshows"): temp_view = dict(view) - temp_view['Media'] = media + temp_view["Media"] = media self.add_playlist(playlist_path, temp_view, True) self.add_nodes(node_path, temp_view, True) index += 1 # Compensate for the duplicate. else: - if view['Media'] in ('movies', 'tvshows', 'musicvideos'): + if view["Media"] in ("movies", "tvshows", "musicvideos"): self.add_playlist(playlist_path, view) - if view['Media'] not in ('music',): + if view["Media"] not in ("music",): self.add_nodes(node_path, view) index += 1 - for single in [{'Name': translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, - {'Name': translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, - {'Name': translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: + for single in [ + { + "Name": translate("fav_movies"), + "Tag": "Favorite movies", + "Media": "movies", + }, + { + "Name": translate("fav_tvshows"), + "Tag": "Favorite tvshows", + "Media": "tvshows", + }, + { + "Name": translate("fav_episodes"), + "Tag": "Favorite episodes", + "Media": "episodes", + }, + ]: self.add_single_node(node_path, index, "favorites", single) index += 1 @@ -234,95 +249,107 @@ class Views(object): self.window_nodes() def add_playlist(self, path, view, mixed=False): - - ''' Create or update the xps file. - ''' - file = os.path.join(path, "jellyfin%s%s.xsp" % (view['Media'], view['Id'])) + """Create or update the xps file.""" + file = os.path.join(path, "jellyfin%s%s.xsp" % (view["Media"], view["Id"])) try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: - xml = etree.Element('smartplaylist', {'type': view['Media']}) - etree.SubElement(xml, 'name') - etree.SubElement(xml, 'match') + xml = etree.Element("smartplaylist", {"type": view["Media"]}) + etree.SubElement(xml, "name") + etree.SubElement(xml, "match") except Exception: LOG.warning("Unable to parse file '%s'", file) - xml = etree.Element('smartplaylist', {'type': view['Media']}) - etree.SubElement(xml, 'name') - etree.SubElement(xml, 'match') + xml = etree.Element("smartplaylist", {"type": view["Media"]}) + etree.SubElement(xml, "name") + etree.SubElement(xml, "match") - name = xml.find('name') - name.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], view['Media']) + name = xml.find("name") + name.text = ( + view["Name"] if not mixed else "%s (%s)" % (view["Name"], view["Media"]) + ) - match = xml.find('match') + match = xml.find("match") match.text = "all" - for rule in xml.findall('.//value'): - if rule.text == view['Tag']: + for rule in xml.findall(".//value"): + if rule.text == view["Tag"]: break else: - rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) - etree.SubElement(rule, 'value').text = view['Tag'] + rule = etree.SubElement(xml, "rule", {"field": "tag", "operator": "is"}) + etree.SubElement(rule, "value").text = view["Tag"] tree = etree.ElementTree(xml) tree.write(file) def add_nodes(self, path, view, mixed=False): - - ''' Create or update the video node file. - ''' - folder = os.path.join(path, "jellyfin%s%s" % (view['Media'], view['Id'])) + """Create or update the video node file.""" + folder = os.path.join(path, "jellyfin%s%s" % (view["Media"], view["Id"])) if not xbmcvfs.exists(folder): xbmcvfs.mkdir(folder) self.node_index(folder, view, mixed) - if view['Media'] == 'tvshows': + if view["Media"] == "tvshows": self.node_tvshow(folder, view) else: self.node(folder, view) def add_single_node(self, path, index, item_type, view): - file = os.path.join(path, "jellyfin_%s.xml" % view['Tag'].replace(" ", "")) + file = os.path.join(path, "jellyfin_%s.xml" % view["Tag"].replace(" ", "")) try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: - xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'match') - etree.SubElement(xml, 'content') + xml = self.node_root( + ( + "folder" + if item_type == "favorites" and view["Media"] == "episodes" + else "filter" + ), + index, + ) + etree.SubElement(xml, "label") + etree.SubElement(xml, "match") + etree.SubElement(xml, "content") except Exception: LOG.warning("Unable to parse file '%s'", file) - xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'match') - etree.SubElement(xml, 'content') + xml = self.node_root( + ( + "folder" + if item_type == "favorites" and view["Media"] == "episodes" + else "filter" + ), + index, + ) + etree.SubElement(xml, "label") + etree.SubElement(xml, "match") + etree.SubElement(xml, "content") - label = xml.find('label') - label.text = view['Name'] + label = xml.find("label") + label.text = view["Name"] - content = xml.find('content') - content.text = view['Media'] + content = xml.find("content") + content.text = view["Media"] - match = xml.find('match') + match = xml.find("match") match.text = "all" - if view['Media'] != 'episodes': + if view["Media"] != "episodes": - for rule in xml.findall('.//value'): - if rule.text == view['Tag']: + for rule in xml.findall(".//value"): + if rule.text == view["Tag"]: break else: - rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) - etree.SubElement(rule, 'value').text = view['Tag'] + rule = etree.SubElement(xml, "rule", {"field": "tag", "operator": "is"}) + etree.SubElement(rule, "value").text = view["Tag"] - if item_type == 'favorites' and view['Media'] == 'episodes': - path = self.window_browse(view, 'FavEpisodes') + if item_type == "favorites" and view["Media"] == "episodes": + path = self.window_browse(view, "FavEpisodes") self.node_favepisodes(xml, path) else: self.node_all(xml) @@ -331,62 +358,68 @@ class Views(object): tree.write(file) def node_root(self, root, index): - - ''' Create the root element - ''' - if root == 'main': - element = etree.Element('node', {'order': str(index)}) - elif root == 'filter': - element = etree.Element('node', {'order': str(index), 'type': "filter"}) + """Create the root element""" + if root == "main": + element = etree.Element("node", {"order": str(index)}) + elif root == "filter": + element = etree.Element("node", {"order": str(index), "type": "filter"}) else: - element = etree.Element('node', {'order': str(index), 'type': "folder"}) + element = etree.Element("node", {"order": str(index), "type": "folder"}) - etree.SubElement(element, 'icon').text = "special://home/addons/plugin.video.jellyfin/resources/icon.png" + etree.SubElement(element, "icon").text = ( + "special://home/addons/plugin.video.jellyfin/resources/icon.png" + ) return element def node_index(self, folder, view, mixed=False): file = os.path.join(folder, "index.xml") - index = self.sync['SortedViews'].index(view['Id']) + index = self.sync["SortedViews"].index(view["Id"]) try: if os.path.isfile(file): xml = etree.parse(file).getroot() - xml.set('order', str(index)) + xml.set("order", str(index)) else: - xml = self.node_root('main', index) - etree.SubElement(xml, 'label') + xml = self.node_root("main", index) + etree.SubElement(xml, "label") except Exception as error: LOG.exception(error) - xml = self.node_root('main', index) - etree.SubElement(xml, 'label') + xml = self.node_root("main", index) + etree.SubElement(xml, "label") - label = xml.find('label') - label.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], translate(view['Media'])) + label = xml.find("label") + label.text = ( + view["Name"] + if not mixed + else "%s (%s)" % (view["Name"], translate(view["Media"])) + ) tree = etree.ElementTree(xml) tree.write(file) def node(self, folder, view): - for node in NODES[view['Media']]: + for node in NODES[view["Media"]]: xml_name = node[0] - xml_label = node[1] or view['Name'] + xml_label = node[1] or view["Name"] file = os.path.join(folder, "%s.xml" % xml_name) - self.add_node(NODES[view['Media']].index(node), file, view, xml_name, xml_label) + self.add_node( + NODES[view["Media"]].index(node), file, view, xml_name, xml_label + ) def node_tvshow(self, folder, view): - for node in NODES[view['Media']]: + for node in NODES[view["Media"]]: xml_name = node[0] - xml_label = node[1] or view['Name'] - xml_index = NODES[view['Media']].index(node) + xml_label = node[1] or view["Name"] + xml_index = NODES[view["Media"]].index(node) file = os.path.join(folder, "%s.xml" % xml_name) - if xml_name == 'nextepisodes': + if xml_name == "nextepisodes": path = self.window_nextepisodes(view) self.add_dynamic_node(xml_index, file, view, xml_name, xml_label, path) else: @@ -398,35 +431,35 @@ class Views(object): if os.path.isfile(file): xml = etree.parse(file).getroot() else: - xml = self.node_root('filter', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'match') - etree.SubElement(xml, 'content') + xml = self.node_root("filter", index) + etree.SubElement(xml, "label") + etree.SubElement(xml, "match") + etree.SubElement(xml, "content") except Exception: LOG.warning("Unable to parse file '%s'", file) - xml = self.node_root('filter', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'match') - etree.SubElement(xml, 'content') + xml = self.node_root("filter", index) + etree.SubElement(xml, "label") + etree.SubElement(xml, "match") + etree.SubElement(xml, "content") - label = xml.find('label') + label = xml.find("label") label.text = str(name) if type(name) == int else name - content = xml.find('content') - content.text = view['Media'] + content = xml.find("content") + content.text = view["Media"] - match = xml.find('match') + match = xml.find("match") match.text = "all" - for rule in xml.findall('.//value'): - if rule.text == view['Tag']: + for rule in xml.findall(".//value"): + if rule.text == view["Tag"]: break else: - rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) - etree.SubElement(rule, 'value').text = view['Tag'] + rule = etree.SubElement(xml, "rule", {"field": "tag", "operator": "is"}) + etree.SubElement(rule, "value").text = view["Tag"] - getattr(self, 'node_' + node)(xml) # get node function based on node type + getattr(self, "node_" + node)(xml) # get node function based on node type tree = etree.ElementTree(xml) tree.write(file) @@ -436,237 +469,258 @@ class Views(object): if os.path.isfile(file): xml = etree.parse(file).getroot() else: - xml = self.node_root('folder', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'content') + xml = self.node_root("folder", index) + etree.SubElement(xml, "label") + etree.SubElement(xml, "content") except Exception: LOG.warning("Unable to parse file '%s'", file) - xml = self.node_root('folder', index) - etree.SubElement(xml, 'label') - etree.SubElement(xml, 'content') + xml = self.node_root("folder", index) + etree.SubElement(xml, "label") + etree.SubElement(xml, "content") # Migration for https://github.com/jellyfin/jellyfin-kodi/issues/239 - if xml.attrib.get('type') == 'filter': - xml.attrib = {'type': 'folder', 'order': '5'} + if xml.attrib.get("type") == "filter": + xml.attrib = {"type": "folder", "order": "5"} - label = xml.find('label') + label = xml.find("label") label.text = name - getattr(self, 'node_' + node)(xml, path) + getattr(self, "node_" + node)(xml, path) tree = etree.ElementTree(xml) tree.write(file) def node_all(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "sorttitle": break else: - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, "order", {"direction": "ascending"}).text = ( + "sorttitle" + ) def node_nextepisodes(self, root, path): - for rule in root.findall('.//path'): + for rule in root.findall(".//path"): rule.text = path break else: - etree.SubElement(root, 'path').text = path + etree.SubElement(root, "path").text = path - for rule in root.findall('.//content'): + for rule in root.findall(".//content"): rule.text = "episodes" break else: - etree.SubElement(root, 'content').text = "episodes" + etree.SubElement(root, "content").text = "episodes" def node_recent(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "dateadded": break else: - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, "order", {"direction": "descending"}).text = ( + "dateadded" + ) - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'playcount': - rule.find('value').text = "0" + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "playcount": + rule.find("value").text = "0" break else: - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" + rule = etree.SubElement( + root, "rule", {"field": "playcount", "operator": "is"} + ) + etree.SubElement(rule, "value").text = "0" def node_inprogress(self, root): - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'inprogress': + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "inprogress": break else: - etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + etree.SubElement(root, "rule", {"field": "inprogress", "operator": "true"}) - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) def node_genres(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "sorttitle": break else: - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, "order", {"direction": "ascending"}).text = ( + "sorttitle" + ) - for rule in root.findall('.//group'): + for rule in root.findall(".//group"): rule.text = "genres" break else: - etree.SubElement(root, 'group').text = "genres" + etree.SubElement(root, "group").text = "genres" def node_unwatched(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "sorttitle": break else: - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, "order", {"direction": "ascending"}).text = ( + "sorttitle" + ) - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'playcount': - rule.find('value').text = "0" + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "playcount": + rule.find("value").text = "0" break else: - rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" + rule = etree.SubElement( + root, "rule", {"field": "playcount", "operator": "is"} + ) + etree.SubElement(rule, "value").text = "0" def node_sets(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "sorttitle": break else: - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, "order", {"direction": "ascending"}).text = ( + "sorttitle" + ) - for rule in root.findall('.//group'): + for rule in root.findall(".//group"): rule.text = "sets" break else: - etree.SubElement(root, 'group').text = "sets" + etree.SubElement(root, "group").text = "sets" def node_random(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "random": break else: - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" + etree.SubElement(root, "order", {"direction": "ascending"}).text = "random" - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) def node_recommended(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "rating": break else: - etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" + etree.SubElement(root, "order", {"direction": "descending"}).text = "rating" - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'playcount': - rule.find('value').text = "0" + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "playcount": + rule.find("value").text = "0" break else: - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" + rule = etree.SubElement( + root, "rule", {"field": "playcount", "operator": "is"} + ) + etree.SubElement(rule, "value").text = "0" - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'rating': - rule.find('value').text = "7" + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "rating": + rule.find("value").text = "7" break else: - rule = etree.SubElement(root, 'rule', {'field': "rating", 'operator': "greaterthan"}) - etree.SubElement(rule, 'value').text = "7" + rule = etree.SubElement( + root, "rule", {"field": "rating", "operator": "greaterthan"} + ) + etree.SubElement(rule, "value").text = "7" def node_recentepisodes(self, root): - for rule in root.findall('.//order'): + for rule in root.findall(".//order"): if rule.text == "dateadded": break else: - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, "order", {"direction": "descending"}).text = ( + "dateadded" + ) - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'playcount': - rule.find('value').text = "0" + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "playcount": + rule.find("value").text = "0" break else: - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" + rule = etree.SubElement( + root, "rule", {"field": "playcount", "operator": "is"} + ) + etree.SubElement(rule, "value").text = "0" - content = root.find('content') + content = root.find("content") content.text = "episodes" def node_inprogressepisodes(self, root): - for rule in root.findall('.//limit'): + for rule in root.findall(".//limit"): rule.text = str(self.limit) break else: - etree.SubElement(root, 'limit').text = str(self.limit) + etree.SubElement(root, "limit").text = str(self.limit) - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'inprogress': + for rule in root.findall(".//rule"): + if rule.attrib["field"] == "inprogress": break else: - etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + etree.SubElement(root, "rule", {"field": "inprogress", "operator": "true"}) - content = root.find('content') + content = root.find("content") content.text = "episodes" def node_favepisodes(self, root, path): - for rule in root.findall('.//path'): + for rule in root.findall(".//path"): rule.text = path break else: - etree.SubElement(root, 'path').text = path + etree.SubElement(root, "path").text = path - for rule in root.findall('.//content'): + for rule in root.findall(".//content"): rule.text = "episodes" break else: - etree.SubElement(root, 'content').text = "episodes" + etree.SubElement(root, "content").text = "episodes" def order_media_folders(self, folders): - - ''' Returns a list of sorted media folders based on the Jellyfin views. - Insert them in SortedViews and remove Views that are not in media folders. - ''' + """Returns a list of sorted media folders based on the Jellyfin views. + Insert them in SortedViews and remove Views that are not in media folders. + """ if not folders: return folders - sorted_views = list(self.sync['SortedViews']) + sorted_views = list(self.sync["SortedViews"]) unordered = [x[0] for x in folders] grouped = [x for x in unordered if x not in sorted_views] @@ -678,14 +732,13 @@ class Views(object): return [folders[unordered.index(x)] for x in sorted_folders] def window_nodes(self): - - ''' Just read from the database and populate based on SortedViews - Set up the window properties that reflect the jellyfin server views and more. - ''' + """Just read from the database and populate based on SortedViews + Set up the window properties that reflect the jellyfin server views and more. + """ self.window_clear() - self.window_clear('Jellyfin.wnodes') + self.window_clear("Jellyfin.wnodes") - with Database('jellyfin') as jellyfindb: + with Database("jellyfin") as jellyfindb: libraries = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() libraries = self.order_media_folders(libraries or []) @@ -698,20 +751,30 @@ class Views(object): LOG.exception(error) for library in libraries: - view = {'Id': library.view_id, 'Name': library.view_name, 'Tag': library.view_name, 'Media': library.media_type} + view = { + "Id": library.view_id, + "Name": library.view_name, + "Tag": library.view_name, + "Media": library.media_type, + } - if library.view_id in [x.replace('Mixed:', "") for x in self.sync['Whitelist']]: # Synced libraries + if library.view_id in [ + x.replace("Mixed:", "") for x in self.sync["Whitelist"] + ]: # Synced libraries - if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed'): + if view["Media"] in ("movies", "tvshows", "musicvideos", "mixed"): - if view['Media'] == 'mixed': - for media in ('movies', 'tvshows'): + if view["Media"] == "mixed": + for media in ("movies", "tvshows"): for node in NODES[media]: temp_view = dict(view) - temp_view['Media'] = media - temp_view['Name'] = "%s (%s)" % (view['Name'], translate(media)) + temp_view["Media"] = media + temp_view["Name"] = "%s (%s)" % ( + view["Name"], + translate(media), + ) self.window_node(index, temp_view, *node) self.window_wnode(windex, temp_view, *node) @@ -719,206 +782,231 @@ class Views(object): index += 1 windex += 1 else: - for node in NODES[view['Media']]: + for node in NODES[view["Media"]]: self.window_node(index, view, *node) - if view['Media'] in ('movies', 'tvshows'): + if view["Media"] in ("movies", "tvshows"): self.window_wnode(windex, view, *node) - if view['Media'] in ('movies', 'tvshows'): + if view["Media"] in ("movies", "tvshows"): windex += 1 - elif view['Media'] == 'music': - self.window_node(index, view, 'music') + elif view["Media"] == "music": + self.window_node(index, view, "music") else: # Dynamic entry - if view['Media'] in ('homevideos', 'books', 'playlists'): - self.window_wnode(windex, view, 'browse') + if view["Media"] in ("homevideos", "books", "playlists"): + self.window_wnode(windex, view, "browse") windex += 1 - self.window_node(index, view, 'browse') + self.window_node(index, view, "browse") index += 1 - for single in [{'Name': translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, - {'Name': translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, - {'Name': translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: + for single in [ + { + "Name": translate("fav_movies"), + "Tag": "Favorite movies", + "Media": "movies", + }, + { + "Name": translate("fav_tvshows"), + "Tag": "Favorite tvshows", + "Media": "tvshows", + }, + { + "Name": translate("fav_episodes"), + "Tag": "Favorite episodes", + "Media": "episodes", + }, + ]: self.window_single_node(index, "favorites", single) index += 1 - window('Jellyfin.nodes.total', str(index)) - window('Jellyfin.wnodes.total', str(windex)) + window("Jellyfin.nodes.total", str(index)) + window("Jellyfin.wnodes.total", str(windex)) def window_node(self, index, view, node=None, node_label=None): - - ''' Leads to another listing of nodes. - ''' - if view['Media'] in ('homevideos', 'photos'): - path = self.window_browse(view, None if node in ('all', 'browse') else node) - elif node == 'nextepisodes': + """Leads to another listing of nodes.""" + if view["Media"] in ("homevideos", "photos"): + path = self.window_browse(view, None if node in ("all", "browse") else node) + elif node == "nextepisodes": path = self.window_nextepisodes(view) - elif node == 'music': + elif node == "music": path = self.window_music(view) - elif node == 'browse': + elif node == "browse": path = self.window_browse(view) else: path = self.window_path(view, node) - if node == 'music': + if node == "music": window_path = "ActivateWindow(Music,%s,return)" % path - elif node in ('browse', 'homevideos', 'photos'): + elif node in ("browse", "homevideos", "photos"): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type(node_label) == int else node_label - node_label = node_label or view['Name'] + node_label = node_label or view["Name"] - if node in ('all', 'music'): + if node in ("all", "music"): window_prop = "Jellyfin.nodes.%s" % index - window('%s.index' % window_prop, path.replace('all.xml', "")) # dir - window('%s.title' % window_prop, view['Name']) - window('%s.content' % window_prop, path) + window("%s.index" % window_prop, path.replace("all.xml", "")) # dir + window("%s.title" % window_prop, view["Name"]) + window("%s.content" % window_prop, path) - elif node == 'browse': + elif node == "browse": window_prop = "Jellyfin.nodes.%s" % index - window('%s.title' % window_prop, view['Name']) + window("%s.title" % window_prop, view["Name"]) else: window_prop = "Jellyfin.nodes.%s.%s" % (index, node) - window('%s.title' % window_prop, node_label) - window('%s.content' % window_prop, path) + window("%s.title" % window_prop, node_label) + window("%s.content" % window_prop, path) - window('%s.id' % window_prop, view['Id']) - window('%s.path' % window_prop, window_path) - window('%s.type' % window_prop, view['Media']) - self.window_artwork(window_prop, view['Id']) + window("%s.id" % window_prop, view["Id"]) + window("%s.path" % window_prop, window_path) + window("%s.type" % window_prop, view["Media"]) + self.window_artwork(window_prop, view["Id"]) def window_single_node(self, index, item_type, view): - - ''' Single destination node. - ''' - path = "library://video/jellyfin_%s.xml" % view['Tag'].replace(" ", "") + """Single destination node.""" + path = "library://video/jellyfin_%s.xml" % view["Tag"].replace(" ", "") window_path = "ActivateWindow(Videos,%s,return)" % path window_prop = "Jellyfin.nodes.%s" % index - window('%s.title' % window_prop, view['Name']) - window('%s.path' % window_prop, window_path) - window('%s.content' % window_prop, path) - window('%s.type' % window_prop, item_type) + window("%s.title" % window_prop, view["Name"]) + window("%s.path" % window_prop, window_path) + window("%s.content" % window_prop, path) + window("%s.type" % window_prop, item_type) def window_wnode(self, index, view, node=None, node_label=None): - - ''' Similar to window_node, but does not contain music, musicvideos. - Contains books, audiobooks. - ''' - if view['Media'] in ('homevideos', 'photos', 'books', 'playlists'): - path = self.window_browse(view, None if node in ('all', 'browse') else node) + """Similar to window_node, but does not contain music, musicvideos. + Contains books, audiobooks. + """ + if view["Media"] in ("homevideos", "photos", "books", "playlists"): + path = self.window_browse(view, None if node in ("all", "browse") else node) else: path = self.window_path(view, node) - if node in ('browse', 'homevideos', 'photos', 'books', 'playlists'): + if node in ("browse", "homevideos", "photos", "books", "playlists"): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type(node_label) == int else node_label - node_label = node_label or view['Name'] + node_label = node_label or view["Name"] - if node == 'all': + if node == "all": window_prop = "Jellyfin.wnodes.%s" % index - window('%s.index' % window_prop, path.replace('all.xml', "")) # dir - window('%s.title' % window_prop, view['Name']) - elif node == 'browse': + window("%s.index" % window_prop, path.replace("all.xml", "")) # dir + window("%s.title" % window_prop, view["Name"]) + elif node == "browse": window_prop = "Jellyfin.wnodes.%s" % index - window('%s.title' % window_prop, view['Name']) + window("%s.title" % window_prop, view["Name"]) else: window_prop = "Jellyfin.wnodes.%s.%s" % (index, node) - window('%s.title' % window_prop, node_label) - window('%s.content' % window_prop, path) + window("%s.title" % window_prop, node_label) + window("%s.content" % window_prop, path) - window('%s.id' % window_prop, view['Id']) - window('%s.path' % window_prop, window_path) - window('%s.type' % window_prop, view['Media']) - self.window_artwork(window_prop, view['Id']) + window("%s.id" % window_prop, view["Id"]) + window("%s.path" % window_prop, window_path) + window("%s.type" % window_prop, view["Media"]) + self.window_artwork(window_prop, view["Id"]) - LOG.debug("--[ wnode/%s/%s ] %s", index, window('%s.title' % window_prop), window('%s.artwork' % window_prop)) + LOG.debug( + "--[ wnode/%s/%s ] %s", + index, + window("%s.title" % window_prop), + window("%s.artwork" % window_prop), + ) def window_artwork(self, prop, view_id): if not self.server.logged_in: - window('%s.artwork' % prop, clear=True) + window("%s.artwork" % prop, clear=True) elif self.media_folders is not None: for library in self.media_folders: - if library['Id'] == view_id and 'Primary' in library.get('ImageTags', {}): - server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] - artwork = api.API(None, server_address).get_artwork(view_id, 'Primary') - window('%s.artwork' % prop, artwork) + if library["Id"] == view_id and "Primary" in library.get( + "ImageTags", {} + ): + server_address = self.server.auth.get_server_info( + self.server.auth.server_id + )["address"] + artwork = api.API(None, server_address).get_artwork( + view_id, "Primary" + ) + window("%s.artwork" % prop, artwork) break else: - window('%s.artwork' % prop, clear=True) + window("%s.artwork" % prop, clear=True) def window_path(self, view, node): - return "library://video/jellyfin%s%s/%s.xml" % (view['Media'], view['Id'], node) + return "library://video/jellyfin%s%s/%s.xml" % (view["Media"], view["Id"], node) def window_music(self, view): return "library://music/" def window_nextepisodes(self, view): - params = { - 'id': view['Id'], - 'mode': "nextepisodes", - 'limit': self.limit - } + params = {"id": view["Id"], "mode": "nextepisodes", "limit": self.limit} return "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) def window_browse(self, view, node=None): - params = { - 'mode': "browse", - 'type': view['Media'] - } + params = {"mode": "browse", "type": view["Media"]} - if view.get('Id'): - params['id'] = view['Id'] + if view.get("Id"): + params["id"] = view["Id"] if node: - params['folder'] = node + params["folder"] = node return "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) def window_clear(self, name=None): - - ''' Clearing window prop setup for Views. - ''' - total = int(window((name or 'Jellyfin.nodes') + '.total') or 0) + """Clearing window prop setup for Views.""" + total = int(window((name or "Jellyfin.nodes") + ".total") or 0) props = [ - - "index", "id", "path", "artwork", "title", "content", "type" - "inprogress.content", "inprogress.title", - "inprogress.content", "inprogress.path", - "nextepisodes.title", "nextepisodes.content", - "nextepisodes.path", "unwatched.title", - "unwatched.content", "unwatched.path", - "recent.title", "recent.content", "recent.path", - "recentepisodes.title", "recentepisodes.content", - "recentepisodes.path", "inprogressepisodes.title", - "inprogressepisodes.content", "inprogressepisodes.path" + "index", + "id", + "path", + "artwork", + "title", + "content", + "type" "inprogress.content", + "inprogress.title", + "inprogress.content", + "inprogress.path", + "nextepisodes.title", + "nextepisodes.content", + "nextepisodes.path", + "unwatched.title", + "unwatched.content", + "unwatched.path", + "recent.title", + "recent.content", + "recent.path", + "recentepisodes.title", + "recentepisodes.content", + "recentepisodes.path", + "inprogressepisodes.title", + "inprogressepisodes.content", + "inprogressepisodes.path", ] for i in range(total): for prop in props: - window('Jellyfin.nodes.%s.%s' % (str(i), prop), clear=True) + window("Jellyfin.nodes.%s.%s" % (str(i), prop), clear=True) for prop in props: - window('Jellyfin.nodes.%s' % prop, clear=True) + window("Jellyfin.nodes.%s" % prop, clear=True) def delete_playlist(self, path): @@ -926,25 +1014,21 @@ class Views(object): LOG.info("DELETE playlist %s", path) def delete_playlists(self): - - ''' Remove all jellyfin playlists. - ''' + """Remove all jellyfin playlists.""" path = translate_path("special://profile/playlists/video/") _, files = xbmcvfs.listdir(path) for file in files: - if file.startswith('jellyfin'): + if file.startswith("jellyfin"): self.delete_playlist(os.path.join(path, file)) def delete_playlist_by_id(self, view_id): - - ''' Remove playlist based on view_id. - ''' + """Remove playlist based on view_id.""" path = translate_path("special://profile/playlists/video/") _, files = xbmcvfs.listdir(path) for file in files: file = file - if file.startswith('jellyfin') and file.endswith('%s.xsp' % view_id): + if file.startswith("jellyfin") and file.endswith("%s.xsp" % view_id): self.delete_playlist(os.path.join(path, file)) def delete_node(self, path): @@ -953,20 +1037,18 @@ class Views(object): LOG.info("DELETE node %s", path) def delete_nodes(self): - - ''' Remove node and children files. - ''' + """Remove node and children files.""" path = translate_path("special://profile/library/video/") dirs, files = xbmcvfs.listdir(path) for file in files: - if file.startswith('jellyfin'): + if file.startswith("jellyfin"): self.delete_node(os.path.join(path, file)) for directory in dirs: - if directory.startswith('jellyfin'): + if directory.startswith("jellyfin"): _, files = xbmcvfs.listdir(os.path.join(path, directory)) for file in files: @@ -975,15 +1057,13 @@ class Views(object): xbmcvfs.rmdir(os.path.join(path, directory)) def delete_node_by_id(self, view_id): - - ''' Remove node and children files based on view_id. - ''' + """Remove node and children files based on view_id.""" path = translate_path("special://profile/library/video/") dirs, files = xbmcvfs.listdir(path) for directory in dirs: - if directory.startswith('jellyfin') and directory.endswith(view_id): + if directory.startswith("jellyfin") and directory.endswith(view_id): _, files = xbmcvfs.listdir(os.path.join(path, directory)) for file in files: diff --git a/service.py b/service.py index 9ffa713b..c19472d5 100644 --- a/service.py +++ b/service.py @@ -14,16 +14,16 @@ from jellyfin_kodi.helper import LazyLogger ################################################################################################# LOG = LazyLogger(__name__) -DELAY = int(settings('startupDelay') if settings('SyncInstallRunDone.bool') else 4) +DELAY = int(settings("startupDelay") if settings("SyncInstallRunDone.bool") else 4) ################################################################################################# class ServiceManager(threading.Thread): + """Service thread. + To allow to restart and reload modules internally. + """ - ''' Service thread. - To allow to restart and reload modules internally. - ''' exception = None def __init__(self): @@ -44,10 +44,10 @@ class ServiceManager(threading.Thread): if service is not None: # TODO: fix this properly as to not match on str() - if 'ExitService' not in str(error): + if "ExitService" not in str(error): service.shutdown() - if 'RestartService' in str(error): + if "RestartService" in str(error): service.reload_objects() self.exception = error @@ -58,7 +58,7 @@ if __name__ == "__main__": LOG.info("Delay startup by %s seconds.", DELAY) while True: - if not settings('enableAddon.bool'): + if not settings("enableAddon.bool"): LOG.warning("Jellyfin for Kodi is not enabled.") break @@ -68,12 +68,11 @@ if __name__ == "__main__": session.start() session.join() # Block until the thread exits. - if 'RestartService' in str(session.exception): + if "RestartService" in str(session.exception): continue except Exception as error: - ''' Issue initializing the service. - ''' + """Issue initializing the service.""" LOG.exception(error) break diff --git a/tests/test_clean_none_dict_values.py b/tests/test_clean_none_dict_values.py index 4a4f43d8..982f4515 100644 --- a/tests/test_clean_none_dict_values.py +++ b/tests/test_clean_none_dict_values.py @@ -3,45 +3,51 @@ import pytest from jellyfin_kodi.jellyfin.utils import clean_none_dict_values -@pytest.mark.parametrize("obj,expected", [ - (None, None), - ([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]), - ({'foo': None, 'bar': 123}, {'bar': 123}), - ({ - 'dict': { - 'empty': None, - 'string': "Hello, Woorld!", - }, - 'number': 123, - 'list': [ - None, - 123, - "foo", +@pytest.mark.parametrize( + "obj,expected", + [ + (None, None), + ([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]), + ({"foo": None, "bar": 123}, {"bar": 123}), + ( { - 'empty': None, - 'number': 123, - 'string': "foo", - 'list': [], - 'dict': {}, - } - ] - }, { - 'dict': { - 'string': "Hello, Woorld!", - }, - 'number': 123, - 'list': [ - None, - 123, - "foo", + "dict": { + "empty": None, + "string": "Hello, Woorld!", + }, + "number": 123, + "list": [ + None, + 123, + "foo", + { + "empty": None, + "number": 123, + "string": "foo", + "list": [], + "dict": {}, + }, + ], + }, { - 'number': 123, - 'string': "foo", - 'list': [], - 'dict': {}, - } - ] - }), -]) + "dict": { + "string": "Hello, Woorld!", + }, + "number": 123, + "list": [ + None, + 123, + "foo", + { + "number": 123, + "string": "foo", + "list": [], + "dict": {}, + }, + ], + }, + ), + ], +) def test_clean_none_dict_values(obj, expected): assert clean_none_dict_values(obj) == expected diff --git a/tests/test_imports.py b/tests/test_imports.py index 7946604a..6bf60859 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -37,6 +37,7 @@ def test_import_downloader(): def test_import_entrypoint(): import jellyfin_kodi.entrypoint import jellyfin_kodi.entrypoint.context + # import jellyfin_kodi.entrypoint.default # FIXME: Messes with sys.argv import jellyfin_kodi.entrypoint.service # noqa: F401 diff --git a/typings/jellyfin_kodi/database/jellyfin_db.pyi b/typings/jellyfin_kodi/database/jellyfin_db.pyi index 2cfc2a3d..2859419b 100644 --- a/typings/jellyfin_kodi/database/jellyfin_db.pyi +++ b/typings/jellyfin_kodi/database/jellyfin_db.pyi @@ -1,13 +1,11 @@ from sqlite3 import Cursor from typing import Any, List, Optional, NamedTuple - class ViewRow(NamedTuple): view_id: str view_name: str media_type: str - class JellyfinDatabase: cursor: Cursor = ... def __init__(self, cursor: Cursor) -> None: ...