diff --git a/.env.dist b/.env.dist index 6e8a3f9..753fad3 100755 --- a/.env.dist +++ b/.env.dist @@ -1,4 +1,3 @@ DEBUG=false PORT=5000 SECRET_KEY=ANY_SECRET_KEY_HERE -PLAYER_AUTOSTART_FILE=./var/run/play # Replace by "/dev/null" if not needed diff --git a/README.md b/README.md index fc0925f..7b0f5d6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Obscreen is a user-friendly self-hosted digital signage tool. Manage a fleet of Try it! -Demo Server (Location: Roubaix - France): https://demo.obscreen.io +Demo Server (Location: Roubaix - France): [https://demo.obscreen.io](https://demo.obscreen.io?username=admin&password=admin) It is a temporary live demo, all data will be deleted after 10 minutes. Sponsored by myself. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4d15f21..0411353 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,10 +9,8 @@ services: environment: - DEBUG=${DEBUG-false} - PORT=${PORT-5000} - - PLAYER_AUTOSTART_FILE=/app/var/run/play - SECRET_KEY=${SECRET_KEY-ANY_SECRET_KEY_HERE} volumes: - .:/app - - ${PLAYER_AUTOSTART_FILE-/dev/null}:/app/var/run/play ports: - ${PORT}:${PORT} \ No newline at end of file diff --git a/docker-compose.headless.yml b/docker-compose.headless.yml index 052dea2..57d8828 100644 --- a/docker-compose.headless.yml +++ b/docker-compose.headless.yml @@ -6,10 +6,8 @@ services: environment: - DEBUG=false - PORT=5000 - - PLAYER_AUTOSTART_FILE=/app/var/run/play - SECRET_KEY=ANY_SECRET_KEY_HERE volumes: - - /dev/null:/app/var/run/play - ./data/db:/app/data/db - ./data/uploads:/app/data/uploads ports: diff --git a/docker-compose.yml b/docker-compose.yml index a381993..57d8828 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,8 @@ services: environment: - DEBUG=false - PORT=5000 - - PLAYER_AUTOSTART_FILE=/app/var/run/play - SECRET_KEY=ANY_SECRET_KEY_HERE volumes: - - ./var/run/play:/app/var/run/play - ./data/db:/app/data/db - ./data/uploads:/app/data/uploads ports: diff --git a/docs/setup-run-headless.md b/docs/setup-run-headless.md index 64fd81d..ccf5961 100644 --- a/docs/setup-run-headless.md +++ b/docs/setup-run-headless.md @@ -23,7 +23,6 @@ cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen docker run --restart=always --name obscreen --pull=always \ -e DEBUG=false \ -e PORT=5000 \ - -e PLAYER_AUTOSTART_FILE=/app/var/run/play \ -e SECRET_KEY=ANY_SECRET_KEY_HERE \ -p 5000:5000 \ -v ./data/db:/app/data/db \ diff --git a/docs/setup-run-on-rpi.md b/docs/setup-run-on-rpi.md index 19e4b81..3a9e7f3 100644 --- a/docs/setup-run-on-rpi.md +++ b/docs/setup-run-on-rpi.md @@ -25,14 +25,10 @@ curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout # t # Prepare application data file tree cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen -# Prepare player autostart file -mkdir -p var/run && touch var/run/play && chmod +x var/run/play - # Run the Docker container docker run --rm --name obscreen --pull=always \ -e DEBUG=false \ -e PORT=5000 \ - -e PLAYER_AUTOSTART_FILE=/app/var/run/play \ -e SECRET_KEY=ANY_SECRET_KEY_HERE \ -p 5000:5000 \ -v ./data/db:/app/data/db \ @@ -52,9 +48,6 @@ docker run --rm --name obscreen --pull=always \ # Prepare application data file tree cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads obscreen/system && cd obscreen -# Prepare player autostart file -mkdir -p var/run && touch var/run/play && chmod +x var/run/play - # Download docker-compose.yml curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml diff --git a/lang/en.json b/lang/en.json index ded0e21..b15e1df 100644 --- a/lang/en.json +++ b/lang/en.json @@ -226,6 +226,7 @@ "basic_month_10": "October", "basic_month_11": "November", "basic_month_12": "December", + "common_bad_file_type": "Bad file type uploaded", "common_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect", "common_pick_element": "Pick an element", "common_untitled": "", @@ -275,9 +276,8 @@ "enum_variable_section_general": "1. General", "enum_variable_section_player_options": "2. Player options", "enum_variable_section_player_animation": "3. Player animation", - "enum_variable_section_playlist": "4. Playlists", - "enum_variable_section_fleet": "5. Fleet management", - "enum_variable_section_security": "6. Security", + "enum_variable_section_fleet": "4. Fleet management", + "enum_variable_section_security": "5. Security", "enum_application_language_english": "English", "enum_application_language_french": "French", "enum_application_language_italian": "Italian", diff --git a/lang/es.json b/lang/es.json index 20e25ac..a1ef804 100644 --- a/lang/es.json +++ b/lang/es.json @@ -227,6 +227,7 @@ "basic_month_10": "Octubre", "basic_month_11": "Noviembre", "basic_month_12": "Diciembre", + "common_bad_file_type": "Tipo de archivo incorrecto cargado", "common_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto", "common_pick_element": "Elige un elemento", "common_untitled": "", @@ -276,9 +277,8 @@ "enum_variable_section_general": "1. General", "enum_variable_section_player_options": "2. Opciones del reproductor", "enum_variable_section_player_animation": "3. Animación del reproductor", - "enum_variable_section_playlist": "4. Playlist", - "enum_variable_section_fleet": "5. Gestión de flota", - "enum_variable_section_security": "6. Seguridad", + "enum_variable_section_fleet": "4. Gestión de flota", + "enum_variable_section_security": "5. Seguridad", "enum_application_language_english": "Inglés", "enum_application_language_french": "Francés", "enum_application_language_italian": "Italiano", diff --git a/lang/fr.json b/lang/fr.json index fee0f09..03e102b 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -228,6 +228,7 @@ "basic_month_10": "Octobre", "basic_month_11": "Novembre", "basic_month_12": "Décembre", + "common_bad_file_type": "Type de fichier uploadé incorrect", "common_restart_needed": "Veuillez redémarrer obscreen studio (ou redémarrer l'appareil) pour que les changements soient pris en compte", "common_pick_element": "Choisissez un élément", "common_untitled": "", @@ -277,9 +278,8 @@ "enum_variable_section_general": "1. Général", "enum_variable_section_player_options": "2. Options du lecteur", "enum_variable_section_player_animation": "3. Animation du lecteur", - "enum_variable_section_playlist": "4. Playlists", - "enum_variable_section_fleet": "5. Gestion de flotte", - "enum_variable_section_security": "6. Sécurité", + "enum_variable_section_fleet": "4. Gestion de flotte", + "enum_variable_section_security": "5. Sécurité", "enum_application_language_english": "Anglais", "enum_application_language_french": "Français", "enum_application_language_italian": "Italien", diff --git a/lang/it.json b/lang/it.json index 2fbc0de..1cebbea 100644 --- a/lang/it.json +++ b/lang/it.json @@ -227,6 +227,7 @@ "basic_month_10": "Ottobre", "basic_month_11": "Novembre", "basic_month_12": "Dicembre", + "common_bad_file_type": "Tipo di file caricato non valido", "common_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto", "common_pick_element": "Scegli un elemento", "common_untitled": "", @@ -276,9 +277,8 @@ "enum_variable_section_general": "1. Generale", "enum_variable_section_player_options": "2. Opzioni monitor", "enum_variable_section_player_animation": "3. Animazioni monitor", - "enum_variable_section_playlist": "4. Playlist", - "enum_variable_section_fleet": "5. Gestione panoramica", - "enum_variable_section_security": "6. Sicurezza", + "enum_variable_section_fleet": "4. Gestione panoramica", + "enum_variable_section_security": "5. Sicurezza", "enum_application_language_english": "Inglese", "enum_application_language_french": "Francese", "enum_application_language_italian": "Italiano", diff --git a/src/controller/AuthController.py b/src/controller/AuthController.py index f438bba..984c990 100644 --- a/src/controller/AuthController.py +++ b/src/controller/AuthController.py @@ -48,7 +48,7 @@ class AuthController(ObController): return render_template( 'auth/login.jinja.html', login_error=login_error, - last_username=request.form['username'] if 'username' in request.form else '' + last_username=request.form['username'] if 'username' in request.form else None ) def logout(self): diff --git a/src/controller/ContentController.py b/src/controller/ContentController.py index 19ad179..9fc1ac4 100644 --- a/src/controller/ContentController.py +++ b/src/controller/ContentController.py @@ -63,8 +63,11 @@ class ContentController(ObController): def slideshow_content_add(self): working_folder_path, working_folder = self.get_working_folder() + route_args = { + "path": working_folder_path, + } - self._model_store.content().add_form_raw( + content = self._model_store.content().add_form_raw( name=request.form['name'], type=str_to_enum(request.form['type'], ContentType), request_files=request.files, @@ -73,7 +76,10 @@ class ContentController(ObController): folder_id=working_folder.id if working_folder else None ) - return redirect(url_for('slideshow_content_list', path=working_folder_path)) + if not content: + route_args["error"] = 'common_bad_file_type' + + return redirect(url_for('slideshow_content_list', **route_args)) def slideshow_content_upload_bulk(self): working_folder_path, working_folder = self.get_working_folder() diff --git a/src/controller/PlayerController.py b/src/controller/PlayerController.py index 3d72c50..1fd3e26 100644 --- a/src/controller/PlayerController.py +++ b/src/controller/PlayerController.py @@ -64,6 +64,7 @@ class PlayerController(ObController): return render_template( 'player/default.jinja.html', interfaces=[iface['ip_address'] for iface in get_network_interfaces()], + external_url=self._model_store.variable().get_one_by_name('external_url').as_string().strip(), time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds'), noplaylist=request.args.get('noplaylist', '0') == '1' ) diff --git a/src/controller/SysinfoController.py b/src/controller/SysinfoController.py index b0fad80..5f690fc 100644 --- a/src/controller/SysinfoController.py +++ b/src/controller/SysinfoController.py @@ -68,5 +68,6 @@ class SysinfoController(ObController): def sysinfo_get_ipaddr(self): return jsonify({ + 'external_url': self._model_store.variable().get_one_by_name('external_url').as_string().strip(), 'interfaces': [iface['ip_address'] for iface in get_network_interfaces()] }) diff --git a/src/manager/ConfigManager.py b/src/manager/ConfigManager.py index c4735b2..c036afc 100644 --- a/src/manager/ConfigManager.py +++ b/src/manager/ConfigManager.py @@ -1,33 +1,27 @@ -import re import os import sys import logging import argparse -from src.manager.VariableManager import VariableManager -from src.util.utils import am_i_in_docker from dotenv import load_dotenv load_dotenv() class ConfigManager: DEFAULT_PORT = 5000 - DEFAULT_PLAYER_AUTOSTART_PATH = './var/run/play' VERSION_FILE = 'version.txt' - def __init__(self, variable_manager: VariableManager): - self._variable_manager = variable_manager + def __init__(self): self._CONFIG = { 'version': None, + 'demo': False, 'port': self.DEFAULT_PORT, 'bind': '0.0.0.0', 'debug': False, - 'player_autostart_file': self.DEFAULT_PLAYER_AUTOSTART_PATH, 'log_file': None, 'log_level': 'INFO', 'log_stdout': True, 'secret_key': 'ANY_SECRET_KEY_HERE', - 'player_url': 'http://localhost:{}'.format(self.DEFAULT_PORT) } self.load_version() @@ -36,8 +30,6 @@ class ConfigManager: self._CONFIG['port'] = self._CONFIG['port'] if self._CONFIG['port'] else self.DEFAULT_PORT - self.autoconfigure() - if self.map().get('debug'): logging.debug(self._CONFIG) @@ -50,10 +42,10 @@ class ConfigManager: parser.add_argument('--port', '-p', default=self._CONFIG['port'], help='Application port') parser.add_argument('--bind', '-b', default=self._CONFIG['bind'], help='Application bind address') parser.add_argument('--secret-key', '-s', default=self._CONFIG['secret_key'], help='Application secret key (any random string)') - parser.add_argument('--player-autostart-file', '-x', default=self._CONFIG['player_autostart_file'], help='Path to player 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') + parser.add_argument('--demo', '-o', default=self._CONFIG['demo'], help='Demo mode to showcase obscreen in a sandbox') parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number') return parser.parse_args() @@ -67,8 +59,8 @@ class ConfigManager: if args.debug: self._CONFIG['debug'] = args.debug - if args.player_autostart_file: - self._CONFIG['player_autostart_file'] = args.player_autostart_file + if args.demo: + self._CONFIG['demo'] = args.demo if args.log_file: self._CONFIG['log_file'] = args.log_file if args.secret_key: @@ -91,60 +83,3 @@ class ConfigManager: value = True self._CONFIG[key.lower()] = value logging.info(f"Env var {key} has been found") - - def autoconfigure(self) -> None: - self.autoconfigure_player_url() - - if self.map().get('player_autostart_file'): - self.autoconfigure_player_autostart_file() - - - def autoconfigure_player_url(self) -> str: - self._CONFIG['player_url'] = 'http://localhost:{}'.format(self.map().get('port')) - - return self._CONFIG['player_url'] - - def autoconfigure_player_autostart_file(self) -> None: - path = self.map().get('player_autostart_file') - in_docker = am_i_in_docker() - player_autostart_path = self.DEFAULT_PLAYER_AUTOSTART_PATH if in_docker else path - - if os.path.isdir(path) or not os.path.exists(path): - if not in_docker: - open(player_autostart_path, 'a').close() - else: - logging.error( - "Player autostart file {} doesn't exist on your server'\n".format( - player_autostart_path - ) - ) - sys.exit(1) - else: - logging.info("Overriding player autostart file {}".format(player_autostart_path)) - - player_url = self.map().get('player_url') - os.makedirs(os.path.dirname(player_autostart_path), exist_ok=True) - xenv_presets = """#!/bin/bash - -# Disable screensaver and DPMS -xset s off -xset -dpms -xset s noblank - -# Start unclutter to hide the mouse cursor -unclutter -display :0 -noevents -grab & - -# Modify Chromium preferences to avoid restore messages -mkdir -p __HOME__/.config/chromium/Default 2>/dev/null -touch __HOME__/.config/chromium/Default/Preferences -sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' __HOME__/.config/chromium/Default/Preferences - -RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}') -WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1) -HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2) - -# Start Chromium in kiosk mode -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 --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=${WIDTH},${HEIGHT} --display=:0 __PLAYER_URL__ - """.replace('__PLAYER_URL__', player_url).replace('__HOME__', os.environ['HOME']) - with open(player_autostart_path, 'w') as file: - file.write(xenv_presets) diff --git a/src/manager/ContentManager.py b/src/manager/ContentManager.py index 9cee23e..31e624f 100644 --- a/src/manager/ContentManager.py +++ b/src/manager/ContentManager.py @@ -177,6 +177,11 @@ class ContentManager(ModelManager): if not object or object.filename == '': return None + guessed_type = ContentType.guess_content_type_file(object) + + if not guessed_type or guessed_type != type: + return None + if object: object.seek(0) object_name = randomize_filename(object.filename) diff --git a/src/manager/VariableManager.py b/src/manager/VariableManager.py index ee68894..fdf7311 100644 --- a/src/manager/VariableManager.py +++ b/src/manager/VariableManager.py @@ -5,6 +5,7 @@ from typing import Dict, Optional, List, Tuple, Union from src.manager.DatabaseManager import DatabaseManager from src.manager.LangManager import LangManager +from src.manager.ConfigManager import ConfigManager from src.manager.UserManager import UserManager from src.model.entity.Variable import Variable from src.model.entity.Selectable import Selectable @@ -38,9 +39,10 @@ class VariableManager: "value TEXT" ] - def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager): + def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager, config_manager: ConfigManager): self._lang_manager = lang_manager self._user_manager = user_manager + self._config_manager = config_manager self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL) self._var_map = {} self.reload() @@ -94,6 +96,9 @@ class VariableManager: if variable.refresh_player != default_var['refresh_player']: self._db.update_by_id(self.TABLE_NAME, variable.id, {"refresh_player": default_var['refresh_player']}) + if variable.editable != default_var['editable']: + self._db.update_by_id(self.TABLE_NAME, variable.id, {"editable": default_var['editable']}) + if not same_selectables_keys or not same_selectables_label: self._db.update_by_id(self.TABLE_NAME, variable.id, {"selectables": default_var['selectables']}) @@ -103,13 +108,15 @@ class VariableManager: return variable def reload(self) -> None: + demo = self._config_manager.map().get('demo') + default_vars = [ # Editable (Customizable settings) ### General {"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": self.t(ApplicationLanguage), "refresh_player": False}, - {"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False}, - {"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32, "unit": VariableUnit.MEGABYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False}, + {"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": False if demo else True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False}, + {"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32, "unit": VariableUnit.MEGABYTE, "type": VariableType.INT, "editable": False if demo else True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False}, {"name": "dark_mode", "section": self.t(VariableSection.GENERAL), "value": True, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_dark_mode'), "refresh_player": False}, ### Player Options @@ -209,6 +216,16 @@ class VariableManager: return self.get_by(query="editable = 0", sort="name") def update_form(self, id: int, value: Union[int, bool, str]) -> None: + variable = self.get(id) + + if not variable: + if var.name in self._var_map: + del self._var_map[var.name] + return None + + if not variable.editable: + return None + self._db.update_by_id(self.TABLE_NAME, id, {"value": value}) var = self.get_one_by("id = {}".format(id)) self._var_map[var.name] = var diff --git a/src/service/ModelStore.py b/src/service/ModelStore.py index 8dea041..86fbe48 100644 --- a/src/service/ModelStore.py +++ b/src/service/ModelStore.py @@ -19,19 +19,19 @@ class ModelStore: def __init__(self, get_plugins: Dict): self._get_plugins = get_plugins + # Core + self._config_manager = ConfigManager() + self._logging_manager = LoggingManager(config_manager=self._config_manager) + # Pure self._lang_manager = LangManager() self._database_manager = DatabaseManager() # Dynamics self._user_manager = UserManager(lang_manager=self._lang_manager, database_manager=self._database_manager, on_user_delete=self.on_user_delete) - self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager) + self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, config_manager=self._config_manager) self._lang_manager.set_lang(self.variable().map().get('lang').as_string()) - # Core - self._config_manager = ConfigManager(variable_manager=self._variable_manager) - self._logging_manager = LoggingManager(config_manager=self._config_manager) - # Model self._folder_manager = FolderManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager) self._node_player_manager = NodePlayerManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager) diff --git a/views/auth/login.jinja.html b/views/auth/login.jinja.html index e885646..5a4efc5 100644 --- a/views/auth/login.jinja.html +++ b/views/auth/login.jinja.html @@ -29,13 +29,13 @@
- +
- +
diff --git a/views/configuration/sysinfo/list.jinja.html b/views/configuration/sysinfo/list.jinja.html index e1de33c..0e537b6 100644 --- a/views/configuration/sysinfo/list.jinja.html +++ b/views/configuration/sysinfo/list.jinja.html @@ -52,9 +52,9 @@ {% if ro_variable.value %} {% if ro_variable.type.value == 'bool' %} {% if ro_variable.display() %} - ✅ + {% else %} - ❌ + {% endif %} {% else %} {{ ro_variable.display() }} @@ -78,9 +78,9 @@ {{ env_key.replace('_',' ')|capitalize }} {% if env_value == true %} - ✅ + {% elif env_value == false %} - ❌ + {% elif env_value == none %} {{ l.common_empty }} {% else %} diff --git a/views/player/default.jinja.html b/views/player/default.jinja.html index 5e54297..4ff87b9 100755 --- a/views/player/default.jinja.html +++ b/views/player/default.jinja.html @@ -3,6 +3,7 @@ diff --git a/views/slideshow/contents/list.jinja.html b/views/slideshow/contents/list.jinja.html index a9cb838..b1dc924 100644 --- a/views/slideshow/contents/list.jinja.html +++ b/views/slideshow/contents/list.jinja.html @@ -95,6 +95,11 @@ {{ l.slideshow_content_referenced_in_slide_error }}
+ {% elif request.args.get('error') %} +
+ + {{ t(request.args.get('error')) }} +
{% else %} {% endif %}