From 3de5b2e43d2f79a285b20c61602bb9739b942a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 15 Sep 2025 02:43:39 +0200 Subject: [PATCH 1/8] Restructure stop decorator and LibraryException --- jellyfin_kodi/full_sync.py | 21 +++++++++---- jellyfin_kodi/helper/exceptions.py | 14 +++++---- jellyfin_kodi/helper/utils.py | 12 -------- jellyfin_kodi/helper/wrapper.py | 18 ++++++----- jellyfin_kodi/library.py | 48 ++++++++++++++++++++---------- 5 files changed, 67 insertions(+), 46 deletions(-) diff --git a/jellyfin_kodi/full_sync.py b/jellyfin_kodi/full_sync.py index 36453ada..f3b0d126 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -13,7 +13,12 @@ from .objects import Movies, TVShows, MusicVideos, Music from .database import Database, get_sync, save_sync, jellyfin_db from .helper import translate, settings, window, progress, dialog, LazyLogger, xmls from .helper.utils import get_screensaver, set_screensaver -from .helper.exceptions import LibraryException, PathValidationException +from .helper.exceptions import ( + LibraryException, + LibraryExitException, + LibrarySyncLaterException, + PathValidationException, +) ################################################################################################## @@ -169,11 +174,13 @@ class FullSync(object): selection = dialog("multi", translate(33120), choices) if selection is None: - raise LibraryException("LibrarySelection") + # TODO: Why are we handling these two differently? + # presumably one means the dialog got aborted, the other means that we just pressed ok without selecting any libraries + raise LibraryException("Library selection dialog returned None.") elif not selection: LOG.info("Nothing was selected.") - raise LibraryException("SyncLibraryLater") + raise LibrarySyncLaterException("No libraries where selected, sync later.") if 0 in selection: selection = list(range(1, len(libraries) + 1)) @@ -275,11 +282,13 @@ class FullSync(object): media[library["CollectionType"]](library) except LibraryException as error: - - if error.status == "StopCalled": + # TODO: Fixme; We're catching all LibraryException here, + # but silently ignoring any that isn't the exit condition. + # Investigate what would be appropriate behavior here. + if isinstance(error, LibraryExitException): save_sync(self.sync) - raise + LOG.warning("Ignoring exception %s", error) except PathValidationException: raise diff --git a/jellyfin_kodi/helper/exceptions.py b/jellyfin_kodi/helper/exceptions.py index 712a64a9..9849ec54 100644 --- a/jellyfin_kodi/helper/exceptions.py +++ b/jellyfin_kodi/helper/exceptions.py @@ -19,9 +19,15 @@ class HTTPException(Exception): class LibraryException(Exception): - # Jellyfin library sync exception - def __init__(self, status): - self.status = status + pass + + +class LibraryExitException(LibraryException): + "Exception raised to propagate application exit." + + +class LibrarySyncLaterException(LibraryException): + "Raised when no libraries are selected for sync." class PathValidationException(Exception): @@ -30,5 +36,3 @@ class PathValidationException(Exception): TODO: Investigate the usage of this to see if it can be done better. """ - - pass diff --git a/jellyfin_kodi/helper/utils.py b/jellyfin_kodi/helper/utils.py index d3d780d8..60efdaf9 100644 --- a/jellyfin_kodi/helper/utils.py +++ b/jellyfin_kodi/helper/utils.py @@ -162,18 +162,6 @@ def dialog(dialog_type, *args, **kwargs): return types[dialog_type](*args, **kwargs) -def should_stop(): - """Checkpoint during the sync process.""" - if xbmc.Monitor().waitForAbort(0.00001): - return True - - if window("jellyfin_should_stop.bool"): - LOG.info("exiiiiitttinggg") - return True - - return not window("jellyfin_online.bool") - - def get_screensaver(): """Get the current screensaver value.""" result = JSONRPC("Settings.getSettingValue").execute( diff --git a/jellyfin_kodi/helper/wrapper.py b/jellyfin_kodi/helper/wrapper.py index b1cb4199..0750db5f 100644 --- a/jellyfin_kodi/helper/wrapper.py +++ b/jellyfin_kodi/helper/wrapper.py @@ -4,11 +4,12 @@ from __future__ import division, absolute_import, print_function, unicode_litera ################################################################################################# import xbmcgui +import xbmc from . import LazyLogger -from .utils import should_stop -from .exceptions import LibraryException +from .utils import window +from .exceptions import LibraryExitException from .translate import translate ################################################################################################# @@ -55,14 +56,15 @@ def stop(func): def wrapper(*args, **kwargs): - try: - if should_stop(): # ??? TODO: Fixme - raise Exception + if xbmc.Monitor().waitForAbort(0.00001): + raise LibraryExitException("Kodi aborted, exiting...") - except Exception as error: - LOG.exception(error) + if window("jellyfin_should_stop.bool"): + LOG.info("exiiiiitttinggg") + raise LibraryExitException("Should stop flag raised, exiting...") - raise LibraryException("StopCalled") + if not window("jellyfin_online.bool"): + raise LibraryExitException("Jellyfin not online, exiting...") return func(*args, **kwargs) diff --git a/jellyfin_kodi/library.py b/jellyfin_kodi/library.py index 2d75313e..c3cacc84 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -19,7 +19,11 @@ from .views import Views from .downloader import GetItemWorker from .helper import translate, api, stop, settings, window, dialog, event, LazyLogger from .helper.utils import split_list, set_screensaver, get_screensaver -from .helper.exceptions import LibraryException +from .helper.exceptions import ( + LibraryException, + LibraryExitException, + LibrarySyncLaterException, +) from .jellyfin import Jellyfin ################################################################################################## @@ -93,7 +97,8 @@ class Library(threading.Thread): try: self.service() - except LibraryException: + except LibraryException as error: + LOG.warning(error) break except Exception as error: LOG.exception(error) @@ -438,19 +443,20 @@ class Library(threading.Thread): return True return True + + except LibrarySyncLaterException as error: + LOG.error(error.status) + dialog("ok", "{jellyfin}", translate(33129)) + settings("SyncInstallRunDone.bool", True) + sync = get_sync() + sync["Libraries"] = [] + save_sync(sync) + + return True + except LibraryException as error: LOG.error(error.status) - if error.status in "SyncLibraryLater": - - dialog("ok", "{jellyfin}", translate(33129)) - settings("SyncInstallRunDone.bool", True) - sync = get_sync() - sync["Libraries"] = [] - save_sync(sync) - - return True - except Exception as error: LOG.exception(error) @@ -758,8 +764,12 @@ class UpdateWorker(threading.Thread): (item["Type"], api.API(item).get_naming()) ) except LibraryException as error: - if error.status == "StopCalled": + # TODO: Fixme; We're catching all LibraryException here, + # but silently ignoring any that isn't the exit condition. + # Investigate what would be appropriate behavior here. + if isinstance(error, LibraryExitException): break + LOG.warning("Ignoring exception %s", error) except Exception as error: LOG.exception(error) @@ -823,8 +833,12 @@ class UserDataWorker(threading.Thread): elif item["Type"] == "Audio": music.userdata(item) except LibraryException as error: - if error.status == "StopCalled": + # TODO: Fixme; We're catching all LibraryException here, + # but silently ignoring any that isn't the exit condition. + # Investigate what would be appropriate behavior here. + if isinstance(error, LibraryExitException): break + LOG.warning("Ignoring exception %s", error) except Exception as error: LOG.exception(error) @@ -943,8 +957,12 @@ class RemovedWorker(threading.Thread): try: obj(item["Id"]) except LibraryException as error: - if error.status == "StopCalled": + # TODO: Fixme; We're catching all LibraryException here, + # but silently ignoring any that isn't the exit condition. + # Investigate what would be appropriate behavior here. + if isinstance(error, LibraryExitException): break + LOG.warning("Ignoring exception %s", error) except Exception as error: LOG.exception(error) finally: From 36652fc5cdaeb83f54ff017de5906480a3e29d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 15 Sep 2025 06:32:10 +0200 Subject: [PATCH 2/8] Add stubs to LazyLogger for code completion --- jellyfin_kodi/helper/lazylogger.py | 86 +++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/jellyfin_kodi/helper/lazylogger.py b/jellyfin_kodi/helper/lazylogger.py index bfd15f4b..fa4981a9 100644 --- a/jellyfin_kodi/helper/lazylogger.py +++ b/jellyfin_kodi/helper/lazylogger.py @@ -3,7 +3,8 @@ from __future__ import division, absolute_import, print_function, unicode_litera class LazyLogger(object): - """`helper.loghandler.getLogger()` is used everywhere. + """ + `helper.loghandler.getLogger()` is used everywhere. This class helps to avoid import errors. """ @@ -19,3 +20,86 @@ class LazyLogger(object): self.__logger = getLogger(self.__logger_name) return getattr(self.__logger, name) + + ##################################################################### + # Following are stubs of methods provided by `logging.Logger`. # + # Please ensure any actually functional code is above this comment. # + ##################################################################### + + def setLevel(self, level): + """ + Set the logging level of this logger. level must be an int or a str. + """ + ... + + def debug(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'DEBUG'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.debug("Houston, we have a %s", "thorny problem", exc_info=1) + """ + ... + + def info(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'INFO'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.info("Houston, we have a %s", "interesting problem", exc_info=1) + """ + ... + + def warning(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'WARNING'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.warning("Houston, we have a %s", "bit of a problem", exc_info=1) + """ + ... + + def error(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'ERROR'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.error("Houston, we have a %s", "major problem", exc_info=1) + """ + ... + + def exception(self, msg, *args, exc_info=True, **kwargs): + """ + Convenience method for logging an ERROR with exception information. + """ + ... + + def critical(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'CRITICAL'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.critical("Houston, we have a %s", "major disaster", exc_info=1) + """ + ... + + def log(self, level, msg, *args, **kwargs): + """ + Log 'msg % args' with the integer severity 'level'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.log(level, "We have a %s", "mysterious problem", exc_info=1) + """ + ... From 614c5ec02b2c66c88302165acb2726b9d5ea3fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Tue, 16 Sep 2025 05:26:28 +0200 Subject: [PATCH 3/8] Fix flake8 complaints --- .gitignore | 1 + jellyfin_kodi/entrypoint/service.py | 1 + jellyfin_kodi/helper/api.py | 3 ++- tox.ini | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7dfead0d..bc081898 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ Thumbs.db !.vscode/extensions.json !.vscode/settings.json pyinstrument/ +.venv/ # Now managed by templates addon.xml diff --git a/jellyfin_kodi/entrypoint/service.py b/jellyfin_kodi/entrypoint/service.py index e267571d..8702be88 100644 --- a/jellyfin_kodi/entrypoint/service.py +++ b/jellyfin_kodi/entrypoint/service.py @@ -10,6 +10,7 @@ from importlib import reload # Workaround for threads using datetime: _striptime is locked import _strptime # noqa:F401 + import xbmc import xbmcgui diff --git a/jellyfin_kodi/helper/api.py b/jellyfin_kodi/helper/api.py index 5ec3736c..da4a0971 100644 --- a/jellyfin_kodi/helper/api.py +++ b/jellyfin_kodi/helper/api.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals +import json + ################################################################################################## from . import settings, LazyLogger from .utils import translate_path -import json ################################################################################################## diff --git a/tox.ini b/tox.ini index e353deb0..1c30f574 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [flake8] max-line-length = 9999 import-order-style = pep8 -exclude = .git,.vscode +exclude = .git,.vscode,.venv extend-ignore = I202 E203 From 2272150f93b4290b5f547697dbc257f569e1c55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Tue, 16 Sep 2025 05:27:11 +0200 Subject: [PATCH 4/8] Add mypy configuration --- .vscode/extensions.json | 3 ++- mypy.ini | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 mypy.ini diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 19cfbb8b..d197a994 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "ms-vscode-remote.remote-containers", - "ms-python.black-formatter" + "ms-python.black-formatter", + "ms-python.mypy-type-checker" ] } diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..526cc57f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +check_untyped_defs = True +warn_unused_configs = True +files = . From 402be319cad3d0f98bf0ccca8d8bfd70506585a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Thu, 18 Sep 2025 17:32:28 +0200 Subject: [PATCH 5/8] Gate stubs behind TYPE_CHECKING --- jellyfin_kodi/helper/lazylogger.py | 120 +++++++++++++++-------------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/jellyfin_kodi/helper/lazylogger.py b/jellyfin_kodi/helper/lazylogger.py index fa4981a9..5ea9fffa 100644 --- a/jellyfin_kodi/helper/lazylogger.py +++ b/jellyfin_kodi/helper/lazylogger.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals +from typing import TYPE_CHECKING + class LazyLogger(object): """ @@ -26,80 +28,82 @@ class LazyLogger(object): # Please ensure any actually functional code is above this comment. # ##################################################################### - def setLevel(self, level): - """ - Set the logging level of this logger. level must be an int or a str. - """ - ... + if TYPE_CHECKING: - def debug(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'DEBUG'. + def setLevel(self, level): + """ + Set the logging level of this logger. level must be an int or a str. + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def debug(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'DEBUG'. - logger.debug("Houston, we have a %s", "thorny problem", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. - def info(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'INFO'. + logger.debug("Houston, we have a %s", "thorny problem", exc_info=1) + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def info(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'INFO'. - logger.info("Houston, we have a %s", "interesting problem", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. - def warning(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'WARNING'. + logger.info("Houston, we have a %s", "interesting problem", exc_info=1) + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def warning(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'WARNING'. - logger.warning("Houston, we have a %s", "bit of a problem", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. - def error(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'ERROR'. + logger.warning("Houston, we have a %s", "bit of a problem", exc_info=1) + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def error(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'ERROR'. - logger.error("Houston, we have a %s", "major problem", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. - def exception(self, msg, *args, exc_info=True, **kwargs): - """ - Convenience method for logging an ERROR with exception information. - """ - ... + logger.error("Houston, we have a %s", "major problem", exc_info=1) + """ + ... - def critical(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'CRITICAL'. + def exception(self, msg, *args, exc_info=True, **kwargs): + """ + Convenience method for logging an ERROR with exception information. + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def critical(self, msg, *args, **kwargs): + """ + Log 'msg % args' with severity 'CRITICAL'. - logger.critical("Houston, we have a %s", "major disaster", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. - def log(self, level, msg, *args, **kwargs): - """ - Log 'msg % args' with the integer severity 'level'. + logger.critical("Houston, we have a %s", "major disaster", exc_info=1) + """ + ... - To pass exception information, use the keyword argument exc_info with - a true value, e.g. + def log(self, level, msg, *args, **kwargs): + """ + Log 'msg % args' with the integer severity 'level'. - logger.log(level, "We have a %s", "mysterious problem", exc_info=1) - """ - ... + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.log(level, "We have a %s", "mysterious problem", exc_info=1) + """ + ... From 8c467be69f19b5ecf80a8f37dc03e0ee10e06042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Thu, 18 Sep 2025 21:28:33 +0200 Subject: [PATCH 6/8] Remove missed references to LibraryException.status --- jellyfin_kodi/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jellyfin_kodi/library.py b/jellyfin_kodi/library.py index c3cacc84..13604d9a 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -445,7 +445,7 @@ class Library(threading.Thread): return True except LibrarySyncLaterException as error: - LOG.error(error.status) + LOG.error(error) dialog("ok", "{jellyfin}", translate(33129)) settings("SyncInstallRunDone.bool", True) sync = get_sync() @@ -455,7 +455,7 @@ class Library(threading.Thread): return True except LibraryException as error: - LOG.error(error.status) + LOG.error(error) except Exception as error: LOG.exception(error) From 308daec219200904fd41db6d1f9ace259b7d3fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Thu, 18 Sep 2025 22:33:30 +0200 Subject: [PATCH 7/8] Add functools wraps --- jellyfin_kodi/helper/wrapper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jellyfin_kodi/helper/wrapper.py b/jellyfin_kodi/helper/wrapper.py index 0750db5f..a559bcee 100644 --- a/jellyfin_kodi/helper/wrapper.py +++ b/jellyfin_kodi/helper/wrapper.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals +from functools import wraps + ################################################################################################# import xbmcgui @@ -23,6 +25,7 @@ def progress(message=None): """Will start and close the progress dialog.""" def decorator(func): + @wraps(func) def wrapper(self, item=None, *args, **kwargs): dialog = xbmcgui.DialogProgressBG() @@ -54,6 +57,7 @@ def progress(message=None): def stop(func): """Wrapper to catch exceptions and return using catch""" + @wraps(func) def wrapper(*args, **kwargs): if xbmc.Monitor().waitForAbort(0.00001): @@ -74,6 +78,7 @@ def stop(func): def jellyfin_item(func): """Wrapper to retrieve the jellyfin_db item.""" + @wraps(func) def wrapper(self, item, *args, **kwargs): e_item = self.jellyfin_db.get_item_by_id( item["Id"] if isinstance(item, dict) else item From ae543f86f0468913a08b0255443f82cc4351d144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Thu, 18 Sep 2025 22:41:24 +0200 Subject: [PATCH 8/8] Fix spelling --- jellyfin_kodi/full_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jellyfin_kodi/full_sync.py b/jellyfin_kodi/full_sync.py index f3b0d126..1448028a 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -180,7 +180,7 @@ class FullSync(object): elif not selection: LOG.info("Nothing was selected.") - raise LibrarySyncLaterException("No libraries where selected, sync later.") + raise LibrarySyncLaterException("No libraries were selected, sync later.") if 0 in selection: selection = list(range(1, len(libraries) + 1))