From 949e8aac8000d1a6a76e0c13fdc45c01a8ea2956 Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 21 Jun 2018 19:24:54 +0300 Subject: [PATCH 1/3] Hot-swap themes --- medusa/__main__.py | 3 ++- medusa/app.py | 1 + medusa/config.py | 38 +++++++++++++++++++++++++++++ medusa/server/api/v2/base.py | 37 +++++++++++++++++----------- medusa/server/api/v2/config.py | 8 +++++- medusa/server/core.py | 4 +++ medusa/server/web/config/general.py | 3 ++- 7 files changed, 77 insertions(+), 17 deletions(-) diff --git a/medusa/__main__.py b/medusa/__main__.py index 6dd861f2c9..7f1be2c483 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -328,6 +328,7 @@ def start(self, args): # Initialize all available themes app.AVAILABLE_THEMES = read_themes() + app.DATA_ROOT = os.path.join(app.PROG_DIR, 'themes') # Load the config and publish it to the application package if self.console_logging and not os.path.isfile(app.CONFIG_FILE): @@ -374,7 +375,7 @@ def start(self, args): self.web_options = { 'port': int(self.start_port), 'host': self.web_host, - 'data_root': os.path.join(app.PROG_DIR, 'themes'), + 'data_root': app.DATA_ROOT, 'vue_root': os.path.join(app.PROG_DIR, 'vue'), 'web_root': app.WEB_ROOT, 'log_dir': self.log_dir, diff --git a/medusa/app.py b/medusa/app.py index 52606bb5e3..ce268dbd10 100644 --- a/medusa/app.py +++ b/medusa/app.py @@ -520,6 +520,7 @@ # UI THEME_NAME = None AVAILABLE_THEMES = [] +DATA_ROOT = None THEME = 'dark' THEME_PATH = None THEME_DATA_ROOT = None diff --git a/medusa/config.py b/medusa/config.py index 874ccfbbcf..f0b405c296 100644 --- a/medusa/config.py +++ b/medusa/config.py @@ -39,6 +39,8 @@ from six import iteritems, string_types, text_type from six.moves.urllib.parse import urlunsplit, uses_netloc +from tornado.web import StaticFileHandler + log = BraceAdapter(logging.getLogger(__name__)) log.logger.addHandler(logging.NullHandler()) @@ -473,6 +475,42 @@ def change_remove_from_client(new_state): log.info(u'Stopping TORRENTCHECKER thread') +def change_theme(theme_name): + """ + Hot-swap theme. + + :param theme_name: New theme name + """ + if theme_name == app.THEME_NAME: + return False + + old_theme_name = app.THEME_NAME + old_data_root = os.path.join(app.DATA_ROOT, old_theme_name) + + app.THEME_NAME = theme_name + app.THEME_DATA_ROOT = os.path.join(app.DATA_ROOT, theme_name) + + static_file_handlers = app.instance.web_server.app.static_file_handlers + + log.info('Switching theme from "{old}" to "{new}"', {'old': old_theme_name, 'new': theme_name}) + + for rule in static_file_handlers.target.rules: + if old_data_root not in rule.target_kwargs['path']: + # Skip other static file handlers + continue + + old_path = rule.target_kwargs['path'] + new_path = old_path.replace(old_data_root, app.THEME_DATA_ROOT) + rule.target_kwargs['path'] = new_path + + log.debug('Changed {old} to {new}', {'old': old_path, 'new': new_path}) + + # Reset cache + StaticFileHandler.reset() + + return True + + def CheckSection(CFG, sec): """ Check if INI section exists, if not create it """ diff --git a/medusa/server/api/v2/base.py b/medusa/server/api/v2/base.py index 6f3fecf8c5..11f7b857f6 100644 --- a/medusa/server/api/v2/base.py +++ b/medusa/server/api/v2/base.py @@ -389,8 +389,8 @@ def set_nested_value(data, key, value): class PatchField(object): """Represent a field to be patched.""" - def __init__(self, target_type, attr, attr_type, - validator=None, converter=None, default_value=None, post_processor=None): + def __init__(self, target_type, attr, attr_type, validator=None, converter=None, + default_value=None, setter=None, post_processor=None): """Constructor.""" if not hasattr(target_type, attr): raise ValueError('{0!r} has no attribute {1}'.format(target_type, attr)) @@ -401,6 +401,7 @@ def __init__(self, target_type, attr, attr_type, self.validator = validator or (lambda v: isinstance(v, self.attr_type)) self.converter = converter or (lambda v: v) self.default_value = default_value + self.setter = setter self.post_processor = post_processor def patch(self, target, value): @@ -413,7 +414,10 @@ def patch(self, target, value): if valid: try: - setattr(target, self.attr, self.converter(value)) + if self.setter: + self.setter(target, self.attr, self.converter(value)) + else: + setattr(target, self.attr, self.converter(value)) except AttributeError: log.warning( 'Error trying to change attribute %s on target %s, you sure' @@ -431,44 +435,49 @@ def patch(self, target, value): class StringField(PatchField): """Patch string fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, post_processor=None): + def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + setter=None, post_processor=None): """Constructor.""" super(StringField, self).__init__(target_type, attr, string_types, validator=validator, converter=converter, - default_value=default_value, post_processor=post_processor) + default_value=default_value, setter=setter, post_processor=post_processor) class IntegerField(PatchField): """Patch integer fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, post_processor=None): + def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + setter=None, post_processor=None): """Constructor.""" super(IntegerField, self).__init__(target_type, attr, int, validator=validator, converter=converter, - default_value=default_value, post_processor=post_processor) + default_value=default_value, setter=setter, post_processor=post_processor) class ListField(PatchField): """Patch list fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, post_processor=None): + def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + setter=None, post_processor=None): """Constructor.""" super(ListField, self).__init__(target_type, attr, list, validator=validator, converter=converter, - default_value=default_value, post_processor=post_processor) + default_value=default_value, setter=setter, post_processor=post_processor) class BooleanField(PatchField): """Patch boolean fields.""" - def __init__(self, target_type, attr, validator=None, converter=int, default_value=None, post_processor=None): + def __init__(self, target_type, attr, validator=None, converter=int, default_value=None, + setter=None, post_processor=None): """Constructor.""" super(BooleanField, self).__init__(target_type, attr, bool, validator=validator, converter=converter, - default_value=default_value, post_processor=post_processor) + default_value=default_value, setter=setter, post_processor=post_processor) class EnumField(PatchField): """Patch enumeration fields.""" - def __init__(self, target_type, attr, enums, attr_type=text_type, - converter=None, default_value=None, post_processor=None): + def __init__(self, target_type, attr, enums, attr_type=text_type, converter=None, + default_value=None, setter=None, post_processor=None): """Constructor.""" super(EnumField, self).__init__(target_type, attr, attr_type, validator=lambda v: v in enums, - converter=converter, default_value=default_value, post_processor=post_processor) + converter=converter, default_value=default_value, + setter=setter, post_processor=post_processor) diff --git a/medusa/server/api/v2/config.py b/medusa/server/api/v2/config.py index 46e4d4c9a3..0013cd7f28 100644 --- a/medusa/server/api/v2/config.py +++ b/medusa/server/api/v2/config.py @@ -10,6 +10,7 @@ from medusa import ( app, common, + config, db, ) from medusa.helper.mappings import NonEmptyDict @@ -38,6 +39,11 @@ def layout_schedule_post_processor(v): app.COMING_EPS_SORT = 'date' +def theme_name_setter(object, name, value): + """Hot-swap theme.""" + config.change_theme(value) + + class ConfigHandler(BaseRequestHandler): """Config request handler.""" @@ -69,7 +75,7 @@ class ConfigHandler(BaseRequestHandler): 'layout.show.allSeasons': BooleanField(app, 'DISPLAY_ALL_SEASONS'), 'layout.show.specials': BooleanField(app, 'DISPLAY_SHOW_SPECIALS'), 'layout.show.showListOrder': ListField(app, 'SHOW_LIST_ORDER'), - 'theme.name': StringField(app, 'THEME_NAME'), + 'theme.name': StringField(app, 'THEME_NAME', setter=theme_name_setter), 'backlogOverview.period': StringField(app, 'BACKLOG_PERIOD'), 'backlogOverview.status': StringField(app, 'BACKLOG_STATUS'), 'rootDirs': ListField(app, 'ROOT_DIRS'), diff --git a/medusa/server/core.py b/medusa/server/core.py index 8569f0e057..b49c2ed185 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -237,6 +237,10 @@ def __init__(self, options=None): {'path': os.path.join(self.options['theme_data_root'], 'index.html'), 'default_filename': 'index.html'}), ]) + # Used for hot-swapping themes + # This is the 2nd rule from the end, because the last one is always `self.app.wildcard_router` + self.app.static_file_handlers = self.app.default_router.rules[-2] + # API v1 handlers self.app.add_handlers('.*$', [ # Main handler diff --git a/medusa/server/web/config/general.py b/medusa/server/web/config/general.py index 091eb0c0e3..3dce0025b5 100644 --- a/medusa/server/web/config/general.py +++ b/medusa/server/web/config/general.py @@ -193,7 +193,8 @@ def saveGeneral(self, log_dir=None, log_nr=5, log_size=1, web_port=None, notify_ app.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) - app.THEME_NAME = theme_name + config.change_theme(theme_name) + app.LAYOUT_WIDE = config.checkbox_to_value(layout_wide) app.FANART_BACKGROUND = config.checkbox_to_value(fanart_background) app.FANART_BACKGROUND_OPACITY = fanart_background_opacity From f59a345198f62db40d8ddbac0688af40de4059fe Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 21 Jun 2018 19:24:54 +0300 Subject: [PATCH 2/3] Fix favicon.ico route --- medusa/server/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/medusa/server/core.py b/medusa/server/core.py index b49c2ed185..605d854d4f 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -201,8 +201,8 @@ def __init__(self, options=None): # Static File Handlers self.app.add_handlers('.*$', [ # favicon - (r'{base}/(favicon\.ico)'.format(base=self.options['theme_path']), StaticFileHandler, - {'path': os.path.join(self.options['theme_data_root'], 'assets', 'img/ico/favicon.ico')}), + (r'{base}/favicon\.ico()'.format(base=self.options['theme_path']), StaticFileHandler, + {'path': os.path.join(self.options['theme_data_root'], 'assets', 'img', 'ico', 'favicon.ico')}), # images (r'{base}/images/(.*)'.format(base=self.options['theme_path']), StaticFileHandler, From 229fb8da6eb33876cf17e3ac0ff01b405f221d5e Mon Sep 17 00:00:00 2001 From: sharkykh Date: Thu, 21 Jun 2018 19:24:55 +0300 Subject: [PATCH 3/3] APIv2: Refactor 'target_type' => 'target' --- medusa/server/api/v2/base.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/medusa/server/api/v2/base.py b/medusa/server/api/v2/base.py index 11f7b857f6..a32a95d0b9 100644 --- a/medusa/server/api/v2/base.py +++ b/medusa/server/api/v2/base.py @@ -389,13 +389,13 @@ def set_nested_value(data, key, value): class PatchField(object): """Represent a field to be patched.""" - def __init__(self, target_type, attr, attr_type, validator=None, converter=None, + def __init__(self, target, attr, attr_type, validator=None, converter=None, default_value=None, setter=None, post_processor=None): """Constructor.""" - if not hasattr(target_type, attr): - raise ValueError('{0!r} has no attribute {1}'.format(target_type, attr)) + if not hasattr(target, attr): + raise ValueError('{0!r} has no attribute {1}'.format(target, attr)) - self.target_type = target_type + self.target = target self.attr = attr self.attr_type = attr_type self.validator = validator or (lambda v: isinstance(v, self.attr_type)) @@ -435,49 +435,49 @@ def patch(self, target, value): class StringField(PatchField): """Patch string fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + def __init__(self, target, attr, validator=None, converter=None, default_value=None, setter=None, post_processor=None): """Constructor.""" - super(StringField, self).__init__(target_type, attr, string_types, validator=validator, converter=converter, + super(StringField, self).__init__(target, attr, string_types, validator=validator, converter=converter, default_value=default_value, setter=setter, post_processor=post_processor) class IntegerField(PatchField): """Patch integer fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + def __init__(self, target, attr, validator=None, converter=None, default_value=None, setter=None, post_processor=None): """Constructor.""" - super(IntegerField, self).__init__(target_type, attr, int, validator=validator, converter=converter, + super(IntegerField, self).__init__(target, attr, int, validator=validator, converter=converter, default_value=default_value, setter=setter, post_processor=post_processor) class ListField(PatchField): """Patch list fields.""" - def __init__(self, target_type, attr, validator=None, converter=None, default_value=None, + def __init__(self, target, attr, validator=None, converter=None, default_value=None, setter=None, post_processor=None): """Constructor.""" - super(ListField, self).__init__(target_type, attr, list, validator=validator, converter=converter, + super(ListField, self).__init__(target, attr, list, validator=validator, converter=converter, default_value=default_value, setter=setter, post_processor=post_processor) class BooleanField(PatchField): """Patch boolean fields.""" - def __init__(self, target_type, attr, validator=None, converter=int, default_value=None, + def __init__(self, target, attr, validator=None, converter=int, default_value=None, setter=None, post_processor=None): """Constructor.""" - super(BooleanField, self).__init__(target_type, attr, bool, validator=validator, converter=converter, + super(BooleanField, self).__init__(target, attr, bool, validator=validator, converter=converter, default_value=default_value, setter=setter, post_processor=post_processor) class EnumField(PatchField): """Patch enumeration fields.""" - def __init__(self, target_type, attr, enums, attr_type=text_type, converter=None, + def __init__(self, target, attr, enums, attr_type=text_type, converter=None, default_value=None, setter=None, post_processor=None): """Constructor.""" - super(EnumField, self).__init__(target_type, attr, attr_type, validator=lambda v: v in enums, + super(EnumField, self).__init__(target, attr, attr_type, validator=lambda v: v in enums, converter=converter, default_value=default_value, setter=setter, post_processor=post_processor)