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
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

View File

@ -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

View File

@ -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

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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

View File

@ -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):

View File

@ -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/<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 = ''):
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

View File

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

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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},

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 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)

View File

@ -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

View File

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

View File

@ -78,7 +78,7 @@
</label>
<div class="widget vertical">
{% 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 %}
{% set location = content.location %}

View File

@ -57,7 +57,7 @@
<div class="form-group">
<label for="" class="object-label"></label>
<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" />
</div>
</div>