This commit is contained in:
jr-k 2024-07-21 01:25:40 +02:00
parent 046666b524
commit b7231a35a5
19 changed files with 500 additions and 50 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ var/run/*
.env .env
venv/ venv/
node_modules node_modules
tmp.py

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,14 +9,15 @@ jQuery(document).ready(function ($) {
}).data('input'); }).data('input');
$form.find('.content-object-input').each(function() { $form.find('.content-object-input').each(function() {
const active = $(this).attr('data-input-type') === inputType; const $input = $(this);
const active = $input.attr('data-input-type') === inputType;
if ($(this).is('input[type=file]')) { const $holder = $input.parents('.object-holder:eq(0)');
$(this).prop('disabled', !active).prop('required', active); $holder.find('input, select, textarea').prop('disabled', !active).prop('required', active).toggleClass('hidden', !active);
$(this).parents('label:eq(0)').toggleClass('hidden', !active); $holder.toggleClass('hidden', !active);
} else { console.log(active)
$(this).prop('disabled', !active).prop('required', active).toggleClass('hidden', !active); console.log($input)
} if (active)
console.log($holder)
}); });
const optionAttributes = $selectedOption.get(0).attributes; const optionAttributes = $selectedOption.get(0).attributes;

View File

@ -35,6 +35,28 @@ form {
flex: 1; flex: 1;
margin-bottom: 20px; margin-bottom: 20px;
.object-holder {
flex-direction: column;
align-self: stretch;
justify-content: flex-start;
align-items: flex-start;
display: flex;
flex: 1;
input,
select,
textarea {
flex: 1;
align-self: stretch;
}
.form-group {
label {
margin-top: 20px;
margin-bottom: 8px;
}
}
}
label { label {
flex: 1; flex: 1;

View File

@ -226,6 +226,7 @@
"basic_month_10": "October", "basic_month_10": "October",
"basic_month_11": "November", "basic_month_11": "November",
"basic_month_12": "December", "basic_month_12": "December",
"common_bad_directory_path": "Directory does not exist in the specified path",
"common_bad_file_type": "Bad file type uploaded", "common_bad_file_type": "Bad file type uploaded",
"common_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect", "common_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect",
"common_pick_element": "Pick an element", "common_pick_element": "Pick an element",
@ -282,6 +283,9 @@
"enum_application_language_french": "French", "enum_application_language_french": "French",
"enum_application_language_italian": "Italian", "enum_application_language_italian": "Italian",
"enum_application_language_spanish": "Spanish", "enum_application_language_spanish": "Spanish",
"enum_content_type_external_storage": "External Storage",
"enum_content_type_external_storage_object_label": "Choose an external storage drive",
"enum_content_type_external_storage_target_path_label": "Write an existing directory path inside your storage device",
"enum_content_type_url": "URL", "enum_content_type_url": "URL",
"enum_content_type_video": "Video", "enum_content_type_video": "Video",
"enum_content_type_picture": "Picture", "enum_content_type_picture": "Picture",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Octubre", "basic_month_10": "Octubre",
"basic_month_11": "Noviembre", "basic_month_11": "Noviembre",
"basic_month_12": "Diciembre", "basic_month_12": "Diciembre",
"common_bad_directory_path": "El directorio no existe en la ruta especificada",
"common_bad_file_type": "Tipo de archivo incorrecto cargado", "common_bad_file_type": "Tipo de archivo incorrecto cargado",
"common_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto", "common_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto",
"common_pick_element": "Elige un elemento", "common_pick_element": "Elige un elemento",
@ -283,6 +284,9 @@
"enum_application_language_french": "Francés", "enum_application_language_french": "Francés",
"enum_application_language_italian": "Italiano", "enum_application_language_italian": "Italiano",
"enum_application_language_spanish": "Español", "enum_application_language_spanish": "Español",
"enum_content_type_external_storage": "Almacenamiento externo",
"enum_content_type_external_storage_object_label": "Elija una unidad de almacenamiento externa",
"enum_content_type_external_storage_target_path_label": "Escribe una ruta de directorio existente dentro de tu dispositivo de almacenamiento",
"enum_content_type_url": "URL", "enum_content_type_url": "URL",
"enum_content_type_video": "Video", "enum_content_type_video": "Video",
"enum_content_type_picture": "Imagen", "enum_content_type_picture": "Imagen",

View File

@ -228,6 +228,7 @@
"basic_month_10": "Octobre", "basic_month_10": "Octobre",
"basic_month_11": "Novembre", "basic_month_11": "Novembre",
"basic_month_12": "Décembre", "basic_month_12": "Décembre",
"common_bad_directory_path": "Le dossier n'existe pas dans le chemin indiqué",
"common_bad_file_type": "Type de fichier uploadé incorrect", "common_bad_file_type": "Type de fichier uploadé incorrect",
"common_restart_needed": "Veuillez redémarrer obscreen studio (ou redémarrer l'appareil) pour que les changements soient pris en compte", "common_restart_needed": "Veuillez redémarrer obscreen studio (ou redémarrer l'appareil) pour que les changements soient pris en compte",
"common_pick_element": "Choisissez un élément", "common_pick_element": "Choisissez un élément",
@ -284,6 +285,9 @@
"enum_application_language_french": "Français", "enum_application_language_french": "Français",
"enum_application_language_italian": "Italien", "enum_application_language_italian": "Italien",
"enum_application_language_spanish": "Espagnol", "enum_application_language_spanish": "Espagnol",
"enum_content_type_external_storage": "Stockage externe",
"enum_content_type_external_storage_object_label": "Choisissez un disque de stockage externe",
"enum_content_type_external_storage_target_path_label": "Écrivez un chemin de répertoire existant dans votre périphérique de stockage",
"enum_content_type_url": "URL", "enum_content_type_url": "URL",
"enum_content_type_video": "Vidéo", "enum_content_type_video": "Vidéo",
"enum_content_type_picture": "Image", "enum_content_type_picture": "Image",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Ottobre", "basic_month_10": "Ottobre",
"basic_month_11": "Novembre", "basic_month_11": "Novembre",
"basic_month_12": "Dicembre", "basic_month_12": "Dicembre",
"common_bad_directory_path": "La directory non esiste nel percorso specificato",
"common_bad_file_type": "Tipo di file caricato non valido", "common_bad_file_type": "Tipo di file caricato non valido",
"common_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto", "common_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto",
"common_pick_element": "Scegli un elemento", "common_pick_element": "Scegli un elemento",
@ -283,6 +284,9 @@
"enum_application_language_french": "Francese", "enum_application_language_french": "Francese",
"enum_application_language_italian": "Italiano", "enum_application_language_italian": "Italiano",
"enum_application_language_spanish": "Spagnolo", "enum_application_language_spanish": "Spagnolo",
"enum_content_type_external_storage": "Archiviazione esterna",
"enum_content_type_external_storage_object_label": "Scegli un'unità di archiviazione esterna",
"enum_content_type_external_storage_target_path_label": "Écrivez un chemin de répertoire existant dans votre périphérique de stockage",
"enum_content_type_url": "URL", "enum_content_type_url": "URL",
"enum_content_type_video": "Video", "enum_content_type_video": "Video",
"enum_content_type_picture": "Immagine", "enum_content_type_picture": "Immagine",

View File

@ -48,6 +48,7 @@ class ContentController(ObController):
self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list') self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list')
working_folder_path, working_folder = self.get_working_folder() working_folder_path, working_folder = self.get_working_folder()
slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True) slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True)
external_storages = self._model_store.external_storage().list_usb_storage_devices()
return render_template( return render_template(
'slideshow/contents/list.jinja.html', 'slideshow/contents/list.jinja.html',
@ -59,6 +60,11 @@ 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,
external_storages={storage.mount_point: "{} ({} - {}GB)".format(
storage.mount_point,
storage.logical_name,
storage.total_size_in_gigabytes()
) for storage in external_storages},
) )
def slideshow_content_add(self): def slideshow_content_add(self):
@ -67,12 +73,21 @@ class ContentController(ObController):
"path": working_folder_path, "path": working_folder_path,
} }
location = request.form['object'] if 'object' in request.form else None
if 'storage' in request.form:
location = "{}/{}".format(request.form['storage'], location.strip('/'))
if not os.path.exists(location):
route_args["error"] = "common_bad_directory_path"
return redirect(url_for('slideshow_content_list', **route_args))
content = self._model_store.content().add_form_raw( content = self._model_store.content().add_form_raw(
name=request.form['name'], name=request.form['name'],
type=str_to_enum(request.form['type'], ContentType), type=str_to_enum(request.form['type'], ContentType),
request_files=request.files, request_files=request.files,
upload_dir=self._app.config['UPLOAD_FOLDER'], upload_dir=self._app.config['UPLOAD_FOLDER'],
location=request.form['object'] if 'object' in request.form else None, location=location,
folder_id=working_folder.id if working_folder else None folder_id=working_folder.id if working_folder else None
) )
@ -87,7 +102,7 @@ class ContentController(ObController):
for key in request.files: for key in request.files:
files = request.files.getlist(key) files = request.files.getlist(key)
for file in files: for file in files:
type = ContentType.guess_content_type_file(file) type = ContentType.guess_content_type_file(file.filename)
name = file.filename.rsplit('.', 1)[0] name = file.filename.rsplit('.', 1)[0]
if type: if type:
@ -240,7 +255,9 @@ class ContentController(ObController):
var_external_url = self._model_store.variable().get_one_by_name('external_url') var_external_url = self._model_store.variable().get_one_by_name('external_url')
location = content.location location = content.location
if content.type == ContentType.YOUTUBE: if content.type == ContentType.EXTERNAL_STORAGE:
location = "file://{}".format(location)
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.as_string().strip()) > 0 and content.has_file(): elif len(var_external_url.as_string().strip()) > 0 and content.has_file():
location = "{}/{}".format(var_external_url.value, content.location) location = "{}/{}".format(var_external_url.value, content.location)

View File

@ -2,10 +2,12 @@ import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Optional 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
from pathlib import Path
from src.model.entity.Slide import Slide from src.model.entity.Slide import Slide
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
@ -123,7 +125,9 @@ class PlayerController(ObController):
playlist_notifications = [] playlist_notifications = []
for slide in slides: for slide in slides:
if slide['content_id']: if not slide['content_id']:
continue
if int(slide['content_id']) not in contents: if int(slide['content_id']) not in contents:
continue continue
@ -131,31 +135,18 @@ class PlayerController(ObController):
slide['name'] = content['name'] slide['name'] = content['name']
slide['location'] = content['location'] slide['location'] = content['location']
slide['type'] = content['type'] slide['type'] = content['type']
else:
continue
has_valid_start_date = 'cron_schedule' in slide and slide['cron_schedule'] and get_safe_cron_descriptor(slide['cron_schedule']) and is_cron_calendar_moment(slide['cron_schedule']) if slide['type'] == ContentType.EXTERNAL_STORAGE.value:
has_valid_end_date = 'cron_schedule_end' in slide and slide['cron_schedule_end'] and get_safe_cron_descriptor(slide['cron_schedule_end']) and is_cron_calendar_moment(slide['cron_schedule_end']) mount_point_dir = Path(slide['location'])
if mount_point_dir.is_dir():
if slide['is_notification']: for file in mount_point_dir.iterdir():
if has_valid_start_date: if file.is_file() and not file.stem.startswith('.'):
playlist_notifications.append(slide) slide['type'] = ContentType.guess_content_type_file(str(file.resolve())).value
slide['location'] = "file://{}".format(str(file.resolve()))
slide['name'] = file.stem
self._feed_playlist(playlist_loop, playlist_notifications, slide)
else: else:
logging.warn('Slide \'{}\' is a notification but start date is invalid'.format(slide['name'])) self._feed_playlist(playlist_loop, playlist_notifications, slide)
else:
if has_valid_start_date:
start_date = get_cron_date_time(slide['cron_schedule'], object=True)
if datetime.now() <= start_date:
continue
if has_valid_end_date:
end_date = get_cron_date_time(slide['cron_schedule_end'], object=True)
if datetime.now() >= end_date:
continue
playlist_loop.append(slide)
else:
playlist_loop.append(slide)
playlists = { playlists = {
'playlist_id': playlist.id if playlist else None, 'playlist_id': playlist.id if playlist else None,
@ -167,3 +158,24 @@ class PlayerController(ObController):
} }
return playlists return playlists
def _feed_playlist(self, loop: List, notifications: List, slide: Dict) -> None:
has_valid_start_date = 'cron_schedule' in slide and slide['cron_schedule'] and get_safe_cron_descriptor(slide['cron_schedule']) and is_cron_calendar_moment(slide['cron_schedule'])
has_valid_end_date = 'cron_schedule_end' in slide and slide['cron_schedule_end'] and get_safe_cron_descriptor(slide['cron_schedule_end']) and is_cron_calendar_moment(slide['cron_schedule_end'])
if slide['is_notification']:
if has_valid_start_date:
return notifications.append(slide)
return logging.warn('Slide \'{}\' is a notification but start date is invalid'.format(slide['name']))
if has_valid_start_date:
start_date = get_cron_date_time(slide['cron_schedule'], object=True)
if datetime.now() <= start_date:
return
if has_valid_end_date:
end_date = get_cron_date_time(slide['cron_schedule_end'], object=True)
if datetime.now() >= end_date:
return
loop.append(slide)

View File

@ -177,7 +177,7 @@ class ContentManager(ModelManager):
if not object or object.filename == '': if not object or object.filename == '':
return None return None
guessed_type = ContentType.guess_content_type_file(object) guessed_type = ContentType.guess_content_type_file(object.filename)
if not guessed_type or guessed_type != type: if not guessed_type or guessed_type != type:
return None return None

View File

@ -0,0 +1,208 @@
import os
import psutil
import platform
import logging
from typing import Dict, Optional, List, Tuple, Union
from werkzeug.datastructures import FileStorage
from src.model.entity.ExternalStorage import ExternalStorage
from src.util.utils import get_yt_video_id
from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
from src.service.ModelManager import ModelManager
from src.util.UtilFile import randomize_filename
class ExternalStorageManager(ModelManager):
TABLE_NAME = "external_storage"
TABLE_MODEL = [
"uuid CHAR(255)",
"total_size INTEGER",
"logical_name TEXT",
"mount_point TEXT",
"content_id INTEGER",
"created_by CHAR(255)",
"updated_by CHAR(255)",
"created_at INTEGER",
"updated_at INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager, variable_manager: VariableManager):
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
def hydrate_object(self, raw_external_storage: dict, id: int = None) -> ExternalStorage:
if id:
raw_external_storage['id'] = id
[raw_external_storage, user_tracker_edits] = self.user_manager.initialize_user_trackers(raw_external_storage)
if len(user_tracker_edits) > 0:
self._db.update_by_id(self.TABLE_NAME, raw_external_storage['id'], user_tracker_edits)
return ExternalStorage(**raw_external_storage)
def hydrate_list(self, raw_external_storages: list) -> List[ExternalStorage]:
return [self.hydrate_object(raw_external_storage) for raw_external_storage in raw_external_storages]
def get(self, id: int) -> Optional[ExternalStorage]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query, sort: Optional[str] = None) -> List[ExternalStorage]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_one_by(self, query) -> Optional[ExternalStorage]:
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_all(self, sort: Optional[str] = 'created_at', ascending=False) -> List[ExternalStorage]:
return self.hydrate_list(self._db.get_all(table_name=self.TABLE_NAME, sort=sort, ascending=ascending))
def get_all_indexed(self, attribute: str = 'id', multiple=False) -> Dict[str, ExternalStorage]:
index = {}
for item in self.get_external_storages():
id = getattr(item, attribute)
if multiple:
if id not in index:
index[id] = []
index[id].append(item)
else:
index[id] = item
return index
def forget_for_user(self, user_id: int):
external_storages = self.get_by("created_by = '{}' or updated_by = '{}'".format(user_id, user_id))
edits_external_storages = self.user_manager.forget_user_for_entity(external_storages, user_id)
for external_storage_id, edits in edits_external_storages.items():
self._db.update_by_id(self.TABLE_NAME, external_storage_id, edits)
def get_external_storages(self, content_id: Optional[int] = None) -> List[ExternalStorage]:
query = " 1=1 "
if content_id:
query = "{} {}".format(query, "AND content_id = {}".format(content_id))
return self.get_by(query=query)
def pre_add(self, external_storage: Dict) -> Dict:
self.user_manager.track_user_on_create(external_storage)
self.user_manager.track_user_on_update(external_storage)
return external_storage
def pre_update(self, external_storage: Dict) -> Dict:
self.user_manager.track_user_on_update(external_storage)
return external_storage
def pre_delete(self, external_storage_id: str) -> str:
return external_storage_id
def post_add(self, external_storage_id: str) -> str:
return external_storage_id
def post_update(self, external_storage_id: str) -> str:
return external_storage_id
def post_updates(self):
pass
def post_delete(self, external_storage_id: str) -> str:
return external_storage_id
def update_form(self, id: int, logical_name: Optional[str] = None, mount_point: Optional[str] = None, content_id: Optional[int] = None, total_size: Optional[int] = None) -> ExternalStorage:
external_storage = self.get(id)
if not external_storage:
return
form = {
"total_size": total_size if total_size else external_storage.total_size,
"logical_name": logical_name if logical_name else external_storage.logical_name,
"mount_point": mount_point if mount_point else external_storage.mount_point,
"content_id": content_id if content_id else external_storage.content_id,
}
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
self.post_update(id)
return self.get(id)
def add_form(self, external_storage: Union[ExternalStorage, Dict]) -> None:
form = external_storage
if not isinstance(external_storage, dict):
form = external_storage.to_dict()
del form['id']
self._db.add(self.TABLE_NAME, self.pre_add(form))
self.post_add(external_storage.id)
def add_form_raw(self, logical_name: Optional[str] = None, mount_point: Optional[str] = None, content_id: Optional[int] = None, total_size: Optional[int] = None) -> ExternalStorage:
external_storage = ExternalStorage(
logical_name=logical_name,
mount_point=mount_point,
content_id=content_id,
total_size=total_size,
)
self.add_form(external_storage)
return self.get_one_by(query="uuid = '{}'".format(external_storage.uuid))
def delete(self, id: int) -> None:
external_storage = self.get(id)
if external_storage:
self.pre_delete(id)
self._db.delete_by_id(self.TABLE_NAME, id)
self.post_delete(id)
def to_dict(self, external_storages: List[ExternalStorage]) -> List[Dict]:
return [external_storage.to_dict() for external_storage in external_storages]
@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

View File

@ -0,0 +1,141 @@
import json
import time
import uuid
import math
from typing import Optional, Union
from src.model.enum.FolderEntity import FolderEntity
from src.util.utils import str_to_enum
class ExternalStorage:
def __init__(self, uuid: str = '', total_size: int = 0, logical_name: str = '', mount_point: str = '', content_id: Optional[int] = None, id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._uuid = uuid if uuid else self.generate_and_set_uuid()
self._id = id if id else None
self._total_size = total_size
self._logical_name = logical_name
self._mount_point = mount_point
self._content_id = content_id
self._created_by = created_by if created_by else None
self._updated_by = updated_by if updated_by else None
self._created_at = int(created_at if created_at else time.time())
self._updated_at = int(updated_at if updated_at else time.time())
def generate_and_set_uuid(self) -> str:
self._uuid = str(uuid.uuid4())
return self._uuid
@property
def id(self) -> Optional[int]:
return self._id
@property
def uuid(self) -> str:
return self._uuid
@uuid.setter
def uuid(self, value: str):
self._uuid = value
@property
def content_id(self) -> Optional[int]:
return self._content_id
@content_id.setter
def content_id(self, value: Optional[int]):
self._content_id = value
@property
def total_size(self) -> int:
return self._total_size
@total_size.setter
def total_size(self, value: int):
self._total_size = value
@property
def logical_name(self) -> str:
return self._logical_name
@logical_name.setter
def logical_name(self, value: str):
self._logical_name = value
@property
def mount_point(self) -> str:
return self._mount_point
@mount_point.setter
def mount_point(self, value: str):
self._mount_point = value
@property
def created_by(self) -> str:
return self._created_by
@created_by.setter
def created_by(self, value: str):
self._created_by = value
@property
def updated_by(self) -> str:
return self._updated_by
@updated_by.setter
def updated_by(self, value: str):
self._updated_by = value
@property
def created_at(self) -> int:
return self._created_at
@created_at.setter
def created_at(self, value: int):
self._created_at = value
@property
def updated_at(self) -> int:
return self._updated_at
@updated_at.setter
def updated_at(self, value: int):
self._updated_at = value
def __str__(self) -> str:
return f"ExternalStorage(" \
f"id='{self.id}',\n" \
f"uuid='{self.uuid}',\n" \
f"total_size='{self.total_size}',\n" \
f"logical_name='{self.logical_name}',\n" \
f"mount_point='{self.mount_point}',\n" \
f"content_id='{self.content_id}',\n" \
f"created_by='{self.created_by}',\n" \
f"updated_by='{self.updated_by}',\n" \
f"created_at='{self.created_at}',\n" \
f"updated_at='{self.updated_at}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_dict(self) -> dict:
return {
"id": self.id,
"uuid": self.uuid,
"total_size": self.total_size,
"logical_name": self.logical_name,
"mount_point": self.mount_point,
"content_id": self.content_id,
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
def total_size_in_gigabytes(self) -> str:
return f"{self.total_size / (1024 ** 3):.2f}"

View File

@ -10,6 +10,7 @@ class ContentInputType(Enum):
UPLOAD = 'upload' UPLOAD = 'upload'
TEXT = 'text' TEXT = 'text'
STORAGE = 'storage'
@staticmethod @staticmethod
def is_editable(value: Enum) -> bool: def is_editable(value: Enum) -> bool:
@ -17,18 +18,21 @@ class ContentInputType(Enum):
return False return False
elif value == ContentInputType.TEXT: elif value == ContentInputType.TEXT:
return True return True
elif value == ContentInputType.STORAGE:
return False
class ContentType(Enum): class ContentType(Enum):
EXTERNAL_STORAGE = 'external_storage'
PICTURE = 'picture' PICTURE = 'picture'
URL = 'url' URL = 'url'
YOUTUBE = 'youtube' YOUTUBE = 'youtube'
VIDEO = 'video' VIDEO = 'video'
@staticmethod @staticmethod
def guess_content_type_file(file): def guess_content_type_file(filename: str):
mime_type, _ = mimetypes.guess_type(file.filename) mime_type, _ = mimetypes.guess_type(filename)
if mime_type in [ if mime_type in [
'image/gif', 'image/gif',
@ -55,6 +59,8 @@ class ContentType(Enum):
return ContentInputType.TEXT return ContentInputType.TEXT
elif value == ContentType.URL: elif value == ContentType.URL:
return ContentInputType.TEXT return ContentInputType.TEXT
elif value == ContentType.EXTERNAL_STORAGE:
return ContentInputType.STORAGE
@staticmethod @staticmethod
def get_fa_icon(value: Union[Enum, str]) -> str: def get_fa_icon(value: Union[Enum, str]) -> str:
@ -69,6 +75,8 @@ class ContentType(Enum):
return 'fa-brands fa-youtube' return 'fa-brands fa-youtube'
elif value == ContentType.URL: elif value == ContentType.URL:
return 'fa-link' return 'fa-link'
elif value == ContentType.EXTERNAL_STORAGE:
return 'fa-brands fa-usb'
return 'fa-file' return 'fa-file'
@ -85,5 +93,7 @@ class ContentType(Enum):
return 'youtube' return 'youtube'
elif value == ContentType.URL: elif value == ContentType.URL:
return 'danger' return 'danger'
elif value == ContentType.EXTERNAL_STORAGE:
return 'other'
return 'neutral' return 'neutral'

View File

@ -12,6 +12,7 @@ from src.manager.LangManager import LangManager
from src.manager.DatabaseManager import DatabaseManager from src.manager.DatabaseManager import DatabaseManager
from src.manager.ConfigManager import ConfigManager from src.manager.ConfigManager import ConfigManager
from src.manager.LoggingManager import LoggingManager from src.manager.LoggingManager import LoggingManager
from src.manager.ExternalStorageManager import ExternalStorageManager
class ModelStore: class ModelStore:
@ -39,6 +40,7 @@ class ModelStore:
self._playlist_manager = PlaylistManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager) self._playlist_manager = PlaylistManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager) self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._content_manager = ContentManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager) self._content_manager = ContentManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._external_storage_manager = ExternalStorageManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._variable_manager.reload() self._variable_manager.reload()
def logging(self) -> LoggingManager: def logging(self) -> LoggingManager:
@ -47,6 +49,9 @@ class ModelStore:
def config(self) -> ConfigManager: def config(self) -> ConfigManager:
return self._config_manager return self._config_manager
def external_storage(self) -> ExternalStorageManager:
return self._external_storage_manager
def variable(self) -> VariableManager: def variable(self) -> VariableManager:
return self._variable_manager return self._variable_manager
@ -81,6 +86,7 @@ class ModelStore:
return self._get_plugins() return self._get_plugins()
def on_user_delete(self, user_id: int) -> None: def on_user_delete(self, user_id: int) -> None:
self._external_storage_manager.forget_for_user(user_id)
self._playlist_manager.forget_for_user(user_id) self._playlist_manager.forget_for_user(user_id)
self._folder_manager.forget_for_user(user_id) self._folder_manager.forget_for_user(user_id)
self._node_player_group_manager.forget_for_user(user_id) self._node_player_group_manager.forget_for_user(user_id)

View File

@ -327,7 +327,8 @@
} }
const loadPicture = function(element, callbackReady, item) { const loadPicture = function(element, callbackReady, item) {
element.innerHTML = `<img src="/${item.location}" alt="" />`; const hasScheme = item.location.indexOf('://') >= 0;
element.innerHTML = `<img src="${hasScheme ? item.location : ('/' + item.location)}" alt="" />`;
callbackReady(function() {}); callbackReady(function() {});
}; };

View File

@ -11,6 +11,7 @@
{% block add_js %} {% block add_js %}
<script> <script>
var external_storages = {{ json_dumps(external_storages) | safe }};
var l = $.extend(l, { var l = $.extend(l, {
'js_common_http_error_occured': '{{ l.common_http_error_occured }}', 'js_common_http_error_occured': '{{ l.common_http_error_occured }}',
'js_common_http_error_413': '{{ l.common_http_error_413 }}' 'js_common_http_error_413': '{{ l.common_http_error_413 }}'

View File

@ -30,16 +30,30 @@
<div class="form-group object-input"> <div class="form-group object-input">
<label for="" class="object-label">{{ l.slideshow_content_form_label_object }}</label> <label for="" class="object-label">{{ l.slideshow_content_form_label_object }}</label>
<div class="widget"> <div class="widget">
<div class="object-holder hidden">
<input type="text" name="object" data-input-type="text" class="content-object-input" /> <input type="text" name="object" data-input-type="text" class="content-object-input" />
</div>
<label for="content-add-object-input-upload" class="btn-upload hidden"> <label for="content-add-object-input-upload" class="btn-upload hidden object-holder">
<input type="file" name="object" data-input-type="upload" class="content-object-input" disabled="disabled" id="content-add-object-input-upload"/> <input type="file" name="object" data-input-type="upload" class="content-object-input" disabled="disabled" id="content-add-object-input-upload" />
<span class="btn btn-neutral normal"> <span class="btn btn-neutral normal">
<i class="fa fa-file-import"></i> <i class="fa fa-file-import"></i>
{{ l.slideshow_content_form_button_upload }} {{ l.slideshow_content_form_button_upload }}
</span> </span>
<input type="text" value="{{ l.slideshow_content_form_button_upload_choosen }}" disabled="disabled" class="disabled" /> <input type="text" value="{{ l.slideshow_content_form_button_upload_choosen }}" disabled="disabled" class="disabled" />
</label> </label>
<div class="object-holder hidden">
<select name="storage" disabled="disabled">
{% for key, value in external_storages.items() %}
<option value="{{ key }}">{{ value }}</option>
{% endfor %}
</select>
<div class="form-group">
<label for="content-add-object-input-storage-target-path">{{ l.enum_content_type_external_storage_target_path_label }}</label>
<input type="text" name="object" data-input-type="storage" class="content-object-input" disabled="disabled" id="content-add-object-input-storage-target-path" />
</div>
</div>
</div> </div>
</div> </div>