# -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals ################################################################################################# import json import sys from datetime import datetime # Workaround for threads using datetime: _striptime is locked import _strptime # noqa:F401 from kodi_six import xbmc, xbmcgui from six.moves import reload_module as reload from .. import objects from .. import connect from .. import client from .. import library from .. import monitor from ..views import Views from ..helper import ( translate, window, settings, event, dialog, set_addon_mode, LazyLogger, ) from ..helper.utils import JsonDebugPrinter, translate_path from ..helper.xmls import verify_kodi_defaults from ..jellyfin import Jellyfin ################################################################################################# LOG = LazyLogger(__name__) ################################################################################################# class Service(xbmc.Monitor): running = True library_thread = None monitor = None play_event = None warn = True 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) 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("Python Version: %s", sys.version) 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()) xbmc.Monitor.__init__(self) def service(self): """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. """ self.monitor = monitor.Monitor() player = self.monitor.player self.connect = connect.Connect() self.start_default() self.settings["mode"] = settings("useDirectPaths") while self.running: if window("jellyfin_online.bool"): 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 difference.seconds > 10: self.settings["last_progress"] = datetime.today() update = ( datetime.today() - self.settings["last_progress_report"] ).seconds > 250 event("ReportProgressRequested", {"Report": update}) if update: self.settings["last_progress_report"] = datetime.today() if window("jellyfin.restart.bool"): window("jellyfin.restart", clear=True) dialog( "notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False, ) raise Exception("RestartService") if self.waitForAbort(1): break self.shutdown() raise Exception("ExitService") def start_default(self): try: self.connect.register() if not settings("SyncInstallRunDone.bool"): set_addon_mode() except Exception as error: LOG.exception(error) def stop_default(self): window("jellyfin_online", clear=True) Jellyfin().close() if self.library_thread is not None: self.library_thread.stop_client() 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"): 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", ): return data = json.loads(data)[0] else: 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: 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 self.library_thread is None: self.library_thread = library.Library(self) self.library_thread.start() elif method in ("ServerUnreachable", "ServerShuttingDown"): 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, ) if data.get("ServerId") is None: self.stop_default() if self.waitForAbort(120): return 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, ) if data.get("ServerId") is None and self.settings["auth_check"]: self.settings["auth_check"] = False self.stop_default() if self.waitForAbort(5): return self.start_default() elif method == "ServerRestarting": if data.get("ServerId"): return if settings("restartMsg.bool"): dialog( "notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}", ) self.stop_default() if self.waitForAbort(15): return self.start_default() elif method == "ServerConnect": self.connect.register(data["Id"]) xbmc.executebuiltin("Container.Refresh") elif method == "AddServer": self.connect.setup_manual_server() xbmc.executebuiltin("Container.Refresh") elif method == "RemoveServer": self.connect.remove_server(data["Id"]) xbmc.executebuiltin("Container.Refresh") 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"): return LOG.info("[ UserDataChanged ] %s", data) self.library_thread.userdata(data["UserDataList"]) 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"]) elif method == "System.OnQuit": window("jellyfin_should_stop.bool", True) self.running = False elif method in ( "SyncLibrarySelection", "RepairLibrarySelection", "AddLibrarySelection", "RemoveLibrarySelection", ): self.library_thread.select_libraries(method) elif method == "SyncLibrary": if not data.get("Id"): return self.library_thread.add_library(data["Id"], data.get("Update", False)) xbmc.executebuiltin("Container.Refresh") elif method == "RepairLibrary": if not data.get("Id"): return libraries = data["Id"].split(",") for lib in libraries: if not self.library_thread.remove_library(lib): return self.library_thread.add_library(data["Id"]) xbmc.executebuiltin("Container.Refresh") elif method == "RemoveLibrary": libraries = data["Id"].split(",") for lib in libraries: if not self.library_thread.remove_library(lib): return xbmc.executebuiltin("Container.Refresh") elif method == "System.OnSleep": LOG.info("-->[ sleep ]") window("jellyfin_should_stop.bool", True) if self.library_thread is not None: self.library_thread.stop_client() self.library_thread = None Jellyfin.close_all() self.monitor.server = [] self.monitor.sleep = True elif method == "System.OnWake": if not self.monitor.sleep: LOG.warning("System.OnSleep was never called, skip System.OnWake") return LOG.info("--<[ sleep ]") xbmc.sleep(10000) # Allow network to wake up self.monitor.sleep = False window("jellyfin_should_stop", clear=True) try: self.connect.register() except Exception as error: LOG.exception(error) elif method == "GUI.OnScreensaverDeactivated": LOG.info("--<[ screensaver ]") xbmc.sleep(5000) if self.library_thread is not None: self.library_thread.fast_sync() 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"): return if settings("logLevel") != self.settings["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"]: 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"] ): 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 ): self.settings["mode"] = settings("useDirectPaths") LOG.info("New playback mode setting: %s", self.settings["mode"]) if not self.settings.get("mode_warn"): 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 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", ] for mod in reload_modules: del sys.modules[mod] reload(objects.kodi) reload(objects) reload(library) reload(monitor) objects.obj.Objects().mapping() LOG.info("---[ objects reloaded ]") def shutdown(self): LOG.info("---<[ EXITING ]") 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", ] for prop in properties: window(prop, clear=True) Jellyfin.close_all() if self.library_thread is not None: self.library_thread.stop_client() if self.monitor is not None: self.monitor.listener.stop() LOG.info("---<<<[ %s ]", client.get_addon_name())