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
venv/
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');
$form.find('.content-object-input').each(function() {
const active = $(this).attr('data-input-type') === inputType;
if ($(this).is('input[type=file]')) {
$(this).prop('disabled', !active).prop('required', active);
$(this).parents('label:eq(0)').toggleClass('hidden', !active);
} else {
$(this).prop('disabled', !active).prop('required', active).toggleClass('hidden', !active);
}
const $input = $(this);
const active = $input.attr('data-input-type') === inputType;
const $holder = $input.parents('.object-holder:eq(0)');
$holder.find('input, select, textarea').prop('disabled', !active).prop('required', active).toggleClass('hidden', !active);
$holder.toggleClass('hidden', !active);
console.log(active)
console.log($input)
if (active)
console.log($holder)
});
const optionAttributes = $selectedOption.get(0).attributes;

View File

@ -35,6 +35,28 @@ form {
flex: 1;
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 {
flex: 1;

View File

@ -226,6 +226,7 @@
"basic_month_10": "October",
"basic_month_11": "November",
"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_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect",
"common_pick_element": "Pick an element",
@ -282,6 +283,9 @@
"enum_application_language_french": "French",
"enum_application_language_italian": "Italian",
"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_video": "Video",
"enum_content_type_picture": "Picture",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Octubre",
"basic_month_11": "Noviembre",
"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_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto",
"common_pick_element": "Elige un elemento",
@ -283,6 +284,9 @@
"enum_application_language_french": "Francés",
"enum_application_language_italian": "Italiano",
"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_video": "Video",
"enum_content_type_picture": "Imagen",

View File

@ -228,6 +228,7 @@
"basic_month_10": "Octobre",
"basic_month_11": "Novembre",
"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_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",
@ -284,6 +285,9 @@
"enum_application_language_french": "Français",
"enum_application_language_italian": "Italien",
"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_video": "Vidéo",
"enum_content_type_picture": "Image",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Ottobre",
"basic_month_11": "Novembre",
"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_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto",
"common_pick_element": "Scegli un elemento",
@ -283,6 +284,9 @@
"enum_application_language_french": "Francese",
"enum_application_language_italian": "Italiano",
"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_video": "Video",
"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')
working_folder_path, working_folder = self.get_working_folder()
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(
'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),
enum_content_type=ContentType,
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):
@ -67,12 +73,21 @@ class ContentController(ObController):
"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(
name=request.form['name'],
type=str_to_enum(request.form['type'], ContentType),
request_files=request.files,
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
)
@ -87,7 +102,7 @@ class ContentController(ObController):
for key in request.files:
files = request.files.getlist(key)
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]
if type:
@ -240,7 +255,9 @@ class ContentController(ObController):
var_external_url = self._model_store.variable().get_one_by_name('external_url')
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)
elif len(var_external_url.as_string().strip()) > 0 and content.has_file():
location = "{}/{}".format(var_external_url.value, content.location)

View File

@ -2,10 +2,12 @@ import json
import logging
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 pathlib import Path
from src.model.entity.Slide import Slide
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
@ -123,39 +125,28 @@ class PlayerController(ObController):
playlist_notifications = []
for slide in slides:
if slide['content_id']:
if int(slide['content_id']) not in contents:
continue
content = contents[int(slide['content_id'])].to_dict()
slide['name'] = content['name']
slide['location'] = content['location']
slide['type'] = content['type']
else:
if not slide['content_id']:
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'])
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 int(slide['content_id']) not in contents:
continue
if slide['is_notification']:
if has_valid_start_date:
playlist_notifications.append(slide)
else:
logging.warn('Slide \'{}\' is a notification but start date is invalid'.format(slide['name']))
content = contents[int(slide['content_id'])].to_dict()
slide['name'] = content['name']
slide['location'] = content['location']
slide['type'] = content['type']
if slide['type'] == ContentType.EXTERNAL_STORAGE.value:
mount_point_dir = Path(slide['location'])
if mount_point_dir.is_dir():
for file in mount_point_dir.iterdir():
if file.is_file() and not file.stem.startswith('.'):
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:
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)
self._feed_playlist(playlist_loop, playlist_notifications, slide)
playlists = {
'playlist_id': playlist.id if playlist else None,
@ -167,3 +158,24 @@ class PlayerController(ObController):
}
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 == '':
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:
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'
TEXT = 'text'
STORAGE = 'storage'
@staticmethod
def is_editable(value: Enum) -> bool:
@ -17,18 +18,21 @@ class ContentInputType(Enum):
return False
elif value == ContentInputType.TEXT:
return True
elif value == ContentInputType.STORAGE:
return False
class ContentType(Enum):
EXTERNAL_STORAGE = 'external_storage'
PICTURE = 'picture'
URL = 'url'
YOUTUBE = 'youtube'
VIDEO = 'video'
@staticmethod
def guess_content_type_file(file):
mime_type, _ = mimetypes.guess_type(file.filename)
def guess_content_type_file(filename: str):
mime_type, _ = mimetypes.guess_type(filename)
if mime_type in [
'image/gif',
@ -55,6 +59,8 @@ class ContentType(Enum):
return ContentInputType.TEXT
elif value == ContentType.URL:
return ContentInputType.TEXT
elif value == ContentType.EXTERNAL_STORAGE:
return ContentInputType.STORAGE
@staticmethod
def get_fa_icon(value: Union[Enum, str]) -> str:
@ -69,6 +75,8 @@ class ContentType(Enum):
return 'fa-brands fa-youtube'
elif value == ContentType.URL:
return 'fa-link'
elif value == ContentType.EXTERNAL_STORAGE:
return 'fa-brands fa-usb'
return 'fa-file'
@ -85,5 +93,7 @@ class ContentType(Enum):
return 'youtube'
elif value == ContentType.URL:
return 'danger'
elif value == ContentType.EXTERNAL_STORAGE:
return 'other'
return 'neutral'

View File

@ -12,6 +12,7 @@ from src.manager.LangManager import LangManager
from src.manager.DatabaseManager import DatabaseManager
from src.manager.ConfigManager import ConfigManager
from src.manager.LoggingManager import LoggingManager
from src.manager.ExternalStorageManager import ExternalStorageManager
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._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._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()
def logging(self) -> LoggingManager:
@ -47,6 +49,9 @@ class ModelStore:
def config(self) -> ConfigManager:
return self._config_manager
def external_storage(self) -> ExternalStorageManager:
return self._external_storage_manager
def variable(self) -> VariableManager:
return self._variable_manager
@ -81,6 +86,7 @@ class ModelStore:
return self._get_plugins()
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._folder_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) {
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() {});
};

View File

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

View File

@ -30,16 +30,30 @@
<div class="form-group object-input">
<label for="" class="object-label">{{ l.slideshow_content_form_label_object }}</label>
<div class="widget">
<input type="text" name="object" data-input-type="text" class="content-object-input" />
<div class="object-holder hidden">
<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">
<input type="file" name="object" data-input-type="upload" class="content-object-input" disabled="disabled" id="content-add-object-input-upload"/>
<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" />
<span class="btn btn-neutral normal">
<i class="fa fa-file-import"></i>
{{ l.slideshow_content_form_button_upload }}
</span>
<input type="text" value="{{ l.slideshow_content_form_button_upload_choosen }}" disabled="disabled" class="disabled" />
</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>