From 627598f46b98ee0330be51dce42af4faac648822 Mon Sep 17 00:00:00 2001 From: jr-k Date: Fri, 1 Mar 2024 01:29:26 +0100 Subject: [PATCH] split main logic in files --- .dockerignore | 12 +++ .gitignore | 2 +- Dockerfile | 13 ++++ config.json.dist | 5 ++ config.py.dist | 5 -- obscreen.py | 115 +--------------------------- src/Application.py | 28 +++++++ src/controller/SysinfoController.py | 8 +- src/manager/ConfigManager.py | 115 ++++++++++++++++++++++++++++ src/manager/LangManager.py | 21 +++++ src/manager/LoggingManager.py | 34 ++++++++ src/manager/VariableManager.py | 12 ++- src/service/ModelManager.py | 39 ++++++++++ src/service/WebServer.py | 89 +++++++++++++++++++++ src/utils.py | 2 +- 15 files changed, 375 insertions(+), 125 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 config.json.dist delete mode 100644 config.py.dist create mode 100644 src/Application.py create mode 100644 src/manager/ConfigManager.py create mode 100644 src/manager/LangManager.py create mode 100644 src/manager/LoggingManager.py create mode 100644 src/service/ModelManager.py create mode 100644 src/service/WebServer.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7e2396e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.idea +*.iws +*.iml +*.ipr +out/ +data/uploads/* +!data/uploads/sample.jpg +data/db/* +!data/db/slideshow.json.dist +config.json +*.lock +__pycache__/ diff --git a/.gitignore b/.gitignore index c47e628..7e2396e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ data/uploads/* !data/uploads/sample.jpg data/db/* !data/db/slideshow.json.dist -config.py +config.json *.lock __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1e8cf5e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.9.17-alpine3.17 + +RUN apk add --no-cache git chromium + +RUN apk add --no-cache --virtual .build-deps git gcc musl-dev \ + && pip install flask pysondb-v2==2.1.0 \ + && apk del .build-deps gcc musl-dev + +WORKDIR /app + +COPY . . + +ENTRYPOINT ["python", "/app/obscreen.py"] \ No newline at end of file diff --git a/config.json.dist b/config.json.dist new file mode 100644 index 0000000..3e41b6f --- /dev/null +++ b/config.json.dist @@ -0,0 +1,5 @@ +{ + "debug": false, + "reverse_proxy_mode": false, + "lx_file": "/home/pi/.config/lxsession/LXDE-pi/autostart" +} diff --git a/config.py.dist b/config.py.dist deleted file mode 100644 index 375abf9..0000000 --- a/config.py.dist +++ /dev/null @@ -1,5 +0,0 @@ -config = { - "debug": False, # Enable autoreload for html/jinja files - "reverse_proxy_mode": False, # True if you want to use nginx on port 80 - "lx_file": '/home/pi/.config/lxsession/LXDE-pi/autostart' # Path to lx autostart file -} diff --git a/obscreen.py b/obscreen.py index f890f1d..e2e6f44 100755 --- a/obscreen.py +++ b/obscreen.py @@ -1,117 +1,8 @@ #!/usr/bin/python3 -import json import os -import re -import shutil -import subprocess -import sys -import time - - -from flask import Flask, send_from_directory -from src.manager.SlideManager import SlideManager -from src.manager.ScreenManager import ScreenManager -from src.manager.VariableManager import VariableManager -from src.controller.PlayerController import PlayerController -from src.controller.SlideshowController import SlideshowController -from src.controller.FleetController import FleetController -from src.controller.SysinfoController import SysinfoController -from src.controller.SettingsController import SettingsController -from config import config - -# -variable_manager = VariableManager() -vars = variable_manager.get_variable_map() - -screen_manager = ScreenManager() -slide_manager = SlideManager() - -PLAYER_URL = 'http://localhost:{}'.format(vars['port'].as_int()) -with open('./lang/{}.json'.format(vars['lang'].as_string()), 'r') as file: - LANGDICT = json.load(file) - -variable_manager.init(LANGDICT) -# - - -# -if config['reverse_proxy_mode']: - reverse_proxy_config_file = 'system/nginx-obscreen' - with open(reverse_proxy_config_file, 'r') as file: - content = file.read() - with open(reverse_proxy_config_file, 'w') as file: - file.write(re.sub(r'proxy_pass .*?;', 'proxy_pass {};'.format(PLAYER_URL), content)) - PLAYER_URL = 'http://localhost' -# - - -# -app = Flask(__name__, template_folder='views', static_folder='data') -app.config['UPLOAD_FOLDER'] = 'data/uploads' -app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB - -if config['debug']: - app.config['TEMPLATES_AUTO_RELOAD'] = True - -# - - -# -if config['lx_file']: - destination_path = '/home/pi/.config/lxsession/LXDE-pi/autostart' - os.makedirs(os.path.dirname(config['lx_file']), exist_ok=True) - xenv_presets = f""" - @lxpanel --profile LXDE-pi - @pcmanfm --desktop --profile LXDE-pi - @xscreensaver -no-splash - #@point-rpi - @xset s off - @xset -dpms - @xset s noblank - @unclutter -display :0 -noevents -grab - @sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences - #@sleep 10 - @chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 {PLAYER_URL} - """ - with open(config['lx_file'], 'w') as file: - file.write(xenv_presets) -# - - -# -@app.context_processor -def inject_global_vars(): - return dict( - FLEET_ENABLED=vars['fleet_enabled'].as_bool(), - LANG=vars['lang'].as_string(), - STATIC_PREFIX='/data/www/' - ) - - -@app.template_filter('ctime') -def time_ctime(s): - return time.ctime(s) - - - -PlayerController(app, LANGDICT, slide_manager) -SlideshowController(app, LANGDICT, slide_manager, variable_manager) -SettingsController(app, LANGDICT, variable_manager) -SysinfoController(app, LANGDICT, config, variable_manager) - -if vars['fleet_enabled'].as_bool(): - FleetController(app, LANGDICT, screen_manager) - -@app.errorhandler(404) -def not_found(e): - return send_from_directory('views', 'core/error404.html'), 404 -# +from src.Application import Application if __name__ == '__main__': - app.run( - host=vars['bind'].as_string(), - port=vars['port'].as_int(), - debug=config['debug'] - ) - + app = Application(project_dir=os.path.dirname(__file__)) + app.start() diff --git a/src/Application.py b/src/Application.py new file mode 100644 index 0000000..d938878 --- /dev/null +++ b/src/Application.py @@ -0,0 +1,28 @@ +import sys +import logging +import signal +import threading + +from src.service.ModelManager import ModelManager +from src.service.WebServer import WebServer + + +class Application: + + def __init__(self, project_dir: str): + self._project_dir = project_dir + self._stop_event = threading.Event() + self._model_manager = ModelManager() + self._web_server = WebServer(project_dir=project_dir, model_manager=self._model_manager) + + signal.signal(signal.SIGINT, self.signal_handler) + + def start(self) -> None: + self._web_server.run() + + def signal_handler(self, signal, frame) -> None: + logging.info("Shutting down...") + self._stop_event.set() + sys.exit(0) + + diff --git a/src/controller/SysinfoController.py b/src/controller/SysinfoController.py index 2842b42..72a2b19 100644 --- a/src/controller/SysinfoController.py +++ b/src/controller/SysinfoController.py @@ -4,15 +4,17 @@ import platform import subprocess from flask import Flask, render_template, jsonify +from src.manager.VariableManager import VariableManager +from src.manager.ConfigManager import ConfigManager from src.utils import get_ip_address class SysinfoController: - def __init__(self, app, lang_dict, config, variable_manager): + def __init__(self, app, lang_dict, config_manager: ConfigManager, variable_manager: VariableManager): self._app = app self._lang_dict = lang_dict - self._config = config + self._config_manager = config_manager self._variable_manager = variable_manager self.register() @@ -32,7 +34,7 @@ class SysinfoController: def sysinfo_restart(self): if platform.system().lower() == 'darwin': - if self._config['debug']: + if self._config_manager.map().get('debug'): python = sys.executable os.execl(python, python, *sys.argv) else: diff --git a/src/manager/ConfigManager.py b/src/manager/ConfigManager.py new file mode 100644 index 0000000..bb41216 --- /dev/null +++ b/src/manager/ConfigManager.py @@ -0,0 +1,115 @@ +import re +import os +import json +import logging +import argparse + +from src.manager.VariableManager import VariableManager + + +class ConfigManager: + + CONFIG_FILE = 'config.json' + + def __init__(self, variable_manager: VariableManager): + self._variable_manager = variable_manager + self._CONFIG = { + 'debug': False, + 'reverse_proxy_mode': False, + 'lx_file': '/home/pi/.config/lxsession/LXDE-pi/autostart', + 'log_file': None, + 'log_level': 'INFO', + 'log_stdout': True, + 'player_url': 'http://localhost:{}'.format(self._variable_manager.map().get('port').as_int()) + } + + self.load_from_json(file_path=self.CONFIG_FILE) + self.load_from_env() + self.load_from_args() + self.autoconfigure() + + if self.map().get('debug'): + logging.debug(self._CONFIG) + + def map(self) -> dict: + return self._CONFIG + + def parse_arguments(self): + parser = argparse.ArgumentParser(description="Obscreen") + parser.add_argument('--debug', '-d', default=self._CONFIG['debug'], help='Debug mode') + parser.add_argument('--reverse_proxy_mode', '-r', default=self._CONFIG['reverse_proxy_mode'], action='store_true', help='true if you want to use nginx on port 80') + parser.add_argument('--lx-file', '-x', default=self._CONFIG['lx_file'], help='Path to lx autostart file') + parser.add_argument('--log-file', '-lf', default=self._CONFIG['log_file'], help='Log File path') + parser.add_argument('--log-level', '-ll', default=self._CONFIG['log_level'], help='Log Level') + parser.add_argument('--log-stdout', '-ls', default=self._CONFIG['log_stdout'], action='store_true', help='Log to standard output') + + return parser.parse_args() + + def load_from_args(self) -> None: + args = self.parse_arguments() + + if args.debug: + self._CONFIG['debug'] = args.debug + if args.reverse_proxy_mode: + self._CONFIG['reverse_proxy_mode'] = args.reverse_proxy_mode + if args.lx_file: + self._CONFIG['lx_file'] = args.lx_file + if args.log_file: + self._CONFIG['log_file'] = args.log_file + if args.log_level: + self._CONFIG['log_level'] = args.log_level + if args.log_stdout: + self._CONFIG['log_stdout'] = args.log_stdout + + def load_from_json(self, file_path: str) -> None: + try: + with open(file_path, 'r') as file: + json_config = json.load(file) + for key in json_config: + self._CONFIG[key] = json_config[key] + logging.info(f"Json var {key} has been found") + except FileNotFoundError: + logging.error(f"Json configuration file {file_path} doesn't exist.") + + def load_from_env(self) -> None: + for key in self._CONFIG: + if key in os.environ: + self._CONFIG[key] = os.environ[key] + logging.info(f"Env var {key} has been found") + + def autoconfigure(self) -> None: + if self.map().get('reverse_proxy_mode'): + self.autoconfigure_nginx() + + if self.map().get('lx_file'): + self.autoconfigure_lxconf() + + def autoconfigure_nginx(self) -> None: + reverse_proxy_config_file = 'system/nginx-obscreen' + with open(reverse_proxy_config_file, 'r') as file: + content = file.read() + with open(reverse_proxy_config_file, 'w') as file: + file.write(re.sub(r'proxy_pass .*?;', 'proxy_pass {};'.format(self.map().get('player_url')), content)) + + self._CONFIG['player_url'] = 'http://localhost' + + def autoconfigure_lxconf(self) -> None: + destination_path = self.map().get('lx_file') + player_url = self.map().get('player_url') + os.makedirs(os.path.dirname(destination_path), exist_ok=True) + xenv_presets = f""" + @lxpanel --profile LXDE-pi + @pcmanfm --desktop --profile LXDE-pi + @xscreensaver -no-splash + #@point-rpi + @xset s off + @xset -dpms + @xset s noblank + @unclutter -display :0 -noevents -grab + @sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences + #@sleep 10 + @chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 {player_url} + """ + with open(destination_path, 'w') as file: + file.write(xenv_presets) + diff --git a/src/manager/LangManager.py b/src/manager/LangManager.py new file mode 100644 index 0000000..8f7d0e3 --- /dev/null +++ b/src/manager/LangManager.py @@ -0,0 +1,21 @@ +import json +import logging + + +class LangManager: + + LANG_FILE = "lang/{}.json" + + def __init__(self, lang: str): + self._map = {} + + file_name = self.LANG_FILE.format(lang) + + try: + with open(file_name, 'r') as file: + self._map = json.load(file) + except FileNotFoundError: + logging.error("Lang file {} not found".format(file_name)) + + def map(self) -> dict: + return self._map diff --git a/src/manager/LoggingManager.py b/src/manager/LoggingManager.py new file mode 100644 index 0000000..e1bb362 --- /dev/null +++ b/src/manager/LoggingManager.py @@ -0,0 +1,34 @@ +import sys +import logging + +from src.manager.ConfigManager import ConfigManager + + +class LoggingManager: + + def __init__(self, config_manager: ConfigManager): + c_map = config_manager.map() + log_level_str = c_map.get('log_level', 'INFO').upper() + log_level = getattr(logging, log_level_str, logging.INFO) + + self._logger = logging.getLogger() + self._logger.setLevel(log_level) + + if c_map.get('log_file'): + self._add_file_handler(file_path=c_map.get('log_file')) + + if c_map.get('log_stdout'): + self._add_stdout_handler() + + def _add_file_handler(self, file_path: str) -> None: + file_handler = logging.FileHandler(file_path) + file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(file_formatter) + self._logger.addHandler(file_handler) + + def _add_stdout_handler(self) -> None: + console_handler = logging.StreamHandler(sys.stdout) + console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_handler.setFormatter(console_formatter) + self._logger.addHandler(console_handler) + diff --git a/src/manager/VariableManager.py b/src/manager/VariableManager.py index 9c860bd..1223724 100644 --- a/src/manager/VariableManager.py +++ b/src/manager/VariableManager.py @@ -12,9 +12,10 @@ class VariableManager: def __init__(self): self._db = PysonDB(self.DB_FILE) - self.init() + self._var_map = {} + self.reload() - def init(self, lang_dict: Optional[Dict] = None) -> None: + def reload(self, lang_dict: Optional[Dict] = None) -> None: default_vars = [ {"name": "port", "value": 5000, "type": VariableType.INT.value, "editable": True, "description": lang_dict['settings_variable_help_port'] if lang_dict else ""}, {"name": "bind", "value": '0.0.0.0', "type": VariableType.STRING.value, "editable": True, "description": lang_dict['settings_variable_help_bind'] if lang_dict else ""}, @@ -37,7 +38,12 @@ class VariableManager: if variable.name == 'last_restart': self._db.update_by_id(variable.id, {"value": time.time()}) - def get_variable_map(self) -> Dict[str, Variable]: + self._var_map = self.prepare_variable_map() + + def map(self) -> dict: + return self._var_map + + def prepare_variable_map(self) -> Dict[str, Variable]: var_map = {} for var in self.get_all(): diff --git a/src/service/ModelManager.py b/src/service/ModelManager.py new file mode 100644 index 0000000..53e9791 --- /dev/null +++ b/src/service/ModelManager.py @@ -0,0 +1,39 @@ +from src.manager.SlideManager import SlideManager +from src.manager.ScreenManager import ScreenManager +from src.manager.VariableManager import VariableManager +from src.manager.LangManager import LangManager +from src.manager.ConfigManager import ConfigManager +from src.manager.LoggingManager import LoggingManager + + +class ModelManager: + + def __init__(self): + self._variable_manager = VariableManager() + self._config_manager = ConfigManager(variable_manager=self._variable_manager) + self._logging_manager = LoggingManager(config_manager=self._config_manager) + self._screen_manager = ScreenManager() + self._slide_manager = SlideManager() + self._lang_manager = LangManager( + lang=self.variable().map().get('lang').as_string() + ) + self._variable_manager.reload(lang_dict=self._lang_manager.map()) + + def logging(self) -> LoggingManager: + return self._logging_manager + + def config(self) -> ConfigManager: + return self._config_manager + + def variable(self) -> VariableManager: + return self._variable_manager + + def slide(self) -> SlideManager: + return self._slide_manager + + def screen(self) -> ScreenManager: + return self._screen_manager + + def lang(self) -> LangManager: + return self._lang_manager + diff --git a/src/service/WebServer.py b/src/service/WebServer.py new file mode 100644 index 0000000..211852f --- /dev/null +++ b/src/service/WebServer.py @@ -0,0 +1,89 @@ +import os +import time + +from flask import Flask, send_from_directory +from src.service.ModelManager import ModelManager +from src.controller.PlayerController import PlayerController +from src.controller.SlideshowController import SlideshowController +from src.controller.FleetController import FleetController +from src.controller.SysinfoController import SysinfoController +from src.controller.SettingsController import SettingsController + + +class WebServer: + + FOLDER_TEMPLATES = "views" + FOLDER_STATIC = "data" + FOLDER_STATIC_WEB_UPLOADS = "uploads" + FOLDER_STATIC_WEB_ASSETS = "www" + MAX_UPLOADS = 16 * 1024 * 1024 + + def __init__(self, project_dir: str, model_manager: ModelManager): + self._project_dir = project_dir + self._model_manager = model_manager + self._debug = self._model_manager.config().map().get('debug') + self.setup() + + def run(self) -> None: + self._app.run( + host=self._model_manager.variable().map().get('bind').as_string(), + port=self._model_manager.variable().map().get('port').as_int(), + debug=self._debug + ) + + def setup(self) -> None: + self._setup_flask_app() + self._setup_view_globals() + self._setup_view_extensions() + self._setup_view_errors() + self._setup_view_controllers() + + def _get_template_folder(self) -> str: + return "{}/{}".format(self._project_dir, self.FOLDER_TEMPLATES) + + def _get_static_folder(self) -> str: + return "{}/{}".format(self._project_dir, self.FOLDER_STATIC) + + def _setup_flask_app(self) -> None: + self._app = Flask( + __name__, + template_folder=self._get_template_folder(), + static_folder=self._get_static_folder(), + ) + + self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(self.FOLDER_STATIC, self.FOLDER_STATIC_WEB_UPLOADS) + self._app.config['MAX_CONTENT_LENGTH'] = self.MAX_UPLOADS + + if self._debug: + self._app.config['TEMPLATES_AUTO_RELOAD'] = True + + def _setup_view_controllers(self) -> None: + lang_map = self._model_manager.lang().map() + mm = self._model_manager + + PlayerController(self._app, lang_map, mm.slide()) + SlideshowController(self._app, lang_map, mm.slide(), mm.variable()) + SettingsController(self._app, lang_map, mm.variable()) + SysinfoController(self._app, lang_map, mm.config(), mm.variable()) + + if self._model_manager.variable().map().get('fleet_enabled').as_bool(): + FleetController(self._app, lang_map, mm.screen()) + + def _setup_view_globals(self) -> None: + @self._app.context_processor + def inject_global_vars(): + return dict( + FLEET_ENABLED=self._model_manager.variable().map().get('fleet_enabled').as_bool(), + LANG=self._model_manager.variable().map().get('lang').as_string(), + STATIC_PREFIX="/{}/{}/".format(self.FOLDER_STATIC, self.FOLDER_STATIC_WEB_ASSETS) + ) + + def _setup_view_extensions(self) -> None: + @self._app.template_filter('ctime') + def time_ctime(s): + return time.ctime(s) + + def _setup_view_errors(self) -> None: + @self._app.errorhandler(404) + def not_found(e): + return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404 diff --git a/src/utils.py b/src/utils.py index c7a1dfc..3f247de 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +import re import subprocess import platform @@ -11,7 +12,6 @@ def str_to_enum(str_val: str, enum_class) -> Enum: return enum_item raise ValueError(f"{str_val} is not a valid {enum_class.__name__} item") - def get_ip_address() -> Optional[str]: try: os_name = platform.system().lower()