change logic
This commit is contained in:
parent
46f06a6927
commit
f8b6da0d4c
@ -1,3 +1,6 @@
|
||||
DEMO=false
|
||||
DEBUG=false
|
||||
PORT=5000
|
||||
PORT_HTTP_EXTERNAL_STORAGE=5001
|
||||
SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
FROM python:3.9.17-alpine3.17
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev sqlite-dev build-base linux-headers
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev sqlite-dev exfat-fuse ntfs-3g build-base linux-headers
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install -r requirements.txt && apk del .build-deps gcc musl-dev sqlite-dev build-base linux-headers
|
||||
RUN pip install -r requirements.txt && apk del .build-deps gcc musl-dev sqlite-dev exfat-fuse ntfs-3g build-base linux-headers
|
||||
|
||||
ENTRYPOINT ["python", "/app/obscreen.py"]
|
||||
|
||||
@ -76,7 +76,7 @@ If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
||||
|
||||
|
||||
<details closed>
|
||||
<summary><h5>Videos aren't playing why ?</h3></summary>
|
||||
<summary><h3>Videos aren't playing why ?</h3></summary>
|
||||
|
||||
This is "normal" behavior. Videos do not play automatically in Chrome because it requires user interaction with the page (a simple click inside the webpage is enough). If you open the console, you'll see the error: [Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first...](https://goo.gl/xX8pDD)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -11,18 +11,14 @@ jQuery(document).ready(function ($) {
|
||||
$form.find('.content-object-input').each(function() {
|
||||
const $input = $(this);
|
||||
const active = $input.attr('data-input-type') === inputType;
|
||||
const $holder = $input.parents('.object-holder:eq(0)');
|
||||
const $holder = $input.parents('.from-group-condition: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;
|
||||
const color = optionAttributes['data-color'].value;
|
||||
$form.find('.object-label').html(optionAttributes['data-object-label'].value);
|
||||
$form.find('.object-label:visible').html(optionAttributes['data-object-label'].value);
|
||||
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
||||
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
||||
$form.find('button[type=submit]').attr('class', 'btn ' + ('btn-' + color));
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
|
||||
.modals-outer {
|
||||
min-width: 464px;
|
||||
max-width: 464px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
@ -25,6 +25,14 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.from-group-condition {
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -35,32 +43,10 @@ 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;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
@ -66,7 +66,7 @@ docker compose up --detach --pull=always
|
||||
```bash
|
||||
# Install system dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python3-pip python3-venv libsqlite3-dev
|
||||
sudo apt-get install -y git python3-pip python3-venv libsqlite3-dev exfat-fuse ntfs-3g
|
||||
|
||||
# Get files
|
||||
cd ~ && git clone https://github.com/jr-k/obscreen.git && cd obscreen
|
||||
|
||||
@ -284,8 +284,8 @@
|
||||
"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_external_storage_object_label": "Specify an existing directory path with displayable files within your removeable device",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Path relative to a removeable device",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Picture",
|
||||
|
||||
@ -285,8 +285,8 @@
|
||||
"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_external_storage_object_label": "Especifique una ruta de directorio existente con archivos visualizables dentro de su dispositivo extraíble",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Ruta relativa a un dispositivo extraíble",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Imagen",
|
||||
|
||||
@ -286,8 +286,8 @@
|
||||
"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_external_storage_object_label": "Spécifiez un chemin de répertoire existant avec des fichiers affichables dans votre périphérique amovible",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Chemin relatif à un périphérique amovible",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Vidéo",
|
||||
"enum_content_type_picture": "Image",
|
||||
|
||||
@ -285,8 +285,8 @@
|
||||
"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_external_storage_object_label": "Specifica un percorso di directory esistente con file visualizzabili all'interno del tuo dispositivo rimovibile",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Percorso relativo ad un dispositivo rimovibile",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Immagine",
|
||||
|
||||
@ -6,6 +6,7 @@ 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
|
||||
|
||||
@ -18,6 +19,7 @@ class Application:
|
||||
self._model_store = ModelStore(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)
|
||||
@ -29,6 +31,7 @@ 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:
|
||||
@ -59,3 +62,7 @@ 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
|
||||
|
||||
@ -9,6 +9,7 @@ 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
|
||||
|
||||
@ -48,8 +49,6 @@ 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()
|
||||
print(external_storages[0] if len(external_storages) > 0 else 'none')
|
||||
|
||||
return render_template(
|
||||
'slideshow/contents/list.jinja.html',
|
||||
@ -60,12 +59,7 @@ class ContentController(ObController):
|
||||
working_folder=working_folder,
|
||||
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},
|
||||
enum_folder_entity=FolderEntity
|
||||
)
|
||||
|
||||
def slideshow_content_add(self):
|
||||
@ -76,13 +70,6 @@ class ContentController(ObController):
|
||||
|
||||
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),
|
||||
@ -253,21 +240,7 @@ class ContentController(ObController):
|
||||
if not content:
|
||||
return abort(404)
|
||||
|
||||
var_external_url = self._model_store.variable().get_one_by_name('external_url')
|
||||
location = content.location
|
||||
|
||||
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)
|
||||
elif content.has_file():
|
||||
location = "/{}".format(content.location)
|
||||
elif content.type == ContentType.URL:
|
||||
location = 'http://' + content.location if not content.location.startswith('http') else content.location
|
||||
|
||||
return redirect(location)
|
||||
return redirect(self._model_store.content().resolve_content_location(content))
|
||||
|
||||
def slideshow_content_delete_bulk_explr(self):
|
||||
working_folder_path, working_folder = self.get_working_folder()
|
||||
|
||||
@ -139,12 +139,20 @@ class PlayerController(ObController):
|
||||
|
||||
if slide['type'] == ContentType.EXTERNAL_STORAGE.value:
|
||||
mount_point_dir = Path(slide['location'])
|
||||
if True: #mount_point_dir.is_dir():
|
||||
for file in [Path('/Volumes/ESD-USB/obscreen/test/autumn.jpg'), Path('/Volumes/ESD-USB/obscreen/test/soundonly.jpg')]: #mount_point_dir.iterdir():
|
||||
# if file.is_file() and not file.stem.startswith('.'):
|
||||
logging.info(mount_point_dir)
|
||||
if mount_point_dir.is_dir():
|
||||
logging.info('exist !')
|
||||
for file in mount_point_dir.iterdir():
|
||||
logging.info(file)
|
||||
if file.is_file() and not file.stem.startswith('.'):
|
||||
logging.info("f is ok !")
|
||||
slide['type'] = ContentType.guess_content_type_file(str(file.resolve())).value
|
||||
slide['location'] = "file://{}".format(str(file.resolve()))
|
||||
slide['location'] = "{}/{}".format(
|
||||
self._model_store.content().resolve_content_location(content),
|
||||
file.name
|
||||
)
|
||||
slide['name'] = file.stem
|
||||
logging.info(slide.to_json())
|
||||
self._feed_playlist(playlist_loop, playlist_notifications, slide)
|
||||
else:
|
||||
self._feed_playlist(playlist_loop, playlist_notifications, slide)
|
||||
|
||||
@ -42,3 +42,6 @@ class ObController(abc.ABC):
|
||||
|
||||
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
|
||||
|
||||
@ -15,6 +15,8 @@ class ConfigManager:
|
||||
self._CONFIG = {
|
||||
'version': None,
|
||||
'demo': False,
|
||||
'port_http_external_storage': None,
|
||||
'bind_http_external_storage': '0.0.0.0',
|
||||
'port': self.DEFAULT_PORT,
|
||||
'bind': '0.0.0.0',
|
||||
'debug': False,
|
||||
@ -46,6 +48,8 @@ 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 directory')
|
||||
parser.add_argument('--bind-http-external-storage', '-px', default=self._CONFIG['bind_http_external_storage'], help='Bind address of http server serving external storage directory')
|
||||
parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number')
|
||||
|
||||
return parser.parse_args()
|
||||
@ -61,6 +65,10 @@ 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.log_file:
|
||||
self._CONFIG['log_file'] = args.log_file
|
||||
if args.secret_key:
|
||||
|
||||
@ -216,3 +216,24 @@ class ContentManager(ModelManager):
|
||||
|
||||
def count_contents_for_folder(self, folder_id: int) -> int:
|
||||
return len(self.get_contents(folder_id=folder_id))
|
||||
|
||||
def resolve_content_location(self, content: Content) -> str:
|
||||
var_external_url = self._model_store.variable().get_one_by_name('external_url').as_string().strip().strip('/')
|
||||
location = content.location
|
||||
|
||||
if content.type == ContentType.EXTERNAL_STORAGE:
|
||||
port_ex_st = self.get_external_storage_server().get_port()
|
||||
if len(var_external_url) > 0:
|
||||
location = "{}:{}/{}".format(var_external_url, port_ex_st, content.location.strip('/'))
|
||||
else:
|
||||
location = "http://localhost:{}/{}".format(port_ex_st, content.location.strip('/'))
|
||||
elif 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.value, content.location)
|
||||
elif content.has_file():
|
||||
location = "/{}".format(content.location)
|
||||
elif content.type == ContentType.URL:
|
||||
location = 'http://' + content.location if not content.location.startswith('http') else content.location
|
||||
|
||||
return location
|
||||
@ -1,208 +0,0 @@
|
||||
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
|
||||
|
||||
@ -19,7 +19,7 @@ class ContentInputType(Enum):
|
||||
elif value == ContentInputType.TEXT:
|
||||
return True
|
||||
elif value == ContentInputType.STORAGE:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ContentType(Enum):
|
||||
|
||||
103
src/service/ExternalStorageServer.py
Normal file
103
src/service/ExternalStorageServer.py
Normal file
@ -0,0 +1,103 @@
|
||||
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:
|
||||
|
||||
MOUNTPOINT = Path("var/run/storage")
|
||||
|
||||
def __init__(self, kernel, model_store: ModelStore):
|
||||
self._kernel = kernel
|
||||
self._model_store = model_store
|
||||
|
||||
def get_directory(self):
|
||||
return Path(self._kernel.get_project_dir(), self.MOUNTPOINT)
|
||||
|
||||
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 get_port(self) -> Optional[int]:
|
||||
port = self._model_store.config().map().get('port_http_external_storage')
|
||||
return int(port) if port else None
|
||||
|
||||
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 dir://{}:{}".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()}
|
||||
@ -12,7 +12,6 @@ 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:
|
||||
@ -40,7 +39,6 @@ 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:
|
||||
@ -49,9 +47,6 @@ 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
|
||||
|
||||
@ -86,7 +81,6 @@ 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)
|
||||
|
||||
25
system/10-obscreen-media-automount.rules
Normal file
25
system/10-obscreen-media-automount.rules
Normal file
@ -0,0 +1,25 @@
|
||||
# normally we start at sdb to ignore the system hard drive but we're in rpi, sdcard is the system hard drive
|
||||
KERNEL!="sd[a-z]*", GOTO="obscreen_automount_end"
|
||||
ACTION=="add", PROGRAM!="/sbin/blkid %N", GOTO="obscreen_automount_end"
|
||||
|
||||
# import some useful filesystem info as variables
|
||||
IMPORT{program}="/sbin/blkid -o udev -p %N"
|
||||
|
||||
# set mountpoint directory output
|
||||
ENV{dir_name}="/home/pi/obscreen/var/run/storage"
|
||||
ACTION=="add", RUN+="/bin/mkdir -p '%E{dir_name}'"
|
||||
|
||||
# global mount options
|
||||
ACTION=="add", ENV{mount_options}="relatime"
|
||||
|
||||
# filesystem-specific mount options (777/666 dir/file perms for ntfs/vfat)
|
||||
ACTION=="add", ENV{ID_FS_TYPE}=="vfat|ntfs", ENV{mount_options}="$env{mount_options},gid=100,dmask=000,fmask=111,utf8"
|
||||
|
||||
# automount all other filesystems
|
||||
ACTION=="add", ENV{ID_FS_TYPE}!="ntfs", RUN+="/usr/bin/systemd-mount --no-block --automount=yes --collect -o %E{mount_options} /dev/%k '%E{dir_name}'"
|
||||
|
||||
# clean up after device removal
|
||||
ACTION=="remove", ENV{dir_name}!="", RUN+="/usr/bin/systemd-umount '%E{dir_name}'"
|
||||
|
||||
# exit
|
||||
LABEL="obscreen_automount_end"
|
||||
@ -59,11 +59,10 @@ sleep 3
|
||||
|
||||
# Update and install necessary packages
|
||||
apt update
|
||||
apt install -y xinit xserver-xorg chromium-browser unclutter pulseaudio
|
||||
apt install -y xinit xserver-xorg chromium-browser unclutter pulseaudio exfat-fuse ntfs-3g
|
||||
|
||||
# Add user to tty and video groups
|
||||
usermod -aG tty $OWNER
|
||||
usermod -aG video $OWNER
|
||||
# Add user to tty, video and plugdev groups
|
||||
usermod -aG tty,video,plugdev $OWNER
|
||||
|
||||
# Configure Xwrapper
|
||||
touch /etc/X11/Xwrapper.config
|
||||
@ -73,6 +72,12 @@ grep -qxF "needs_root_rights=yes" /etc/X11/Xwrapper.config || echo "needs_root_r
|
||||
# Create the systemd service to start Chromium in kiosk mode
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/obscreen-player.service | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | tee /etc/systemd/system/obscreen-player.service
|
||||
|
||||
# Configure external storage automount
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/10-obscreen-media-automount.rules | sed "s#/home/pi#$WORKING_DIR#g" | tee /etc/udev/rules.d/10-obscreen-media-automount.rules
|
||||
udevadm control --reload-rules
|
||||
systemctl restart udev
|
||||
udevadm trigger
|
||||
|
||||
# Reload systemd, enable and start the service
|
||||
systemctl daemon-reload
|
||||
systemctl enable obscreen-player.service
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
|
||||
{% 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 }}'
|
||||
|
||||
@ -27,32 +27,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group object-input">
|
||||
<label for="" class="object-label">{{ l.slideshow_content_form_label_object }}</label>
|
||||
<div class="from-group-condition hidden">
|
||||
<div class="form-group">
|
||||
<label for="" class="object-label"></label>
|
||||
<div class="widget">
|
||||
<div class="object-holder hidden">
|
||||
<input type="text" name="object" data-input-type="text" class="content-object-input"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="content-add-object-input-upload" class="btn-upload hidden object-holder">
|
||||
<div class="from-group-condition hidden">
|
||||
<div class="form-group">
|
||||
<label for="" class="object-label"></label>
|
||||
<label class="btn-upload" for="content-add-object-input-upload">
|
||||
<div class="widget">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="from-group-condition hidden">
|
||||
<div class="form-group">
|
||||
<label for="" class="object-label"></label>
|
||||
<div class="widget">
|
||||
<input type="text" name="object" data-input-type="storage" class="content-object-input" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user