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 1/2] Tool black: auto-format Python code --- build.py | 81 +- jellyfin_kodi/client.py | 76 +- jellyfin_kodi/connect.py | 186 +-- jellyfin_kodi/database/__init__.py | 242 ++-- 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 | 1168 +++++++++++------ jellyfin_kodi/entrypoint/service.py | 408 +++--- jellyfin_kodi/full_sync.py | 564 ++++---- jellyfin_kodi/helper/api.py | 301 +++-- jellyfin_kodi/helper/exceptions.py | 7 +- jellyfin_kodi/helper/lazylogger.py | 2 + jellyfin_kodi/helper/loghandler.py | 34 +- jellyfin_kodi/helper/playutils.py | 686 +++++----- 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 | 357 ++--- jellyfin_kodi/jellyfin/client.py | 15 +- jellyfin_kodi/jellyfin/configuration.py | 51 +- jellyfin_kodi/jellyfin/connection_manager.py | 167 +-- jellyfin_kodi/jellyfin/credentials.py | 56 +- jellyfin_kodi/jellyfin/http.py | 151 ++- jellyfin_kodi/jellyfin/ws_client.py | 76 +- jellyfin_kodi/library.py | 536 +++++--- jellyfin_kodi/monitor.py | 290 ++-- jellyfin_kodi/objects/actions.py | 1023 ++++++++------- 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 | 396 +++--- jellyfin_kodi/objects/music.py | 573 ++++---- jellyfin_kodi/objects/musicvideos.py | 251 ++-- jellyfin_kodi/objects/obj.py | 79 +- jellyfin_kodi/objects/tvshows.py | 734 ++++++----- jellyfin_kodi/objects/utils.py | 10 +- jellyfin_kodi/player.py | 308 ++--- jellyfin_kodi/views.py | 906 +++++++------ 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, 6655 insertions(+), 4833 deletions(-) diff --git a/build.py b/build.py index bf4495240..148441f36 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 3aac72cbc..e38e52f7c 100644 --- a/jellyfin_kodi/client.py +++ b/jellyfin_kodi/client.py @@ -18,73 +18,69 @@ 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 7566ce909..8a3f2b1db 100644 --- a/jellyfin_kodi/connect.py +++ b/jellyfin_kodi/connect.py @@ -16,7 +16,7 @@ ################################################################################################## 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 @@ def __init__(self): 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 @@ def register(self, server_id=None, options={}): 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 @@ def register_client(self, credentials=None, options=None, server_id=None, server 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 @@ def select_servers(self, state=None): 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 @@ def setup_manual_server(self): 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 @@ def manual_server(self, manager=None): 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 @@ def login(self): 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 @@ def login(self): 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 @@ def setup_login_manual(self): 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 @@ def login_manual(self, user=None, manager=None): 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 e48716679..b92ff1892 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 @@ 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 @@ def _get_database(self, path, silent=False): 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')): - - version_string = re.search('{}(.*).db'.format(database), db_file) + 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 = 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 @@ def __exit__(self, exc_type, exc_val, exc_tb): 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 0cbe9dd6b..5201e9326 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 @@ ################################################################################################## -class JellyfinDatabase(): +class JellyfinDatabase: def __init__(self, cursor): self.cursor = cursor @@ -32,9 +33,7 @@ def update_reference(self, *args): 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 @@ def get_version(self): 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 52d688460..41a59cda3 100644 --- a/jellyfin_kodi/database/queries.py +++ b/jellyfin_kodi/database/queries.py @@ -94,16 +94,126 @@ 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 6d48a5034..8e8eb5db2 100644 --- a/jellyfin_kodi/dialogs/context.py +++ b/jellyfin_kodi/dialogs/context.py @@ -47,8 +47,8 @@ def get_selected(self): 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 @@ def onAction(self, action): 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 1d70bec2d..c63dd519f 100644 --- a/jellyfin_kodi/dialogs/loginmanual.py +++ b/jellyfin_kodi/dialogs/loginmanual.py @@ -18,7 +18,7 @@ CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 -ERROR = {'Invalid': 1, 'Empty': 2} +ERROR = {"Invalid": 1, "Empty": 2} ################################################################################################## @@ -76,7 +76,7 @@ def onClick(self, control): 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 @@ def onClick(self, control): 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 @@ def _add_editcontrol(self, x, y, height, width, password=False): 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 @@ def _add_editcontrol(self, x, y, height, width, password=False): 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 @@ 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/serverconnect.py b/jellyfin_kodi/dialogs/serverconnect.py index 73d720587..adeffbe5a 100644 --- a/jellyfin_kodi/dialogs/serverconnect.py +++ b/jellyfin_kodi/dialogs/serverconnect.py @@ -64,8 +64,10 @@ def onInit(self): 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 @@ def onInit(self): 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 @@ def onAction(self, action): 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 @@ def onClick(self, control): 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 551c840e0..8f4b44bb9 100644 --- a/jellyfin_kodi/dialogs/servermanual.py +++ b/jellyfin_kodi/dialogs/servermanual.py @@ -22,10 +22,7 @@ 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 @@ def onClick(self, control): 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 @@ def onClick(self, control): 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 @@ def onAction(self, action): 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 @@ def _connect_to_server(self, server): 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 8c1428ef6..1abb8949c 100644 --- a/jellyfin_kodi/dialogs/usersconnect.py +++ b/jellyfin_kodi/dialogs/usersconnect.py @@ -52,17 +52,20 @@ def onInit(self): 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 @@ def onAction(self, action): 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 @@ def onClick(self, control): 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 f9fb03ca3..2992af6ee 100644 --- a/jellyfin_kodi/downloader.py +++ b/jellyfin_kodi/downloader.py @@ -25,7 +25,7 @@ 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_wrapper(params): # 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 @@ def run(self): 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 @@ def run(self): 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 dca7e9f92..e5d383b8c 100644 --- a/jellyfin_kodi/entrypoint/context.py +++ b/jellyfin_kodi/entrypoint/context.py @@ -17,14 +17,18 @@ ################################################################################################# 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 @@ def __init__(self, transcode=False, delete=False): 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 @@ def __init__(self, transcode=False, delete=False): 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 @@ def get_media_type(self): 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 @@ def action_menu(self): 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 d8f6c049a..9b4185f2c 100644 --- a/jellyfin_kodi/entrypoint/default.py +++ b/jellyfin_kodi/entrypoint/default.py @@ -14,7 +14,16 @@ 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 @@ 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 @@ def __init__(self): 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') - - elif mode == 'playlist': - api_client.post_session(api_client.config.data['app.session'], "Playing", { - 'PlayCommand': "PlayNow", - 'ItemIds': params['id'], - 'StartPositionTicks': 0 - }) - elif mode == 'deviceid': + 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": 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)) - - 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(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 + ): + + 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 '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 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, + ) 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['UserData']['Played']: - context.append((translate(16104), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&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), + ) + ) 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['UserData']['Played']: - context.append((translate(16104), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&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), + ) + ) 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 getVideoFiles(jellyfinId,jellyfinPath): 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 8c3b6affe..aaf0d03d8 100644 --- a/jellyfin_kodi/entrypoint/service.py +++ b/jellyfin_kodi/entrypoint/service.py @@ -18,7 +18,15 @@ 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) - - 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) + 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()) + + 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 @@ def start_default(self): 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 @@ def stop_default(self): 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 method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect', - 'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', - 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer', - 'Unauthorized', 'UserConfigurationUpdated', 'ServerRestarting', - 'RemoveServer', 'UpdatePassword', 'AddLibrarySelection', 'RemoveLibrarySelection'): + 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", + ): 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'): - - 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 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, + ) 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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 @@ def reload_objects(self): 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 7463c3007..6143e960a 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -23,11 +23,11 @@ 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 @@ def __init__(self, library, server): 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 @@ def mapping(self): 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 @@ def select_libraries(self, libraries): 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 @@ def process_library(self, library_id): 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) - - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] - - 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']) + obj = Movies( + self.server, jellyfindb, videodb, self.direct_path, library + ) + + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] + + 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"], + ) 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 @@ def movies(self, library, dialog): 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) - - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] - - 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) + obj = TVShows( + self.server, jellyfindb, videodb, self.direct_path, library, True + ) + + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] + + 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, + ) 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) - - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] - - 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']) + obj = MusicVideos( + self.server, jellyfindb, videodb, self.direct_path, library + ) + + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] + + 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"], + ) 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 @@ def music(self, library, dialog): 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) - - self.sync['RestorePoint'] = items['RestorePoint'] - start_index = items['RestorePoint']['params']['StartIndex'] - - 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']) + obj = Movies( + self.server, jellyfindb, videodb, self.direct_path, library + ) + + self.sync["RestorePoint"] = items["RestorePoint"] + start_index = items["RestorePoint"]["params"]["StartIndex"] + + 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"], + ) 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 @@ def refresh_boxsets(self, library): @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 7ac839ad2..0174c3edb 100644 --- a/jellyfin_kodi/helper/api.py +++ b/jellyfin_kodi/helper/api.py @@ -14,100 +14,102 @@ 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.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" - - 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" + 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, + } + ) + + 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 "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 @@ def audio_streams(self, tracks): 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 @@ def adjust_resume(cls, resume_seconds): 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 @@ def adjust_resume(cls, resume_seconds): 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 @@ def get_overview(self, overview=None): 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 @@ def get_mpaa(self, rating=None): 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 @@ def get_backdrops(self, item_id, tags, query=None): 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 12105ed80..712a64a94 100644 --- a/jellyfin_kodi/helper/exceptions.py +++ b/jellyfin_kodi/helper/exceptions.py @@ -9,7 +9,11 @@ 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 cead1f117..bfd15f4b9 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 @@ def __init__(self, logger_name=None): 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 ce06ecb12..9d234324b 100644 --- a/jellyfin_kodi/helper/loghandler.py +++ b/jellyfin_kodi/helper/loghandler.py @@ -16,8 +16,8 @@ ################################################################################################## -__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 @@ def __init__(self): 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 @@ def emit(self, record): 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 @@ def _get_log_level(cls, level): 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 @@ def _get_log_level(cls, level): 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 @@ def formatException(self, exc_info): 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 6fc850fcd..baec4b56e 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 {} + + 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"), + } + ) - ''' 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') - }) - - 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 @@ def select_source(self, sources, audio=None, subtitle=None): 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 @@ def select_source(self, sources, audio=None, subtitle=None): 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 @@ def is_file_exists(self, source): 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 @@ def is_file_exists(self, source): 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 @@ def is_strm(self, source): 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'] - - 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" - - if self.info['ForceTranscode']: - - 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)): + """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"): + + """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"]: + + 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) + ) + ): 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 @@ def get(self, source, audio=None, subtitle=None): 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 @@ def get_device_profile(self): "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.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.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, + }, + ) 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 @@ def set_external_subs(self, source, listitem): 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 @@ def set_external_subs(self, source, listitem): 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 @@ def set_external_subs(self, source, listitem): 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 @@ def download_external_subs(cls, src, filename): 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 @@ def get_track_title(track_index): # 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 @@ def get_track_title(track_index): 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 @@ def get_track_title(track_index): 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 @@ def get_track_title(track_index): 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 @@ def get_track_title(track_index): 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 2714e9ee3..781890c7d 100644 --- a/jellyfin_kodi/helper/translate.py +++ b/jellyfin_kodi/helper/translate.py @@ -15,13 +15,11 @@ 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 9d4bfdefe..fb651cf68 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 @@ def __init__(self, method, **kwargs): 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 @@ def execute(self, params=None): 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 1b814da19..8dc59745d 100644 --- a/jellyfin_kodi/helper/wrapper.py +++ b/jellyfin_kodi/helper/wrapper.py @@ -19,9 +19,8 @@ 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 wrapper(self, item=None, *args, **kwargs): 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 wrapper(self, item=None, *args, **kwargs): 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 wrapper(*args, **kwargs): 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 64c93440b..fdf9e3a9f 100644 --- a/jellyfin_kodi/helper/xmls.py +++ b/jellyfin_kodi/helper/xmls.py @@ -19,45 +19,42 @@ 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 6d0bcdec8..c9aee2946 100644 --- a/jellyfin_kodi/jellyfin/__init__.py +++ b/jellyfin_kodi/jellyfin/__init__.py @@ -25,22 +25,22 @@ def wrapper(self, *args, **kwargs): 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 @@ def construct(self): 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 a65c5836f..4ec7ce312 100644 --- a/jellyfin_kodi/jellyfin/api.py +++ b/jellyfin_kodi/jellyfin/api.py @@ -15,7 +15,7 @@ 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 @@ def _http(self, action, url, request=None): 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 @@ def videos(self, handler): 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/%s?MaxWidth=%s&format=%s" % (item_id, art, index, 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), + ) ################################################################################################# @@ -140,16 +147,16 @@ def get_item(self, item_id): 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 @@ def get_images(self, item_id): 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 @@ def get_local_trailers(self, item_id): 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 @@ def check_companion_enabled(self): 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 @@ def check_companion_enabled(self): 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 @@ def post_capabilities(self, data): 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 @@ def item_played(self, item_id, watched): 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 @@ def get_public_info(self, server_address): 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 5026a73ef..d7802df3e 100644 --- a/jellyfin_kodi/jellyfin/client.py +++ b/jellyfin_kodi/jellyfin/client.py @@ -19,11 +19,10 @@ 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 @@ def authenticate(self, credentials=None, options=None): 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 c8407dbf8..09a2b2645 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 @@ def __init__(self): 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 - - def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT): + 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, + ): 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 9e562a636..304f7148e 100644 --- a/jellyfin_kodi/jellyfin/connection_manager.py +++ b/jellyfin_kodi/jellyfin/connection_manager.py @@ -20,10 +20,10 @@ 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 @@ def revoke_token(self): 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 @@ def get_available_servers(self): 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 @@ def get_available_servers(self): 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 @@ def login(self, server_url, username, password=None): 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 @@ def connect_to_address(self, address, options={}): 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 @@ def connect(self, options={}): 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 @@ def connect(self, options={}): 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 @@ def get_server_info(self, server_id): 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 @@ def process_found_servers(self, found_servers): 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 @@ def process_found_servers(self, found_servers): # 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 @@ def _convert_endpoint_address_to_manual_address(self, info): 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 @@ def _update_server_info(self, server, system_info): 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 638f4af30..1ef3d1645 100644 --- a/jellyfin_kodi/jellyfin/credentials.py +++ b/jellyfin_kodi/jellyfin/credentials.py @@ -41,7 +41,7 @@ def _ensure(self): 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 @@ def _clear(self): 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 646dc0718..0c3282137 100644 --- a/jellyfin_kodi/jellyfin/http.py +++ b/jellyfin_kodi/jellyfin/http.py @@ -34,9 +34,13 @@ def start_session(self): 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 @@ def stop_session(self): 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 @@ def request(self, data, session=None): 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 @@ def request(self, data, session=None): 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 @@ def request(self, data, session=None): 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 @@ def request(self, data, session=None): 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 @@ def _process_params(self, params): def _get_header(self, data): - 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 'x-emby-authorization' not in data['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 "x-emby-authorization" not in data["headers"]: self._authorization(data) return data @@ -222,19 +248,26 @@ def _get_header(self, data): 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') - - 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'): - - 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 += "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")}) + + 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"), + } + ) return data diff --git a/jellyfin_kodi/jellyfin/ws_client.py b/jellyfin_kodi/jellyfin/ws_client.py index 65da191e0..38a17f8b4 100644 --- a/jellyfin_kodi/jellyfin/ws_client.py +++ b/jellyfin_kodi/jellyfin/ws_client.py @@ -14,7 +14,8 @@ # 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 @@ def send(self, message, data=""): 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 @@ def on_open(self, ws): 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 c31276641..a200f37de 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -24,8 +24,8 @@ ################################################################################################## 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 @@ def __init__(self, monitor): 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 @@ def __init__(self, monitor): 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 @@ def run(self): 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 @@ def run(self): 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 @@ def check_version(self): 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 not self.player.isPlayingVideo() or settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + """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)") + ): self.worker_downloads() self.worker_sort() @@ -154,7 +163,7 @@ def service(self): 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 @@ def service(self): 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)) - - if not settings('dbSyncScreensaver.bool') and self.screensaver is None: - - xbmc.executebuiltin('InhibitIdleShutdown(true)') + 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: + + 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 @@ def worker_queue_size(self): 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 @@ def worker_downloads(self): 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 @@ def worker_sort(self): 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 queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = UpdateWorker(queue, self.notify_output, self.music_database_lock, "music", self.server, self.direct_path) + 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, + ) 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 @@ def worker_notify(self): 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 @@ def startup(self): Views().get_nodes() try: - if get_sync()['Libraries']: + if get_sync()["Libraries"]: try: with FullSync(self, self.server) as sync: @@ -340,7 +410,7 @@ def startup(self): 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 @@ def startup(self): 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 @@ def startup(self): 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 @@ def startup(self): 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 @@ def fast_sync(self): 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 @@ def fast_sync(self): 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 @@ def select_libraries(self, mode=None): 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 @@ def remove_library(self, library_id): 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 @@ def userdata(self, data): 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 @@ def updated(self, data): 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 @@ def __init__(self, queue, notify, lock, database, server=None, direct_path=None, 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 @@ def run(self): 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 @@ def run(self): 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 @@ def __init__(self, queue, lock, database, server, direct_path): 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 @@ def run(self): 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 @@ def run(self): 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 @@ def __init__(self, queue, output, *args): 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 @@ def run(self): 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 @@ def __init__(self, queue, lock, database, server, direct_path): 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 @@ def run(self): 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 @@ def run(self): 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 @@ def run(self): 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 31b9d67cd..4f02226bb 100644 --- a/jellyfin_kodi/monitor.py +++ b/jellyfin_kodi/monitor.py @@ -46,23 +46,35 @@ def onScanFinished(self, library): 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 method not in ('ReportProgressRequested', 'LoadServer', 'AddUser', 'PlayPlaylist', 'Play', 'Playstate', 'GeneralCommand'): + if sender == "plugin.video.jellyfin": + method = method.split(".")[1] + + 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 @@ def onNotification(self, sender, method, data): if data: data = json.loads(binascii.unhexlify(data[0])) else: - 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. - ''' + 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. + """ 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 @@ def onNotification(self, sender, method, data): 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 @@ def onNotification(self, sender, method, data): 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': - - 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 == 'Playstate': + elif method == "PlayPlaylist": + + 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 == "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 @@ def additional_users(self, server): 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 @@ def playstate(self, data): 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'] - - 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']) - - elif command == 'SetVolume': - xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume']) + """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"]: + 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"]) # 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 @@ def __init__(self, monitor): 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 dbac009e0..4711e1e11 100644 --- a/jellyfin_kodi/objects/actions.py +++ b/jellyfin_kodi/objects/actions.py @@ -31,43 +31,47 @@ def __init__(self, server_id=None, api_client=None): 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 @@ def play(self, item, db_id=None, transcode=False, playlist=False): 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 @@ def _set_intros(self, item): 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) - - 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] + listitem.setPath(part["PlaybackInfo"]["Path"]) + playutils.set_properties( + part, part["PlaybackInfo"]["Method"], self.server_id + ) + + 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] playlist = self.get_playlist(item) player = xbmc.Player() @@ -170,55 +179,66 @@ def play_playlist(self, items, clear=True, seektime=None, audio=None, subtitle=N 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 @@ def set_listitem(self, item, listitem, db_id=None, seektime=None, intro=False): 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 - - 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'] \ + 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 "" + + 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']] - - 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 - - listitem.setArt({ - 'icon': 'DefaultVideo.png', - 'thumb': 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"]) + + 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"], + } + ) - 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['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'] == 'Series': - - 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') - - 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'] == 'BoxSet': - metadata['mediatype'] = "set" - listitem.setProperty('IsFolder', 'true') + 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"], + } + ) + + 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": + + 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") + + 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"] == "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') - - 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']['subtitle']: - listitem.addStreamInfo('subtitle', {'language': track}) - - listitem.setLabel(obj['Title']) - listitem.setInfo('video', metadata) + 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"]["audio"]: + listitem.addStreamInfo( + "audio", {"codec": track["codec"], "channels": track["channels"]} + ) + + for track in obj["Streams"]["subtitle"]: + listitem.addStreamInfo("subtitle", {"language": track}) + + 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']) - - 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'] in ('Artist', 'MusicArtist'): - metadata.update({ - 'mediatype': "artist", - 'musicbrainzartistid': obj['UniqueId'] - }) + 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") + + elif obj["Type"] == "Album": + metadata.update( + {"mediatype": "album", "musicbrainzalbumid": 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'], - }) - - 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', - }) + 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", + } + ) else: - listitem.setProperty('IsFolder', 'true') - listitem.setArt({ - 'icon': 'DefaultFolder.png', - }) - - listitem.setProperty('IsPlayable', 'false') - listitem.setLabel(obj['Title']) - listitem.setInfo('pictures', metadata) + listitem.setProperty("IsFolder", "true") + listitem.setArt( + { + "icon": "DefaultFolder.png", + } + ) + + 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 @@ def run(self): 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 a37320a86..7162c0502 100644 --- a/jellyfin_kodi/objects/kodi/artwork.py +++ b/jellyfin_kodi/objects/kodi/artwork.py @@ -21,15 +21,21 @@ def __init__(self, cursor): 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 @@ def update(self, image_url, kodi_id, media, image): 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 74b93e338..2edb53b84 100644 --- a/jellyfin_kodi/objects/kodi/kodi.py +++ b/jellyfin_kodi/objects/kodi/kodi.py @@ -89,7 +89,13 @@ def remove_path(self, *args): 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 @@ def add_people(self, people, *args): 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 @@ def add_person(self, *args): 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 @@ def _get_person(self, name): 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 @@ def get_person(self, name): 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 @@ def get_studio(self, *args): 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 @@ def add_stream_sub(self, *args): 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 10172c02f..14fba8434 100644 --- a/jellyfin_kodi/objects/kodi/movies.py +++ b/jellyfin_kodi/objects/kodi/movies.py @@ -74,15 +74,11 @@ def get_rating_id(self, *args): 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 @@ def get_unique_id(self, *args): 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 @@ def delete_boxset(self, *args): 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 @@ def migrations(self): 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 @@ def omega_migration(self): 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 84d7a1a47..cad294a1b 100644 --- a/jellyfin_kodi/objects/kodi/music.py +++ b/jellyfin_kodi/objects/kodi/music.py @@ -25,10 +25,9 @@ def __init__(self, cursor): 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 @@ def add_role(self, *args): 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 @@ def get(self, artist_id, name, musicbrainz): 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 @@ def get_album(self, album_id, name, musicbrainz, artists=None, *args): 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 @@ def get_album(self, album_id, name, musicbrainz, artists=None, *args): 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 @@ def rate_song(self, *args): 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 @@ def add_genres(self, kodi_id, genres, media): 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 5bca095d1..d02ef832a 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 88131dc3f..ca6c7fa4c 100644 --- a/jellyfin_kodi/objects/kodi/queries_music.py +++ b/jellyfin_kodi/objects/kodi/queries_music.py @@ -54,7 +54,14 @@ 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 @@ 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 @@ 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 @@ 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 0971c578b..44927b69c 100644 --- a/jellyfin_kodi/objects/movies.py +++ b/jellyfin_kodi/objects/movies.py @@ -8,7 +8,16 @@ 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 @@ def __init__(self, server, jellyfindb, videodb, direct_path, library=None): @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']) - - 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] + 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] 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 @@ def movie(self, item, e_item): 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']: - - 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] + 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"] + ) + + 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. - - Process movies inside boxset. - Process removals from boxset. - ''' - 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. + + 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.artwork.add(obj['Artwork'], obj['SetId'], "set") + 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.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 1ecf68779..310c2052b 100644 --- a/jellyfin_kodi/objects/music.py +++ b/jellyfin_kodi/objects/music.py @@ -39,19 +39,20 @@ def __init__(self, server, jellyfindb, musicdb, direct_path, library=None): @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 @@ def artist(self, item, e_item): # 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']) - - 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['Backdrops']: - obj['Backdrops'] = "%s" % obj['Backdrops'][0] + 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 "" + + if obj["Thumb"]: + obj["Thumb"] = "%s" % obj["Thumb"] + + 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 @@ def album(self, item, e_item): # 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']) - - 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['Thumb']: - obj['Thumb'] = "%s" % obj['Thumb'] + 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") + + if obj["DateAdded"]: + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + + if obj["Thumb"]: + obj["Thumb"] = "%s" % obj["Thumb"] if update: self.album_update(obj) @@ -169,89 +183,90 @@ def album(self, item, e_item): 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 @@ def song(self, item, e_item): # 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) - - if obj['DateAdded']: - obj['DateAdded'] = Local(obj['DateAdded']).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["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["DatePlayed"]: + obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + + obj["Index"] = obj["Disc"] * 2**16 + obj["Index"] if update: self.song_update(obj) @@ -303,227 +322,275 @@ def song(self, item, e_item): 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 @@ def remove_song(self, kodi_id, item_id): @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 93cc7b607..c2d280e44 100644 --- a/jellyfin_kodi/objects/musicvideos.py +++ b/jellyfin_kodi/objects/musicvideos.py @@ -43,24 +43,25 @@ def __init__(self, server, jellyfindb, videodb, direct_path, library=None): @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 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 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" + ] 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 @@ def musicvideo(self, item, e_item): # 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']) - - 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]) - - 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')) + 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("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")) 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 @@ def musicvideo(self, item, e_item): 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 f9015791f..ace80e12f 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. - - 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 - ''' + """Syntax to traverse the item dictionary. + This of the query almost as a url. + + 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 + """ self.mapped_item = {} if not mapping_name: @@ -62,7 +57,7 @@ def map(self, item, mapping_name): 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 @@ def map(self, item, mapping_name): 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 @@ def map(self, item, mapping_name): 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 @@ def map(self, item, mapping_name): 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 @@ def __recursiveloop__(self, obj, keys): def __recursive__(self, obj, keys): - for string in keys.split('/'): + for string in keys.split("/"): if not obj: return @@ -150,10 +155,10 @@ def __filters__(self, obj, filters): 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 31b89ba47..09485509e 100644 --- a/jellyfin_kodi/objects/tvshows.py +++ b/jellyfin_kodi/objects/tvshows.py @@ -11,7 +11,16 @@ 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 @@ 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 @@ def __init__(self, server, jellyfindb, videodb, direct_path, library=None, updat @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 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 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" + ] 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']) - - 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 + 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")) + + 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 @@ def tvshow(self, item, e_item): 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 @@ def tvshow_add(self, obj): # 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 the show is empty, try to remove it. - ''' - 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. + + 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. - - 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'] + """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" + ] 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']) - - 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']) + 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"]) 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 @@ def episode(self, item, e_item): 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) - - LOG.debug("ADD episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) + 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"], + ) 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 - - 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'] + """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" + ] 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 @@ def remove_episode(self, kodi_id, file_id, item_id): @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 31bbf7d12..1aec01412 100644 --- a/jellyfin_kodi/objects/utils.py +++ b/jellyfin_kodi/objects/utils.py @@ -14,8 +14,8 @@ 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 bb6a8bea7..ac7092082 100644 --- a/jellyfin_kodi/player.py +++ b/jellyfin_kodi/player.py @@ -44,11 +44,10 @@ def is_playing_file(self, file): 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 @@ def onPlayBackStarted(self): 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 @@ def onPlayBackStarted(self): if monitor.waitForAbort(2): return - items = window('jellyfin_play.json') + items = window("jellyfin_play.json") count += 1 if count == 20: @@ -90,51 +89,49 @@ def onPlayBackStarted(self): 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 @@ def set_item(self, file, item): 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') - - item.update({ - 'File': file, - 'CurrentPosition': item.get('CurrentPosition') or int(seektime), - 'Muted': muted, - 'Volume': volume, - 'Server': Jellyfin(item['ServerId']).get_client(), - 'Paused': False - }) + 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, + } + ) 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 @@ def set_audio_subs(self, audio=None, subtitle=None): 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 @@ def set_audio_subs(self, audio=None, subtitle=None): 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 @@ def onPlayBackPaused(self): 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 @@ def onPlayBackResumed(self): 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 @@ def report_playback(self, report=True): 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 @@ def report_playback(self, report=True): 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 @@ def stop_playback(self): 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 8569cb1ed..00eea84f8 100644 --- a/jellyfin_kodi/views.py +++ b/jellyfin_kodi/views.py @@ -19,86 +19,86 @@ 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 @@ def __init__(self): 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 @@ def remove_library(self, view_id): 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 @@ def get_libraries(self): 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 @@ def get_views(self): 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 @@ def get_nodes(self): 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 @@ def get_nodes(self): 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') - - label = xml.find('label') - label.text = view['Name'] - - content = xml.find('content') - content.text = view['Media'] - - match = xml.find('match') + 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"] + + content = xml.find("content") + content.text = view["Media"] + + 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 @@ def add_single_node(self, path, index, item_type, view): 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 @@ def add_node(self, index, file, view, node, name): 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 @@ def add_dynamic_node(self, index, file, view, node, name, path): 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" - - for rule in root.findall('.//rule'): - if rule.attrib['field'] == 'rating': - rule.find('value').text = "7" + 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" 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 @@ def order_media_folders(self, folders): 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 @@ def window_nodes(self): 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 @@ def window_nodes(self): 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 @@ def delete_playlist(self, path): 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 @@ def delete_node(self, path): 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 @@ def delete_nodes(self): 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 9ffa713bd..c19472d5b 100644 --- a/service.py +++ b/service.py @@ -14,16 +14,16 @@ ################################################################################################# 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 @@ def run(self): 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 @@ def run(self): 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 @@ def run(self): 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 4a4f43d8b..982f4515d 100644 --- a/tests/test_clean_none_dict_values.py +++ b/tests/test_clean_none_dict_values.py @@ -3,45 +3,51 @@ 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 7946604ae..6bf60859c 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 2cfc2a3d6..2859419b1 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: ... From 4834688128051fe2adda4fa3d0a6a17e64538bd7 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 2/2] Tool black: ignore blame --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..72cc270d2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Tool: black +77637622125a187c5b9cbe72b78c8bd3b26f754a