diff --git a/client/package.json b/client/package.json index db211b52..8347257b 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.6.1", + "version": "0.6.2", "private": true, "scripts": { "build": "vite build", diff --git a/client/src/vue_components/show/config/cues/CueEditor.vue b/client/src/vue_components/show/config/cues/CueEditor.vue index 043642a1..d55560f1 100644 --- a/client/src/vue_components/show/config/cues/CueEditor.vue +++ b/client/src/vue_components/show/config/cues/CueEditor.vue @@ -4,7 +4,14 @@ fluid > - + + + Go to Page + + + + + + + + This is a required field, and must be greater than 0. + + + + @@ -95,6 +136,8 @@ import log from 'loglevel'; import { makeURL } from '@/js/utils'; import ScriptLineCueEditor from '@/vue_components/show/config/cues/ScriptLineCueEditor.vue'; +import { minValue, required } from 'vuelidate/lib/validators'; +import { notNull, notNullAndGreaterThanZero } from '@/js/customValidators'; export default { name: 'CueEditor', @@ -115,8 +158,22 @@ export default { savingInProgress: false, saveError: false, currentMaxPage: 1, + changingPage: false, + pageInputFormState: { + pageNo: 1, + }, }; }, + validations: { + pageInputFormState: { + pageNo: { + required, + notNull, + notNullAndGreaterThanZero, + minValue: minValue(1), + }, + }, + }, async beforeMount() { // Config status await this.GET_SCRIPT_CONFIG_STATUS(); @@ -133,8 +190,12 @@ export default { await this.getMaxScriptPage(); // Initialisation of page data - await this.LOAD_SCRIPT_PAGE(this.currentEditPage); - await this.LOAD_SCRIPT_PAGE(this.currentEditPage + 1); + // Initialisation of page data + const storedPage = localStorage.getItem('cueEditPage'); + if (storedPage != null) { + this.currentEditPage = parseInt(storedPage, 10); + } + await this.goToPageInner(this.currentEditPage); }, methods: { async getMaxScriptPage() { @@ -179,6 +240,23 @@ export default { } return []; }, + validatePageState(name) { + const { $dirty, $error } = this.$v.pageInputFormState[name]; + return $dirty ? !$error : null; + }, + async goToPage() { + this.changingPage = true; + await this.goToPageInner(this.pageInputFormState.pageNo); + this.changingPage = false; + }, + async goToPageInner(pageNo) { + if (pageNo > 1) { + await this.LOAD_SCRIPT_PAGE(parseInt(pageNo, 10) - 1); + } + await this.LOAD_SCRIPT_PAGE(pageNo); + this.currentEditPage = pageNo; + await this.LOAD_SCRIPT_PAGE(parseInt(pageNo, 10) + 1); + }, ...mapMutations(['REMOVE_PAGE', 'ADD_BLANK_LINE', 'SET_LINE']), ...mapActions(['GET_SCENE_LIST', 'GET_ACT_LIST', 'GET_CHARACTER_LIST', 'GET_CHARACTER_GROUP_LIST', 'LOAD_SCRIPT_PAGE', 'ADD_BLANK_PAGE', 'GET_SCRIPT_CONFIG_STATUS', @@ -199,5 +277,10 @@ export default { 'CHARACTER_GROUP_LIST', 'CAN_REQUEST_EDIT', 'CURRENT_EDITOR', 'INTERNAL_UUID', 'GET_SCRIPT_PAGE', 'DEBUG_MODE_ENABLED', 'CUE_TYPES', 'SCRIPT_CUES', 'SCRIPT_CUTS']), }, + watch: { + currentEditPage(val) { + localStorage.setItem('cueEditPage', val); + }, + }, }; diff --git a/client/src/vue_components/show/config/script/ScriptEditor.vue b/client/src/vue_components/show/config/script/ScriptEditor.vue index 8300d67a..0bc58760 100644 --- a/client/src/vue_components/show/config/script/ScriptEditor.vue +++ b/client/src/vue_components/show/config/script/ScriptEditor.vue @@ -189,7 +189,7 @@ :no-close-on-esc="changingPage" @ok="goToPage" > - + HTTPServer: + self.http_server = super().listen(port, address, family=family, backlog=backlog, flags=flags, + reuse_port=reuse_port, **kwargs) + return self.http_server + async def configure(self): await self._configure_logging() + async def post_configure(self): + await self.plugin_manager.start_default() + + async def shutdown(self): + get_logger().warning('DigiScript shutting down - goodbye!') + await self.plugin_manager.shutdown() + async def _configure_logging(self): get_logger().info('Reconfiguring logging!') diff --git a/server/digi_server/plugin_manager.py b/server/digi_server/plugin_manager.py new file mode 100644 index 00000000..620adde2 --- /dev/null +++ b/server/digi_server/plugin_manager.py @@ -0,0 +1,39 @@ +import importlib +from typing import List, TYPE_CHECKING + +from digi_server.logger import get_logger +from utils.pkg_utils import find_end_modules +from utils.server_plugin import BasePlugin, registered_plugins + +if TYPE_CHECKING: + from digi_server.app_server import DigiScriptServer + + +class PluginManager: + def __init__(self, application: 'DigiScriptServer'): + self.application = application + self.IMPORTED_PLUGINS = {} + self.RUNNING_PLUGINS: List[BasePlugin] = [] + + def import_all_plugins(self): + plugins = find_end_modules('.', prefix='server_plugins') + for plugin in plugins: + if plugin != __name__: + get_logger().debug(f'Importing plugin module {plugin}') + mod = importlib.import_module(plugin) + self.IMPORTED_PLUGINS[plugin] = mod + + async def start_plugin(self, name): + get_logger().info(f'Starting plugin: {name}') + plugin_obj = registered_plugins[name](self.application) + await plugin_obj.start() + self.RUNNING_PLUGINS.append(plugin_obj) + + async def start_default(self): + for plugin_name in registered_plugins: + if registered_plugins[plugin_name]._default_plugin: + await self.start_plugin(plugin_name) + + async def shutdown(self): + for plugin in self.RUNNING_PLUGINS: + await plugin.stop() diff --git a/server/main.py b/server/main.py index 1fa4c7f7..03172db1 100755 --- a/server/main.py +++ b/server/main.py @@ -29,8 +29,12 @@ async def main(): get_logger().info(f'Listening on port: {options.port}') if options.debug: get_logger().warning('Running in debug mode') + await app.post_configure() + await asyncio.Event().wait() + await app.shutdown() + if __name__ == '__main__': asyncio.run(main()) diff --git a/server/requirements.txt b/server/requirements.txt index a5477245..6e3393b8 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -7,3 +7,4 @@ marshmallow-sqlalchemy==0.29.0 tornado-prometheus==0.1.2 bcrypt==4.0.1 anytree==2.8.0 +python-osc==1.8.1 diff --git a/server/server_plugins/__init__.py b/server/server_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/server_plugins/osc_server.py b/server/server_plugins/osc_server.py new file mode 100644 index 00000000..430faff9 --- /dev/null +++ b/server/server_plugins/osc_server.py @@ -0,0 +1,40 @@ +import asyncio +from asyncio import DatagramProtocol +from asyncio.transports import BaseTransport +from typing import List, Tuple, Any + +from pythonosc.dispatcher import Dispatcher +from pythonosc.osc_server import AsyncIOOSCUDPServer + +from digi_server.logger import get_logger +from utils.server_plugin import plugin, BasePlugin + + +def default_osc_handler(address: str, *osc_arguments: List[Any]): + get_logger().debug(f'OSC message received with no handler. Address: {address}. ' + f'Arguments: {osc_arguments}') + + +@plugin('OSC Server', default=True) +class OSCServerPlugin(BasePlugin): + def __init__(self, application: 'DigiScriptServer'): + self.dispatcher: Dispatcher = Dispatcher() + self.dispatcher.set_default_handler(default_osc_handler) + self.servers: List[AsyncIOOSCUDPServer] = [] + self.transport_pairs: List[Tuple[BaseTransport, DatagramProtocol]] = [] + + super().__init__(application) + + async def start(self): + for fd, socket in self.application.http_server._sockets.items(): + address = socket.getsockname() + server = AsyncIOOSCUDPServer((address[0], address[1] + 1), + self.dispatcher, + asyncio.get_event_loop()) + transport, protocol = await server.create_serve_endpoint() + self.servers.append(server) + self.transport_pairs.append((transport, protocol)) + + async def stop(self): + for transport_pair in self.transport_pairs: + transport_pair[0].close() diff --git a/server/utils/server_plugin.py b/server/utils/server_plugin.py new file mode 100644 index 00000000..7b2104ae --- /dev/null +++ b/server/utils/server_plugin.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from digi_server.app_server import DigiScriptServer + +registered_plugins = {} + + +def plugin(name, default=False): + def decorator(cls): + if not issubclass(cls, BasePlugin): + raise ValueError('Class must inherit from BasePlugin.') + + if name in registered_plugins: + raise ValueError('Name must be unique.') + + cls._default_plugin = default + registered_plugins[name] = cls + + return cls + + return decorator + + +class BasePlugin: + + def __init__(self, application: 'DigiScriptServer'): + self.application = application + + async def start(self): + raise NotImplementedError + + async def stop(self): + raise NotImplementedError + +