diff --git a/data/www/js/global.js b/data/www/js/global.js new file mode 100644 index 0000000..2f7570d --- /dev/null +++ b/data/www/js/global.js @@ -0,0 +1,19 @@ +jQuery(document).ready(function ($) { + + $(document).on('click', '.protected', function(e) { + e.preventDefault(); + e.stopPropagation(); + + if (confirm(l.js_common_are_you_sure)) { + if ($(this).is('a')) { + if ($(this).attr('target') == '_blank') { + window.open($(this).attr('href')); + } else { + document.location.href = $(this).attr('href'); + } + } + } + + return false; + }) +}); \ No newline at end of file diff --git a/docs/setup-run-headless.md b/docs/setup-run-headless.md index a0d9a2f..5d49715 100644 --- a/docs/setup-run-headless.md +++ b/docs/setup-run-headless.md @@ -114,6 +114,7 @@ chromium --disable-features=Translate --ignore-certificate-errors --disable-web- - Execute following script ```bash cd ~/obscreen +git pull source ./venv/bin/activate pip install -r requirements.txt sudo systemctl restart obscreen-studio.service diff --git a/docs/setup-run-on-rpi.md b/docs/setup-run-on-rpi.md index 4b842f0..be6a5aa 100644 --- a/docs/setup-run-on-rpi.md +++ b/docs/setup-run-on-rpi.md @@ -137,6 +137,7 @@ However, I used this one: `(2,82) = 1920x1080 60Hz 1080p` - Execute following script ```bash cd ~/obscreen +git pull source ./venv/bin/activate pip install -r requirements.txt sudo systemctl restart obscreen-studio.service diff --git a/lang/en.json b/lang/en.json index 7e64b06..be17d43 100644 --- a/lang/en.json +++ b/lang/en.json @@ -165,6 +165,7 @@ "common_unknown_ipaddr": "Unknown IP address", "common_empty": "[Empty]", + "common_are_you_sure": "Are you sure?", "logout": "Logout", "login_error_not_found": "Bad credentials", "login_error_bad_credentials": "Bad credentials", diff --git a/lang/fr.json b/lang/fr.json index 52b3dc6..59f5680 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -165,6 +165,7 @@ "common_unknown_ipaddr": "Adresse IP inconnue", "common_empty": "[Vide]", + "common_are_you_sure": "Êtes-vous sûr ?", "logout": "Déconnexion", "login_error_not_found": "Identifiants invalides", "login_error_bad_credentials": "Identifiants invalides", diff --git a/plugins/system/GitUpdater/GitUpdater.py b/plugins/system/GitUpdater/GitUpdater.py new file mode 100644 index 0000000..5c85a42 --- /dev/null +++ b/plugins/system/GitUpdater/GitUpdater.py @@ -0,0 +1,24 @@ +from src.interface.ObPlugin import ObPlugin + +from typing import List, Dict +from src.model.entity.Variable import Variable +from src.model.enum.VariableType import VariableType +from src.model.enum.HookType import HookType +from src.model.hook.HookRegistration import HookRegistration + + +class GitUpdater(ObPlugin): + + def use_id(self): + return 'git_updater' + + def use_title(self): + return 'Git Updater' + + def use_variables(self) -> List[Variable]: + return [] + + def use_hooks_registrations(self) -> List[HookRegistration]: + return [ + super().add_static_hook_registration(hook=HookType.H_SYSINFO_TOOLBAR_ACTIONS_START, priority=10), + ] diff --git a/plugins/system/GitUpdater/controller/GitUpdaterController.py b/plugins/system/GitUpdater/controller/GitUpdaterController.py new file mode 100644 index 0000000..5f32f72 --- /dev/null +++ b/plugins/system/GitUpdater/controller/GitUpdaterController.py @@ -0,0 +1,20 @@ +from flask import Flask, redirect, url_for +from src.interface.ObController import ObController +from src.utils import run_system_command, sudo_run_system_command, get_working_directory +from src.Application import Application + + +class GitUpdaterController(ObController): + + def register(self): + self._app.add_url_rule('/git-updater/update/now', 'git_updater_update_now', self._auth(self.update_now), methods=['GET']) + + def update_now(self): + sudo_run_system_command(['apt', 'install'] + ''.join('git python3-pip python3-venv libsqlite3-dev')) + run_system_command(['git', '-C', get_working_directory(), 'stash']) + run_system_command(['git', '-C', get_working_directory(), 'checkout', 'tags/v{}'.format(Application.get_version)]) + run_system_command(['git', '-C', get_working_directory(), 'pull']) + run_system_command(['pip', 'install', '-r', 'requirements.txt']) + sudo_run_system_command(['systemctl', 'restart', Application.get_name()]) + + return redirect(url_for('sysinfo_attribute_list')) diff --git a/plugins/system/GitUpdater/lang/en.json b/plugins/system/GitUpdater/lang/en.json new file mode 100644 index 0000000..a0c2cf9 --- /dev/null +++ b/plugins/system/GitUpdater/lang/en.json @@ -0,0 +1,3 @@ +{ + "button_update": "Update" +} diff --git a/plugins/system/GitUpdater/lang/fr.json b/plugins/system/GitUpdater/lang/fr.json new file mode 100644 index 0000000..5da8f45 --- /dev/null +++ b/plugins/system/GitUpdater/lang/fr.json @@ -0,0 +1,3 @@ +{ + "button_update": "Mettre à jour" +} diff --git a/plugins/system/GitUpdater/views/hook/h_sysinfo_toolbar_actions_start.jinja.html b/plugins/system/GitUpdater/views/hook/h_sysinfo_toolbar_actions_start.jinja.html new file mode 100644 index 0000000..5ac7986 --- /dev/null +++ b/plugins/system/GitUpdater/views/hook/h_sysinfo_toolbar_actions_start.jinja.html @@ -0,0 +1,2 @@ +{{ l.git_updater_button_update }} + diff --git a/src/Application.py b/src/Application.py index 50dfd38..0988bd7 100644 --- a/src/Application.py +++ b/src/Application.py @@ -12,6 +12,7 @@ from src.model.enum.HookType import HookType class Application: + def __init__(self, project_dir: str): self._project_dir = project_dir self._stop_event = threading.Event() @@ -19,7 +20,7 @@ class Application: self._template_renderer = TemplateRenderer(project_dir=project_dir, model_store=self._model_store, render_hook=self.render_hook) self._web_server = WebServer(project_dir=project_dir, model_store=self._model_store, template_renderer=self._template_renderer) - logging.info("[Obscreen] Starting...") + logging.info("[obscreen] Starting...") self._plugin_store = PluginStore(project_dir=project_dir, model_store=self._model_store, template_renderer=self._template_renderer, web_server=self._web_server) signal.signal(signal.SIGINT, self.signal_handler) @@ -35,3 +36,11 @@ class Application: def render_hook(self, hook: HookType) -> str: return self._template_renderer.render_hooks(self._plugin_store.map_hooks()[hook]) + @staticmethod + def get_name() -> str: + return 'obscreen-studio' + + @staticmethod + def get_version() -> str: + with open("version.txt", 'r') as file: + return file.read() diff --git a/src/controller/SettingsController.py b/src/controller/SettingsController.py index 386653c..0fc01ed 100644 --- a/src/controller/SettingsController.py +++ b/src/controller/SettingsController.py @@ -38,11 +38,6 @@ class SettingsController(ObController): if variable.name == 'auth_enabled': self.reload_web_server() - if variable.as_bool(): - return redirect(url_for( - 'logout', - restart=1 - )) if variable.name == 'lang': self._model_store.lang().set_lang(variable.value) diff --git a/src/controller/SysinfoController.py b/src/controller/SysinfoController.py index dd10fd1..8c87753 100644 --- a/src/controller/SysinfoController.py +++ b/src/controller/SysinfoController.py @@ -12,7 +12,7 @@ from src.service.ModelStore import ModelStore from src.interface.ObController import ObController from src.utils import get_ip_address, am_i_in_docker -from src.service.Sysinfos import get_all_sysinfos +from src.service.Sysinfo import get_all_sysinfo class SysinfoController(ObController): @@ -25,7 +25,7 @@ class SysinfoController(ObController): def sysinfo(self): return render_template( 'sysinfo/list.jinja.html', - sysinfos=get_all_sysinfos(), + sysinfo=get_all_sysinfo(), last_logs=self._model_store.logging().get_last_lines_of_stdout(100), ro_variables=self._model_store.variable().get_readonly_variables(), env_variables=self._model_store.config().map() diff --git a/src/interface/ObController.py b/src/interface/ObController.py index cee6660..4e5727f 100644 --- a/src/interface/ObController.py +++ b/src/interface/ObController.py @@ -9,8 +9,8 @@ from src.interface.ObPlugin import ObPlugin class ObController(abc.ABC): def __init__(self, web_server, app, auth_required, model_store: ModelStore, template_renderer: TemplateRenderer, plugin: Optional[ObPlugin] = None): - self._app = app self._web_server = web_server + self._app = app self._auth = auth_required self._model_store = model_store self._template_renderer = template_renderer diff --git a/src/model/enum/HookType.py b/src/model/enum/HookType.py index 5c0762a..125091b 100644 --- a/src/model/enum/HookType.py +++ b/src/model/enum/HookType.py @@ -4,12 +4,14 @@ from enum import Enum class HookType(Enum): H_FLEETMODE_SLIDESHOW_TOOLBAR_ACTIONS = 'h_fleetmode_slideshow_toolbar_actions' - H_SLIDESHOW_TOOLBAR_ACTIONS_START = 'h_slideshow_toolbar_actions_start' H_SLIDESHOW_TOOLBAR_ACTIONS_END = 'h_slideshow_toolbar_actions_end' H_SLIDESHOW_CSS = 'h_slideshow_css' H_SLIDESHOW_JAVASCRIPT = 'h_slideshow_javascript' + H_SYSINFO_TOOLBAR_ACTIONS_START = 'h_sysinfo_toolbar_actions_start' + H_SYSINFO_TOOLBAR_ACTIONS_END = 'h_sysinfo_toolbar_actions_end' + H_FLEET_TOOLBAR_ACTIONS_START = 'h_fleet_toolbar_actions_start' H_FLEET_TOOLBAR_ACTIONS_END = 'h_fleet_toolbar_actions_end' H_FLEET_CSS = 'h_fleet_css' diff --git a/src/service/PluginStore.py b/src/service/PluginStore.py index 4ff4c53..9c43716 100644 --- a/src/service/PluginStore.py +++ b/src/service/PluginStore.py @@ -74,7 +74,9 @@ class PluginStore: for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and issubclass(obj, ObController) and obj is not ObController: obj( + web_server=self._web_server, app=self._web_server.get_app(), + auth_required=self._web_server.auth_required, model_store=self._model_store, template_renderer=self._template_renderer, plugin=plugin @@ -156,8 +158,7 @@ class PluginStore: def is_plugin_enabled(self, plugin: ObPlugin) -> bool: var = self._model_store.variable().get_one_by_name(plugin.get_plugin_variable_name(self.DEFAULT_PLUGIN_ENABLED_VARIABLE)) - if var.as_bool: - logging.info("[Plugin] {} enabled".format(plugin.use_title())) + logging.info("[plugin] {} {}".format("🟢" if var.as_bool() else "⚫️", plugin.use_title())) return var.as_bool() if var else False diff --git a/src/service/Sysinfos.py b/src/service/Sysinfo.py similarity index 95% rename from src/service/Sysinfos.py rename to src/service/Sysinfo.py index d45059e..14912c8 100644 --- a/src/service/Sysinfos.py +++ b/src/service/Sysinfo.py @@ -3,7 +3,7 @@ import platform import psutil import socket -from src.utils import convert_size +from src.utils import convert_size, get_working_directory def get_rpi_model(): @@ -94,14 +94,14 @@ def get_default_log_file(): return None -def get_all_sysinfos(): +def get_all_sysinfo(): rpi_model = get_rpi_model() infos = { "sysinfo_rpi_model": rpi_model if rpi_model else 'sysinfo_rpi_model_unknown', "sysinfo_storage_free_space": get_free_space(), "sysinfo_memory_usage": "{}{}".format(get_memory_usage()['percent'], "%"), "sysinfo_os_version": get_os_version(), - "sysinfo_install_directory": os.getcwd() + "sysinfo_install_directory": get_working_directory() } network_info = get_network_info() diff --git a/src/service/TemplateRenderer.py b/src/service/TemplateRenderer.py index 80d3eaa..7c6a00f 100644 --- a/src/service/TemplateRenderer.py +++ b/src/service/TemplateRenderer.py @@ -1,6 +1,6 @@ import os -from flask import Flask, send_from_directory, Markup +from flask import Flask, send_from_directory, Markup, url_for from typing import List from jinja2 import Environment, FileSystemLoader, select_autoescape from src.service.ModelStore import ModelStore @@ -57,7 +57,8 @@ class TemplateRenderer: os.path.basename(hook_registration.template) )) content.append(template.render( - **self.get_view_globals() + **self.get_view_globals(), + url_for=url_for )) elif isinstance(hook_registration, FunctionalHookRegistration): content.append(hook_registration.function()) diff --git a/src/service/WebServer.py b/src/service/WebServer.py index 9e794bc..5997978 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -43,7 +43,6 @@ class WebServer: self.setup() def setup(self) -> None: - self._auth_enabled = self._model_store.variable().map().get('auth_enabled').as_bool() self._setup_flask_app() self._setup_web_globals() self._setup_web_errors() @@ -82,33 +81,37 @@ class WebServer: self._login_manager.init_app(self._app) self._login_manager.login_view = 'login' - if self._auth_enabled and self._model_store.user().count_all_enabled() == 0: + if self._model_store.variable().map().get('auth_enabled').as_bool() and self._model_store.user().count_all_enabled() == 0: self._model_store.user().add_form(User(username="admin", password="admin", enabled=True)) @self._login_manager.user_loader def load_user(user_id): return self._model_store.user().get(user_id) - def _setup_web_controllers(self) -> None: - def auth_required(f): - if not self._auth_enabled: - return f + def auth_required(self, f): + if not self._model_store.variable().map().get('auth_enabled').as_bool(): + return f - def decorated_function(*args, **kwargs): - if not current_user.is_authenticated: - return redirect(url_for('login')) + def decorated_function(*args, **kwargs): + if not self._model_store.variable().map().get('auth_enabled').as_bool(): return f(*args, **kwargs) - return decorated_function + if not current_user.is_authenticated: + return redirect(url_for('login')) + return f(*args, **kwargs) - CoreController(self, self._app, auth_required, self._model_store, self._template_renderer) - PlayerController(self, self._app, auth_required, self._model_store, self._template_renderer) - SlideshowController(self, self._app, auth_required, self._model_store, self._template_renderer) - SettingsController(self, self._app, auth_required, self._model_store, self._template_renderer) - SysinfoController(self, self._app, auth_required, self._model_store, self._template_renderer) - FleetController(self, self._app, auth_required, self._model_store, self._template_renderer) - PlaylistController(self, self._app, auth_required, self._model_store, self._template_renderer) - AuthController(self, self._app, auth_required, self._model_store, self._template_renderer) + return decorated_function + + def _setup_web_controllers(self) -> None: + + CoreController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + PlayerController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + SlideshowController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + SettingsController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + SysinfoController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + FleetController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + PlaylistController(self, self._app, self.auth_required, self._model_store, self._template_renderer) + AuthController(self, self._app, self.auth_required, self._model_store, self._template_renderer) def _setup_web_globals(self) -> None: @self._app.context_processor diff --git a/src/utils.py b/src/utils.py index a56c191..9c6c9d9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,6 +1,7 @@ import os import re import uuid +import inspect import logging import subprocess import unicodedata @@ -234,3 +235,26 @@ def convert_size(size_bytes): p = math.pow(1024, i) s = round(size_bytes / p, 2) return f"{s} {size_name[i]}" + + +def get_working_directory(): + return os.getcwd() + + +def run_system_command(commands: list, shell: bool = False, stdout=subprocess.PIPE, stderr=subprocess.PIPE): + return subprocess.run(commands, shell=shell, stdout=stdout, stderr=stderr) + + +def sudo_run_system_command(commands: list, shell: bool = False, stdout=subprocess.PIPE, stderr=subprocess.PIPE): + if commands[0] != 'sudo': + commands.insert(0, 'sudo') + return run_system_command(commands, shell=shell, stdout=stdout, stderr=stderr) + + +def get_function_caller(depth: int = 3) -> str: + return inspect.getmodulename(inspect.stack()[depth][1]) + + +def clamp(x: float, minimum: float, maximum: float) -> float: + return max(minimum, min(x, maximum)) + diff --git a/views/base.jinja.html b/views/base.jinja.html index 7d5cbad..82de490 100755 --- a/views/base.jinja.html +++ b/views/base.jinja.html @@ -122,6 +122,7 @@ + {{ HOOK(H_ROOT_JAVASCRIPT) }} {% block add_js %}{% endblock %} diff --git a/views/sysinfo/list.jinja.html b/views/sysinfo/list.jinja.html index 0f877f1..60e5a59 100644 --- a/views/sysinfo/list.jinja.html +++ b/views/sysinfo/list.jinja.html @@ -13,7 +13,9 @@

{{ l.sysinfo_page_title }}

+ {{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_START) }} + {{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_END) }}
@@ -35,10 +37,10 @@ - {% for sysinfo_label, sysinfo in sysinfos.items() %} + {% for sysinfo_label, sysinfo_value in sysinfo.items() %} {{ l[sysinfo_label] }} - {{ t(sysinfo) }} + {{ t(sysinfo_value) }} {% endfor %}