diff --git a/.env.dist b/.env.dist index 58e50a9..f1892e1 100755 --- a/.env.dist +++ b/.env.dist @@ -5,11 +5,7 @@ SECRET_KEY=ANY_SECRET_KEY_HERE # Application Server PORT=5000 BIND=0.0.0.0 - -# HTTP External Storage Server -PORT_HTTP_EXTERNAL_STORAGE=5001 -BIND_HTTP_EXTERNAL_STORAGE=0.0.0.0 -CHROOT_HTTP_EXTERNAL_STORAGE=%application_dir%/var/run/storage +EXTERNAL_STORAGE_MOUNTPOINT=%application_dir%/var/run/storage # Misc DEMO=false diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 020e63f..103ad92 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,10 +10,8 @@ services: - DEBUG=false - SECRET_KEY=ANY_SECRET_KEY_HERE - PORT=5000 - - PORT_HTTP_EXTERNAL_STORAGE=5001 volumes: - /etc/localtime:/etc/localtime:ro - ./:/app/ ports: - 5000:5000 - - 5001:5001 diff --git a/docker-compose.yml b/docker-compose.yml index bfe4598..52b8d9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,6 @@ services: - DEBUG=false - SECRET_KEY=ANY_SECRET_KEY_HERE - PORT=5000 - - PORT_HTTP_EXTERNAL_STORAGE=5001 volumes: - /etc/localtime:/etc/localtime:ro - ./data/db:/app/data/db @@ -16,4 +15,3 @@ services: - ./var/run/storage:/app/var/run/storage ports: - 5000:5000 - - 5001:5001 diff --git a/lang/en.json b/lang/en.json index ab7f8fc..cc39692 100644 --- a/lang/en.json +++ b/lang/en.json @@ -184,12 +184,12 @@ "settings_variable_desc_auth_enabled": "Enable auth management", "settings_variable_desc_edition_auth_enabled": "Default user credentials will be %username%/%password%", "settings_variable_desc_external_url": "External url (i.e: https://studio-01.company.com or http://10.10.3.100)", - "settings_variable_desc_external_storage_url": "External url for external storage (i.e: https://studio-01.company.com or http://10.10.3.100)", "settings_variable_desc_slide_upload_limit": "Slide upload limit (in megabytes)", "settings_variable_desc_dark_mode": "Dark mode", "settings_variable_desc_intro_slide_duration": "Introduction slide duration (in seconds)", "settings_variable_desc_default_slide_time_with_seconds": "Show the seconds on the clock in the introduction slide", "settings_variable_desc_polling_interval": "Refresh interval applied for settings to the player (in seconds)", + "settings_variable_desc_player_content_cache": "Enable cache", "settings_variable_desc_slide_animation_enabled": "Enable animation effect between slides", "settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect", "settings_variable_desc_slide_animation_exit_effect": "Slide animation exit effect (generally better off without it)", diff --git a/lang/es.json b/lang/es.json index 8a65d7b..16e2728 100644 --- a/lang/es.json +++ b/lang/es.json @@ -185,12 +185,12 @@ "settings_variable_desc_auth_enabled": "Habilitar gestión de autenticación", "settings_variable_desc_edition_auth_enabled": "Las credenciales predeterminadas del usuario serán %username%/%password%", "settings_variable_desc_external_url": "URL externa (ej.: https://studio-01.company.com o http://10.10.3.100)", - "settings_variable_desc_external_storage_url": "URL externa para almacenamiento externo(ej.: https://studio-01.company.com o http://10.10.3.100)", "settings_variable_desc_slide_upload_limit": "Límite de carga de diapositivas (en megabytes)", "settings_variable_desc_dark_mode": "Modo oscuro", "settings_variable_desc_intro_slide_duration": "Duración de la diapositiva de introducción (en segundos)", "settings_variable_desc_default_slide_time_with_seconds": "Mostrar los segundos en el reloj de la diapositiva de introducción", "settings_variable_desc_polling_interval": "Intervalo de actualización aplicado para configuraciones del reproductor (en segundos)", + "settings_variable_desc_player_content_cache": "Habilitar la caché", "settings_variable_desc_slide_animation_enabled": "Habilitar efecto de animación entre diapositivas", "settings_variable_desc_slide_animation_entrance_effect": "Efecto de entrada de animación de diapositiva", "settings_variable_desc_slide_animation_exit_effect": "Efecto de salida de animación de diapositiva (generalmente mejor sin él)", diff --git a/lang/fr.json b/lang/fr.json index cb583ac..1c6aa2d 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -186,12 +186,12 @@ "settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification", "settings_variable_desc_edition_auth_enabled": "Les identifiants de l'utilisateur par défaut seront %username%/%password%", "settings_variable_desc_external_url": "URL externe (i.e: https://studio-01.company.com or http://10.10.3.100)", - "settings_variable_desc_external_storage_url": "URL externe pour le stockage externe (i.e: https://studio-01.company.com or http://10.10.3.100)", "settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en mégaoctets)", "settings_variable_desc_dark_mode": "Mdoe sombre", "settings_variable_desc_intro_slide_duration": "Durée de la slide d'introduction (en secondes)", "settings_variable_desc_default_slide_time_with_seconds": "Afficher les secondes de l'horloge de la slide d'introduction", "settings_variable_desc_polling_interval": "Intervalle de rafraîchissement des paramètres à appliquer au lecteur (en secondes)", + "settings_variable_desc_player_content_cache": "Activer le cache", "settings_variable_desc_slide_animation_enabled": "Activer les effets d'animation entre les slides", "settings_variable_desc_slide_animation_entrance_effect": "Effet d'animation d'arrivée de la slide", "settings_variable_desc_slide_animation_exit_effect": "Effet d'animation de sortie de la slide (généralement mieux sans)", diff --git a/lang/it.json b/lang/it.json index 8e52246..974504c 100644 --- a/lang/it.json +++ b/lang/it.json @@ -185,12 +185,12 @@ "settings_variable_desc_auth_enabled": "Abilita la gestione autenticazione", "settings_variable_desc_edition_auth_enabled": "Le credenziali utente predefinite sono %username%/%password%", "settings_variable_desc_external_url": "Url esterno (esempio: https://studio-01.company.com or http://10.10.3.100)", - "settings_variable_desc_external_storage_url": "Url esterno per l'archiviazione esterna (esempio: https://studio-01.company.com or http://10.10.3.100)", "settings_variable_desc_slide_upload_limit": "Limite upload slide (in megabytes)", "settings_variable_desc_dark_mode": "Modalità scura", "settings_variable_desc_intro_slide_duration": "Durata introduzione slide (in secondi)", "settings_variable_desc_default_slide_time_with_seconds": "Mostra secondi introduzione slide", "settings_variable_desc_polling_interval": "Intervallo di aggiornamento applicato per le impostazioni del monitor (in secondi)", + "settings_variable_desc_player_content_cache": "Abilita la cache", "settings_variable_desc_slide_animation_enabled": "Abilita l'effetto di animazione tra le diapositive", "settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva", "settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)", diff --git a/src/Application.py b/src/Application.py index 252a061..673e338 100644 --- a/src/Application.py +++ b/src/Application.py @@ -6,7 +6,6 @@ import threading from src.service.ModelStore import ModelStore from src.service.PluginStore import PluginStore from src.service.TemplateRenderer import TemplateRenderer -from src.service.ExternalStorageServer import ExternalStorageServer from src.service.WebServer import WebServer from src.model.enum.HookType import HookType @@ -19,7 +18,6 @@ class Application: self._model_store = ModelStore(self, self.get_plugins) self._template_renderer = TemplateRenderer(kernel=self, model_store=self._model_store, render_hook=self.render_hook) self._web_server = WebServer(kernel=self, model_store=self._model_store, template_renderer=self._template_renderer) - self._external_storage_server = ExternalStorageServer(kernel=self, model_store=self._model_store) logging.info("[{}] Starting application v{}...".format(self.get_name(), self.get_version())) self._plugin_store = PluginStore(kernel=self, model_store=self._model_store, template_renderer=self._template_renderer, web_server=self._web_server) @@ -31,7 +29,6 @@ class Application: if variable: self._model_store.variable().update_by_name(variable.name, variable.as_int() + 1) - self._external_storage_server.run() self._web_server.run() def signal_handler(self, signal, frame) -> None: @@ -62,7 +59,3 @@ class Application: self._model_store.lang().set_lang(lang) self._model_store.variable().reload() self._plugin_store.reload_lang() - - @property - def external_storage_server(self): - return self._external_storage_server diff --git a/src/controller/ContentController.py b/src/controller/ContentController.py index 1f7a521..30ba73b 100644 --- a/src/controller/ContentController.py +++ b/src/controller/ContentController.py @@ -9,7 +9,6 @@ from src.model.entity.Content import Content from src.model.enum.ContentType import ContentType from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH from src.interface.ObController import ObController -from src.service.ExternalStorageServer import ExternalStorageServer from src.util.utils import str_to_enum, get_optional_string from src.util.UtilFile import randomize_filename @@ -60,7 +59,7 @@ class ContentController(ObController): working_folder_children=self._model_store.folder().get_children(folder=working_folder, entity=FolderEntity.CONTENT, sort='created_at', ascending=False), enum_content_type=ContentType, enum_folder_entity=FolderEntity, - chroot_http_external_storage=self.get_external_storage_server().get_directory(), + external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'), ) def slideshow_content_add(self): @@ -119,7 +118,7 @@ class ContentController(ObController): working_folder_path=working_folder_path, working_folder=working_folder, enum_content_type=ContentType, - chroot_http_external_storage=self.get_external_storage_server().get_directory(), + external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint') ) def slideshow_content_save(self, content_id: int = 0): diff --git a/src/controller/PlayerController.py b/src/controller/PlayerController.py index 9b9adeb..41ad881 100644 --- a/src/controller/PlayerController.py +++ b/src/controller/PlayerController.py @@ -1,18 +1,20 @@ +import os import json import logging import hashlib -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional, List, Dict -from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort +from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify, abort, send_file, Response from pathlib import Path from src.model.entity.Slide import Slide +from src.model.entity.Content import Content from src.model.enum.ContentType import ContentType from src.exceptions.NoFallbackPlaylistException import NoFallbackPlaylistException from src.service.ModelStore import ModelStore from src.interface.ObController import ObController -from src.util.utils import get_safe_cron_descriptor, is_cron_in_datetime_moment, is_cron_in_week_moment, is_now_after_cron_date_time_moment, is_now_after_cron_week_moment +from src.util.utils import get_safe_cron_descriptor, is_cron_in_datetime_moment, is_cron_in_week_moment, is_now_after_cron_date_time_moment, is_now_after_cron_week_moment, decode_uri_component from src.util.UtilNetwork import get_safe_remote_addr, get_network_interfaces from src.model.enum.AnimationSpeed import animation_speed_duration @@ -25,6 +27,7 @@ class PlayerController(ObController): self._app.add_url_rule('/player/default', 'player_default', self.player_default, methods=['GET']) self._app.add_url_rule('/player/playlist', 'player_playlist', self.player_playlist, methods=['GET']) self._app.add_url_rule('/player/playlist/use/', 'player_playlist_use', self.player_playlist, methods=['GET']) + self._app.add_url_rule('/serve/content///', 'serve_content_file', self.serve_content_file, methods=['GET']) def player(self, playlist_slug_or_id: str = ''): preview_content_id = request.args.get('preview_content_id') @@ -136,24 +139,26 @@ class PlayerController(ObController): content = contents[int(slide['content_id'])] slide['name'] = content.name - slide['location'] = content.location slide['type'] = content.type.value + slide['location'] = self._model_store.content().resolve_content_location(content) if slide['type'] == ContentType.EXTERNAL_STORAGE.value: - mount_point_dir = Path(self.get_external_storage_server().get_directory(), slide['location']) + mount_point_dir = Path(self._model_store.config().map().get('external_storage_mountpoint'), slide['location']) if mount_point_dir.is_dir(): for file in mount_point_dir.iterdir(): if file.is_file() and not file.stem.startswith('.'): + virtual_content = Content( + name=file.stem, + location=self._model_store.content().resolve_content_location(Path(content.location, file.name)), + type=ContentType.guess_content_type_file(str(file.resolve())), + ) slide = dict(slide) slide['id'] = hashlib.md5(str(file).encode('utf-8')).hexdigest() slide['position'] = position - slide['type'] = ContentType.guess_content_type_file(str(file.resolve())).value - slide['name'] = file.stem slide['delegate_duration'] = 1 if slide['type'] == ContentType.VIDEO.value else 0 - slide['location'] = "{}/{}".format( - self._model_store.content().resolve_content_location(content), - file.name - ) + slide['name'] = virtual_content.stem + slide['type'] = virtual_content.type.value + slide['location'] = self._model_store.content().resolve_content_location(virtual_content) self._check_slide_enablement(playlist_loop, playlist_notifications, slide) position = position + 1 else: @@ -197,3 +202,39 @@ class PlayerController(ObController): return loop.append(slide) + + def serve_content_file(self, content_location, content_type, content_id): + content = self._model_store.content().get(content_id) + + if not content: + abort(404, 'Content not found') + + content_location = decode_uri_component(content_location) + + content_path = str(Path(self.get_application_dir(), content_location)) + + if content_type == ContentType.EXTERNAL_STORAGE.value: + content_path = str(Path(self._model_store.config().map().get('external_storage_mountpoint'), content_location)) + + if not os.path.exists(content_path) or '..' in content_path: + abort(404, 'Content not found') + + if not self._model_store.variable().get_one_by_name('player_content_cache').as_bool(): + response = send_file(content_path) + response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + return response + + content_path_hash = hashlib.sha256(str(content_path).encode()).hexdigest() + etag = f'"{content_path_hash}-{content_id}-{os.path.getmtime(content_path)}"' + + if_none_match = request.headers.get('If-None-Match') + if if_none_match == etag: + return Response(status=304) + + response = send_file(content_path) + response.headers['Cache-Control'] = 'public, max-age=3153600000' # 100 years + response.headers['ETag'] = etag + + return response diff --git a/src/interface/ObController.py b/src/interface/ObController.py index 2842e02..0dcd4df 100644 --- a/src/interface/ObController.py +++ b/src/interface/ObController.py @@ -40,12 +40,12 @@ class ObController(abc.ABC): def reload_lang(self, lang: str): self._kernel.reload_lang(lang) + def get_application_dir(self): + return self._kernel.get_application_dir() + def t(self, token) -> Union[Dict, str]: return self._model_store.lang().translate(token) - def get_external_storage_server(self): - return self._kernel.external_storage_server - def render_view(self, template_file: str, **parameters: dict) -> str: return self._template_renderer.render_view(template_file, self.plugin(), **parameters) diff --git a/src/manager/ConfigManager.py b/src/manager/ConfigManager.py index 25b3bc3..b41a9ff 100644 --- a/src/manager/ConfigManager.py +++ b/src/manager/ConfigManager.py @@ -11,7 +11,6 @@ class ConfigManager: APPLICATION_NAME = "Obscreen" DEFAULT_PORT = 5000 - DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001 VERSION_FILE = 'version.txt' def __init__(self, replacers: Dict): @@ -20,9 +19,7 @@ class ConfigManager: 'application_name': self.APPLICATION_NAME, 'version': None, 'demo': False, - 'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE, - 'bind_http_external_storage': '0.0.0.0', - 'chroot_http_external_storage': '%application_dir%/var/run/storage', + 'external_storage_mountpoint': '%application_dir%/var/run/storage', 'port': self.DEFAULT_PORT, 'bind': '0.0.0.0', 'debug': False, @@ -56,9 +53,7 @@ class ConfigManager: 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('--port-http-external-storage', '-bx', default=self._CONFIG['port_http_external_storage'], help='Port of http server serving external storage') - parser.add_argument('--bind-http-external-storage', '-px', default=self._CONFIG['bind_http_external_storage'], help='Bind address of http server serving external storage') - parser.add_argument('--chroot-http-external-storage', '-cx', default=self._CONFIG['chroot_http_external_storage'], help='Chroot directory of http server serving external storage') + parser.add_argument('--external-storage-mountpoint', '-e', default=self._CONFIG['external_storage_mountpoint'], help='Mountpoint directory of external storage') parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number') return parser.parse_args() @@ -74,12 +69,8 @@ class ConfigManager: self._CONFIG['debug'] = args.debug if args.demo: self._CONFIG['demo'] = args.demo - if args.port_http_external_storage: - self._CONFIG['port_http_external_storage'] = args.port_http_external_storage - if args.bind_http_external_storage: - self._CONFIG['bind_http_external_storage'] = args.bind_http_external_storage - if args.chroot_http_external_storage: - self._CONFIG['chroot_http_external_storage'] = args.chroot_http_external_storage + if args.external_storage_mountpoint: + self._CONFIG['external_storage_mountpoint'] = args.external_storage_mountpoint if args.log_file: self._CONFIG['log_file'] = args.log_file if args.secret_key: diff --git a/src/manager/ContentManager.py b/src/manager/ContentManager.py index aaf6c14..85bc010 100644 --- a/src/manager/ContentManager.py +++ b/src/manager/ContentManager.py @@ -2,6 +2,7 @@ import os from typing import Dict, Optional, List, Tuple, Union from werkzeug.datastructures import FileStorage +from flask import url_for from src.model.entity.Content import Content from src.model.entity.Playlist import Playlist @@ -16,6 +17,7 @@ from src.service.ModelManager import ModelManager from src.util.UtilFile import randomize_filename from src.util.UtilNetwork import get_preferred_ip_address from src.util.UtilVideo import mp4_duration_with_ffprobe +from src.util.utils import encode_uri_component class ContentManager(ModelManager): @@ -233,19 +235,18 @@ class ContentManager(ModelManager): var_external_url = self._variable_manager.get_one_by_name('external_url').as_string().strip().strip('/') location = content.location - if content.type == ContentType.EXTERNAL_STORAGE: - var_external_storage_url = self._variable_manager.get_one_by_name('external_url_storage').as_string().strip().strip('/') - port_ex_st = self._config_manager.map().get('port_http_external_storage') - location = "{}/{}".format( - var_external_storage_url if var_external_storage_url else 'http://{}:{}'.format(get_preferred_ip_address(), port_ex_st), - content.location.strip('/') - ) - elif content.type == ContentType.YOUTUBE: + if content.type == ContentType.YOUTUBE: location = "https://www.youtube.com/watch?v={}".format(content.location) - elif len(var_external_url) > 0 and content.has_file(): - location = "{}/{}".format(var_external_url, content.location) - elif content.has_file(): - location = "/{}".format(content.location) + elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE: + location = "{}/{}".format( + var_external_url if len(var_external_url) > 0 else "", + url_for( + 'serve_content_file', + content_location=encode_uri_component(content.location), + content_type=content.type.value, + content_id=content.id + ).strip('/') + ).strip('/') elif content.type == ContentType.URL: location = 'http://' + content.location if not content.location.startswith('http') else content.location diff --git a/src/manager/DatabaseManager.py b/src/manager/DatabaseManager.py index 92e8d6f..034df67 100644 --- a/src/manager/DatabaseManager.py +++ b/src/manager/DatabaseManager.py @@ -214,6 +214,7 @@ class DatabaseManager: "DELETE FROM settings WHERE name = 'playlist_default_time_sync'", "DELETE FROM settings WHERE name = 'slide_animation_exit_effect'", "DELETE FROM settings WHERE name = 'playlist_enabled'", + "DELETE FROM settings WHERE name = 'external_url_storage'", "UPDATE fleet_player_group SET slug = id WHERE slug = '' or slug is null", "UPDATE content SET uuid = id WHERE uuid = '' or uuid is null", "UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null", diff --git a/src/manager/VariableManager.py b/src/manager/VariableManager.py index ed78441..4016d62 100644 --- a/src/manager/VariableManager.py +++ b/src/manager/VariableManager.py @@ -117,7 +117,6 @@ class VariableManager: ### 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": False if demo else True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False}, - {"name": "external_url_storage", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": False if demo else True, "description": self.t('settings_variable_desc_external_storage_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}, @@ -125,6 +124,7 @@ class VariableManager: {"name": "intro_slide_duration", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 3, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_intro_slide_duration'), "refresh_player": False}, {"name": "default_slide_time_with_seconds", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_default_slide_time_with_seconds'), "refresh_player": False}, {"name": "polling_interval", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 5, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_polling_interval'), "refresh_player": True}, + {"name": "player_content_cache", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": True, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_player_content_cache'), "refresh_player": False}, ### Player Animation {"name": "slide_animation_enabled", "section": self.t(VariableSection.PLAYER_ANIMATION), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_slide_animation_enabled'), "refresh_player": True}, diff --git a/src/service/ExternalStorageServer.py b/src/service/ExternalStorageServer.py deleted file mode 100644 index 6bf7c2a..0000000 --- a/src/service/ExternalStorageServer.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import psutil -import platform -import logging -import http.server -import socketserver -import os -import signal -import sys -import socket -import threading - -from typing import Dict, Optional, List, Tuple, Union -from pathlib import Path - -from src.service.ModelStore import ModelStore -from src.model.entity.ExternalStorage import ExternalStorage - - -class ExternalStorageServer: - - def __init__(self, kernel, model_store: ModelStore): - self._kernel = kernel - self._model_store = model_store - - def get_directory(self): - return self._model_store.config().map().get('chroot_http_external_storage').replace( - '%application_dir%', self._kernel.get_application_dir() - ) - - def get_port(self) -> Optional[int]: - port = self._model_store.config().map().get('port_http_external_storage') - return int(port) if port else None - - def run(self): - port = self.get_port() - bind = self._model_store.config().map().get('bind_http_external_storage') - if not port: - return - - thread = threading.Thread(target=self._start, args=(self.get_directory(), port, bind)) - thread.daemon = True - thread.start() - - def _start(self, directory, port, bind): - class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=directory, **kwargs) - - Handler = CustomHTTPRequestHandler - - class ReusableTCPServer(socketserver.TCPServer): - allow_reuse_address = True - - with ReusableTCPServer((bind, port), Handler) as httpd: - logging.info("Serving external storage on path>{}:{}".format(directory, port)) - httpd.serve_forever() - - @staticmethod - def list_usb_storage_devices() -> List[ExternalStorage]: - os_type = platform.system() - partitions = psutil.disk_partitions() - removable_devices = [] - for partition in partitions: - if 'dontbrowse' in partition.opts: - continue - - if os_type == "Windows": - if 'removable' in partition.opts: - removable_devices.append(partition) - else: - if '/media' in partition.mountpoint or '/run/media' in partition.mountpoint or '/mnt' in partition.mountpoint or '/Volumes' in partition.mountpoint: - removable_devices.append(partition) - - if not removable_devices: - return {} - - storages = [] - - for device in removable_devices: - try: - usage = psutil.disk_usage(device.mountpoint) - # total_size = usage.total / (1024 ** 3) - external_storage = ExternalStorage( - logical_name=device.device, - mount_point=device.mountpoint, - content_id=None, - total_size=usage.total, - ) - storages.append(external_storage) - except Exception as e: - logging.error(f"Could not retrieve size for device {device.device}: {e}") - - return storages - - - @staticmethod - def get_external_storage_devices(): - return {storage.mount_point: "{} ({} - {}GB)".format( - storage.mount_point, - storage.logical_name, - storage.total_size_in_gigabytes() - ) for storage in ExternalStorageServer.list_usb_storage_devices()} \ No newline at end of file diff --git a/src/util/UtilExternalStorage.py b/src/util/UtilExternalStorage.py new file mode 100644 index 0000000..4556ca5 --- /dev/null +++ b/src/util/UtilExternalStorage.py @@ -0,0 +1,54 @@ +import os +import psutil +import platform +import logging +import os + +from typing import Dict, Optional, List, Tuple, Union + +from src.model.entity.ExternalStorage import ExternalStorage + + +def list_usb_storage_devices() -> List[ExternalStorage]: + os_type = platform.system() + partitions = psutil.disk_partitions() + removable_devices = [] + for partition in partitions: + if 'dontbrowse' in partition.opts: + continue + + if os_type == "Windows": + if 'removable' in partition.opts: + removable_devices.append(partition) + else: + if '/media' in partition.mountpoint or '/run/media' in partition.mountpoint or '/mnt' in partition.mountpoint or '/Volumes' in partition.mountpoint: + removable_devices.append(partition) + + if not removable_devices: + return {} + + storages = [] + + for device in removable_devices: + try: + usage = psutil.disk_usage(device.mountpoint) + # total_size = usage.total / (1024 ** 3) + external_storage = ExternalStorage( + logical_name=device.device, + mount_point=device.mountpoint, + content_id=None, + total_size=usage.total, + ) + storages.append(external_storage) + except Exception as e: + logging.error(f"Could not retrieve size for device {device.device}: {e}") + + return storages + + +def get_external_storage_devices(): + return {storage.mount_point: "{} ({} - {}GB)".format( + storage.mount_point, + storage.logical_name, + storage.total_size_in_gigabytes() + ) for storage in ExternalStorageServer.list_usb_storage_devices()} diff --git a/src/util/utils.py b/src/util/utils.py index b937700..023bc02 100644 --- a/src/util/utils.py +++ b/src/util/utils.py @@ -6,7 +6,7 @@ import inspect import subprocess import unicodedata import platform - +import urllib.parse from datetime import datetime, timedelta from typing import Optional, List, Dict @@ -335,3 +335,11 @@ def str_to_bool(value): return True else: raise ValueError('Boolean value expected.') + + +def encode_uri_component(uri_component): + return urllib.parse.quote(uri_component, safe='~()*!.\'') + + +def decode_uri_component(encoded_component): + return urllib.parse.unquote(encoded_component) diff --git a/system/autostart-browser-x11.sh b/system/autostart-browser-x11.sh index ea6950d..54a631c 100755 --- a/system/autostart-browser-x11.sh +++ b/system/autostart-browser-x11.sh @@ -16,4 +16,4 @@ 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 http://localhost:5000 +chromium-browser --disk-cache-size=2147483648 --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 http://localhost:5000 diff --git a/views/player/player.jinja.html b/views/player/player.jinja.html index e91e3a8..3176bcf 100755 --- a/views/player/player.jinja.html +++ b/views/player/player.jinja.html @@ -351,8 +351,7 @@ } const loadPicture = function(element, callbackReady, item) { - const hasScheme = item.location.indexOf('://') >= 0; - element.innerHTML = ``; + element.innerHTML = ``; callbackReady(function() {}); }; @@ -378,8 +377,7 @@ }; const loadVideo = function(element, callbackReady, item) { - const location = item.location.indexOf('http') === 0 ? item.location : `/${item.location}`; - element.innerHTML = ``; + element.innerHTML = ``; const video = element.querySelector('video'); callbackReady(function() {}); diff --git a/views/slideshow/contents/edit.jinja.html b/views/slideshow/contents/edit.jinja.html index 7edc989..b86f644 100644 --- a/views/slideshow/contents/edit.jinja.html +++ b/views/slideshow/contents/edit.jinja.html @@ -78,7 +78,7 @@
{% if content.type == enum_content_type.EXTERNAL_STORAGE %} - + {% endif %} {% set location = content.location %} diff --git a/views/slideshow/contents/modal/add.jinja.html b/views/slideshow/contents/modal/add.jinja.html index 373b222..d722872 100644 --- a/views/slideshow/contents/modal/add.jinja.html +++ b/views/slideshow/contents/modal/add.jinja.html @@ -57,7 +57,7 @@
- +