This commit is contained in:
jr-k 2024-08-08 02:07:14 +02:00
parent 70efdf877e
commit bdb4f1d7bd
22 changed files with 149 additions and 174 deletions

View File

@ -5,11 +5,7 @@ SECRET_KEY=ANY_SECRET_KEY_HERE
# Application Server # Application Server
PORT=5000 PORT=5000
BIND=0.0.0.0 BIND=0.0.0.0
EXTERNAL_STORAGE_MOUNTPOINT=%application_dir%/var/run/storage
# 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
# Misc # Misc
DEMO=false DEMO=false

View File

@ -10,10 +10,8 @@ services:
- DEBUG=false - DEBUG=false
- SECRET_KEY=ANY_SECRET_KEY_HERE - SECRET_KEY=ANY_SECRET_KEY_HERE
- PORT=5000 - PORT=5000
- PORT_HTTP_EXTERNAL_STORAGE=5001
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- ./:/app/ - ./:/app/
ports: ports:
- 5000:5000 - 5000:5000
- 5001:5001

View File

@ -8,7 +8,6 @@ services:
- DEBUG=false - DEBUG=false
- SECRET_KEY=ANY_SECRET_KEY_HERE - SECRET_KEY=ANY_SECRET_KEY_HERE
- PORT=5000 - PORT=5000
- PORT_HTTP_EXTERNAL_STORAGE=5001
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- ./data/db:/app/data/db - ./data/db:/app/data/db
@ -16,4 +15,3 @@ services:
- ./var/run/storage:/app/var/run/storage - ./var/run/storage:/app/var/run/storage
ports: ports:
- 5000:5000 - 5000:5000
- 5001:5001

View File

@ -184,12 +184,12 @@
"settings_variable_desc_auth_enabled": "Enable auth management", "settings_variable_desc_auth_enabled": "Enable auth management",
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be %username%/%password%", "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_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_slide_upload_limit": "Slide upload limit (in megabytes)",
"settings_variable_desc_dark_mode": "Dark mode", "settings_variable_desc_dark_mode": "Dark mode",
"settings_variable_desc_intro_slide_duration": "Introduction slide duration (in seconds)", "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_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_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_enabled": "Enable animation effect between slides",
"settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect", "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)", "settings_variable_desc_slide_animation_exit_effect": "Slide animation exit effect (generally better off without it)",

View File

@ -185,12 +185,12 @@
"settings_variable_desc_auth_enabled": "Habilitar gestión de autenticación", "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_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_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_slide_upload_limit": "Límite de carga de diapositivas (en megabytes)",
"settings_variable_desc_dark_mode": "Modo oscuro", "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_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_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_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_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_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)", "settings_variable_desc_slide_animation_exit_effect": "Efecto de salida de animación de diapositiva (generalmente mejor sin él)",

View File

@ -186,12 +186,12 @@
"settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification", "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_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_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_slide_upload_limit": "Limite d'upload du fichier d'une slide (en mégaoctets)",
"settings_variable_desc_dark_mode": "Mdoe sombre", "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_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_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_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_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_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)", "settings_variable_desc_slide_animation_exit_effect": "Effet d'animation de sortie de la slide (généralement mieux sans)",

View File

@ -185,12 +185,12 @@
"settings_variable_desc_auth_enabled": "Abilita la gestione autenticazione", "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_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_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_slide_upload_limit": "Limite upload slide (in megabytes)",
"settings_variable_desc_dark_mode": "Modalità scura", "settings_variable_desc_dark_mode": "Modalità scura",
"settings_variable_desc_intro_slide_duration": "Durata introduzione slide (in secondi)", "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_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_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_enabled": "Abilita l'effetto di animazione tra le diapositive",
"settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva", "settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva",
"settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)", "settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)",

View File

@ -6,7 +6,6 @@ import threading
from src.service.ModelStore import ModelStore from src.service.ModelStore import ModelStore
from src.service.PluginStore import PluginStore from src.service.PluginStore import PluginStore
from src.service.TemplateRenderer import TemplateRenderer from src.service.TemplateRenderer import TemplateRenderer
from src.service.ExternalStorageServer import ExternalStorageServer
from src.service.WebServer import WebServer from src.service.WebServer import WebServer
from src.model.enum.HookType import HookType from src.model.enum.HookType import HookType
@ -19,7 +18,6 @@ class Application:
self._model_store = ModelStore(self, self.get_plugins) 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._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._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())) 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) 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: if variable:
self._model_store.variable().update_by_name(variable.name, variable.as_int() + 1) self._model_store.variable().update_by_name(variable.name, variable.as_int() + 1)
self._external_storage_server.run()
self._web_server.run() self._web_server.run()
def signal_handler(self, signal, frame) -> None: def signal_handler(self, signal, frame) -> None:
@ -62,7 +59,3 @@ class Application:
self._model_store.lang().set_lang(lang) self._model_store.lang().set_lang(lang)
self._model_store.variable().reload() self._model_store.variable().reload()
self._plugin_store.reload_lang() self._plugin_store.reload_lang()
@property
def external_storage_server(self):
return self._external_storage_server

View File

@ -9,7 +9,6 @@ from src.model.entity.Content import Content
from src.model.enum.ContentType import ContentType from src.model.enum.ContentType import ContentType
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
from src.interface.ObController import ObController 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.utils import str_to_enum, get_optional_string
from src.util.UtilFile import randomize_filename 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), 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_content_type=ContentType,
enum_folder_entity=FolderEntity, 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): def slideshow_content_add(self):
@ -119,7 +118,7 @@ class ContentController(ObController):
working_folder_path=working_folder_path, working_folder_path=working_folder_path,
working_folder=working_folder, working_folder=working_folder,
enum_content_type=ContentType, 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): def slideshow_content_save(self, content_id: int = 0):

View File

@ -1,18 +1,20 @@
import os
import json import json
import logging import logging
import hashlib import hashlib
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional, List, Dict 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 pathlib import Path
from src.model.entity.Slide import Slide from src.model.entity.Slide import Slide
from src.model.entity.Content import Content
from src.model.enum.ContentType import ContentType from src.model.enum.ContentType import ContentType
from src.exceptions.NoFallbackPlaylistException import NoFallbackPlaylistException from src.exceptions.NoFallbackPlaylistException import NoFallbackPlaylistException
from src.service.ModelStore import ModelStore from src.service.ModelStore import ModelStore
from src.interface.ObController import ObController 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.util.UtilNetwork import get_safe_remote_addr, get_network_interfaces
from src.model.enum.AnimationSpeed import animation_speed_duration 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/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', 'player_playlist', self.player_playlist, methods=['GET'])
self._app.add_url_rule('/player/playlist/use/<playlist_slug_or_id>', 'player_playlist_use', self.player_playlist, methods=['GET']) self._app.add_url_rule('/player/playlist/use/<playlist_slug_or_id>', 'player_playlist_use', self.player_playlist, methods=['GET'])
self._app.add_url_rule('/serve/content/<content_type>/<content_id>/<content_location>', 'serve_content_file', self.serve_content_file, methods=['GET'])
def player(self, playlist_slug_or_id: str = ''): def player(self, playlist_slug_or_id: str = ''):
preview_content_id = request.args.get('preview_content_id') preview_content_id = request.args.get('preview_content_id')
@ -136,24 +139,26 @@ class PlayerController(ObController):
content = contents[int(slide['content_id'])] content = contents[int(slide['content_id'])]
slide['name'] = content.name slide['name'] = content.name
slide['location'] = content.location
slide['type'] = content.type.value slide['type'] = content.type.value
slide['location'] = self._model_store.content().resolve_content_location(content)
if slide['type'] == ContentType.EXTERNAL_STORAGE.value: 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(): if mount_point_dir.is_dir():
for file in mount_point_dir.iterdir(): for file in mount_point_dir.iterdir():
if file.is_file() and not file.stem.startswith('.'): 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 = dict(slide)
slide['id'] = hashlib.md5(str(file).encode('utf-8')).hexdigest() slide['id'] = hashlib.md5(str(file).encode('utf-8')).hexdigest()
slide['position'] = position 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['delegate_duration'] = 1 if slide['type'] == ContentType.VIDEO.value else 0
slide['location'] = "{}/{}".format( slide['name'] = virtual_content.stem
self._model_store.content().resolve_content_location(content), slide['type'] = virtual_content.type.value
file.name slide['location'] = self._model_store.content().resolve_content_location(virtual_content)
)
self._check_slide_enablement(playlist_loop, playlist_notifications, slide) self._check_slide_enablement(playlist_loop, playlist_notifications, slide)
position = position + 1 position = position + 1
else: else:
@ -197,3 +202,39 @@ class PlayerController(ObController):
return return
loop.append(slide) 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

View File

@ -40,12 +40,12 @@ class ObController(abc.ABC):
def reload_lang(self, lang: str): def reload_lang(self, lang: str):
self._kernel.reload_lang(lang) self._kernel.reload_lang(lang)
def get_application_dir(self):
return self._kernel.get_application_dir()
def t(self, token) -> Union[Dict, str]: def t(self, token) -> Union[Dict, str]:
return self._model_store.lang().translate(token) 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: def render_view(self, template_file: str, **parameters: dict) -> str:
return self._template_renderer.render_view(template_file, self.plugin(), **parameters) return self._template_renderer.render_view(template_file, self.plugin(), **parameters)

View File

@ -11,7 +11,6 @@ class ConfigManager:
APPLICATION_NAME = "Obscreen" APPLICATION_NAME = "Obscreen"
DEFAULT_PORT = 5000 DEFAULT_PORT = 5000
DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001
VERSION_FILE = 'version.txt' VERSION_FILE = 'version.txt'
def __init__(self, replacers: Dict): def __init__(self, replacers: Dict):
@ -20,9 +19,7 @@ class ConfigManager:
'application_name': self.APPLICATION_NAME, 'application_name': self.APPLICATION_NAME,
'version': None, 'version': None,
'demo': False, 'demo': False,
'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE, 'external_storage_mountpoint': '%application_dir%/var/run/storage',
'bind_http_external_storage': '0.0.0.0',
'chroot_http_external_storage': '%application_dir%/var/run/storage',
'port': self.DEFAULT_PORT, 'port': self.DEFAULT_PORT,
'bind': '0.0.0.0', 'bind': '0.0.0.0',
'debug': False, '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-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('--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('--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('--external-storage-mountpoint', '-e', default=self._CONFIG['external_storage_mountpoint'], help='Mountpoint directory of 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('--version', '-v', default=None, action='store_true', help='Get version number') parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number')
return parser.parse_args() return parser.parse_args()
@ -74,12 +69,8 @@ class ConfigManager:
self._CONFIG['debug'] = args.debug self._CONFIG['debug'] = args.debug
if args.demo: if args.demo:
self._CONFIG['demo'] = args.demo self._CONFIG['demo'] = args.demo
if args.port_http_external_storage: if args.external_storage_mountpoint:
self._CONFIG['port_http_external_storage'] = args.port_http_external_storage self._CONFIG['external_storage_mountpoint'] = args.external_storage_mountpoint
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.log_file: if args.log_file:
self._CONFIG['log_file'] = args.log_file self._CONFIG['log_file'] = args.log_file
if args.secret_key: if args.secret_key:

View File

@ -2,6 +2,7 @@ import os
from typing import Dict, Optional, List, Tuple, Union from typing import Dict, Optional, List, Tuple, Union
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from flask import url_for
from src.model.entity.Content import Content from src.model.entity.Content import Content
from src.model.entity.Playlist import Playlist 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.UtilFile import randomize_filename
from src.util.UtilNetwork import get_preferred_ip_address from src.util.UtilNetwork import get_preferred_ip_address
from src.util.UtilVideo import mp4_duration_with_ffprobe from src.util.UtilVideo import mp4_duration_with_ffprobe
from src.util.utils import encode_uri_component
class ContentManager(ModelManager): 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('/') var_external_url = self._variable_manager.get_one_by_name('external_url').as_string().strip().strip('/')
location = content.location location = content.location
if content.type == ContentType.EXTERNAL_STORAGE: if content.type == ContentType.YOUTUBE:
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:
location = "https://www.youtube.com/watch?v={}".format(content.location) location = "https://www.youtube.com/watch?v={}".format(content.location)
elif len(var_external_url) > 0 and content.has_file(): elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE:
location = "{}/{}".format(var_external_url, content.location) location = "{}/{}".format(
elif content.has_file(): var_external_url if len(var_external_url) > 0 else "",
location = "/{}".format(content.location) 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: elif content.type == ContentType.URL:
location = 'http://' + content.location if not content.location.startswith('http') else content.location location = 'http://' + content.location if not content.location.startswith('http') else content.location

View File

@ -214,6 +214,7 @@ class DatabaseManager:
"DELETE FROM settings WHERE name = 'playlist_default_time_sync'", "DELETE FROM settings WHERE name = 'playlist_default_time_sync'",
"DELETE FROM settings WHERE name = 'slide_animation_exit_effect'", "DELETE FROM settings WHERE name = 'slide_animation_exit_effect'",
"DELETE FROM settings WHERE name = 'playlist_enabled'", "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 fleet_player_group SET slug = id WHERE slug = '' or slug is null",
"UPDATE content SET uuid = id WHERE uuid = '' or uuid is null", "UPDATE content SET uuid = id WHERE uuid = '' or uuid is null",
"UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null", "UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null",

View File

@ -117,7 +117,6 @@ class VariableManager:
### General ### 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": "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", "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": "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}, {"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": "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": "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": "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 ### 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}, {"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},

View File

@ -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()}

View File

@ -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()}

View File

@ -6,7 +6,7 @@ import inspect
import subprocess import subprocess
import unicodedata import unicodedata
import platform import platform
import urllib.parse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, List, Dict from typing import Optional, List, Dict
@ -335,3 +335,11 @@ def str_to_bool(value):
return True return True
else: else:
raise ValueError('Boolean value expected.') 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)

View File

@ -16,4 +16,4 @@ WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2) HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
# Start Chromium in kiosk mode # 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

View File

@ -351,8 +351,7 @@
} }
const loadPicture = function(element, callbackReady, item) { const loadPicture = function(element, callbackReady, item) {
const hasScheme = item.location.indexOf('://') >= 0; element.innerHTML = `<img src="${item.location}" alt="" />`;
element.innerHTML = `<img src="${hasScheme ? item.location : ('/' + item.location)}" alt="" />`;
callbackReady(function() {}); callbackReady(function() {});
}; };
@ -378,8 +377,7 @@
}; };
const loadVideo = function(element, callbackReady, item) { const loadVideo = function(element, callbackReady, item) {
const location = item.location.indexOf('http') === 0 ? item.location : `/${item.location}`; element.innerHTML = `<video ${previewMode ? 'controls' : ''}><source src=${item.location} type="video/mp4" /></video>`;
element.innerHTML = `<video ${previewMode ? 'controls' : ''}><source src=${location} type="video/mp4" /></video>`;
const video = element.querySelector('video'); const video = element.querySelector('video');
callbackReady(function() {}); callbackReady(function() {});

View File

@ -78,7 +78,7 @@
</label> </label>
<div class="widget vertical"> <div class="widget vertical">
{% if content.type == enum_content_type.EXTERNAL_STORAGE %} {% if content.type == enum_content_type.EXTERNAL_STORAGE %}
<input type="text" class="disabled" disabled value="{{ chroot_http_external_storage }}/" /> <input type="text" class="disabled" disabled value="{{ external_storage_mountpoint }}/" />
{% endif %} {% endif %}
{% set location = content.location %} {% set location = content.location %}

View File

@ -57,7 +57,7 @@
<div class="form-group"> <div class="form-group">
<label for="" class="object-label"></label> <label for="" class="object-label"></label>
<div class="widget vertical"> <div class="widget vertical">
<input type="text" class="disabled" value="{{ chroot_http_external_storage }}/" /> <input type="text" class="disabled" value="{{ external_storage_mountpoint }}/" />
<input type="text" name="object" data-input-type="storage" class="content-object-input" disabled="disabled" /> <input type="text" name="object" data-input-type="storage" class="content-object-input" disabled="disabled" />
</div> </div>
</div> </div>