cuemsutils 0.1.0rc11__tar.gz → 0.1.0rc15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/PKG-INFO +2 -2
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/__init__.py +2 -1
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/AudioCue.py +1 -1
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/FadeCue.py +17 -2
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/MediaCue.py +90 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/VideoCue.py +41 -15
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/log.py +57 -6
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/HubServices.py +23 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/Parsers.py +6 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/XmlBuilder.py +29 -2
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/script.xsd +37 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/.gitignore +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/LICENSE +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/README.md +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/create_script.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/ActionCue.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/Cue.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/CueList.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/CueOutput.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/CuemsScript.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/DmxCue.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/FadeProfile.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/cues/__init__.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/helpers.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/timeoutloop.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/CTimecode.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/CTimecodeTimer.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/CommunicatorServices.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/ConfigBase.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/ConfigManager.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/CopyMoveVersioned.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/FadeCalculator.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/SignalEngine.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/StringSanitizer.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/Uuid.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/tools/__init__.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/CMLCuemsConverter.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/Settings.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/XmlReaderWriter.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/__init__.py +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/network_map.xsd +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/outputs.xsd +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/project_mappings.xsd +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/project_settings.xsd +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/cuemsutils/xml/schemas/settings.xsd +0 -0
- {cuemsutils-0.1.0rc11 → cuemsutils-0.1.0rc15}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cuemsutils
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0rc15
|
|
4
4
|
Summary: Reusable classes and methods for CueMS system
|
|
5
5
|
Project-URL: Documentation, https://stagesoft.github.io/cuems-utils/
|
|
6
6
|
Project-URL: Issues, https://github.com/stagesoft/cuems-utils/issues
|
|
@@ -6,7 +6,7 @@ from ..helpers import ensure_items
|
|
|
6
6
|
from ..log import logged, Logger
|
|
7
7
|
|
|
8
8
|
REQ_ITEMS = {
|
|
9
|
-
'master_vol':
|
|
9
|
+
'master_vol': 100 # Default to full volume — 0-100 percent scale (cms:PercentType)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
class AudioCue(MediaCue):
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Stagelab Coop SCCL
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# SPDX-FileContributor: Ion Reguera <ion@stagelab.coop>
|
|
4
|
+
|
|
1
5
|
from enum import Enum
|
|
2
6
|
|
|
3
7
|
from ..helpers import ensure_items, format_timecode
|
|
@@ -8,12 +12,22 @@ _ZERO_TC = CTimecode('00:00:00.000')
|
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
class FadeCurveType(Enum):
|
|
11
|
-
"""Enumeration of supported fade curve shapes.
|
|
15
|
+
"""Enumeration of supported fade curve shapes.
|
|
16
|
+
|
|
17
|
+
``linear``, ``sigmoid``, ``ease_in`` and ``ease_out`` are the shapes
|
|
18
|
+
gradient-motiond's CurveFactory implements; the editor UI submits these.
|
|
19
|
+
``exponential`` and ``logarithmic`` are legacy names kept so projects
|
|
20
|
+
authored by older editors still load — CurveFactory does NOT implement
|
|
21
|
+
them, and the frontend maps them onto ``ease_in``/``ease_out`` (the same
|
|
22
|
+
accelerating/decelerating shapes) on load, healing them on next save.
|
|
23
|
+
"""
|
|
12
24
|
|
|
13
25
|
linear = 'linear'
|
|
14
26
|
exponential = 'exponential'
|
|
15
27
|
logarithmic = 'logarithmic'
|
|
16
28
|
sigmoid = 'sigmoid'
|
|
29
|
+
ease_in = 'ease_in'
|
|
30
|
+
ease_out = 'ease_out'
|
|
17
31
|
|
|
18
32
|
def __str__(self):
|
|
19
33
|
return self.value
|
|
@@ -125,7 +139,8 @@ class FadeCue(ActionCue):
|
|
|
125
139
|
|
|
126
140
|
Args:
|
|
127
141
|
curve_type (FadeCurveType | str): A FadeCurveType member or its
|
|
128
|
-
string value ('linear', '
|
|
142
|
+
string value ('linear', 'sigmoid', 'ease_in', 'ease_out', or
|
|
143
|
+
the legacy 'exponential'/'logarithmic').
|
|
129
144
|
|
|
130
145
|
Raises:
|
|
131
146
|
ValueError: If curve_type is not a recognised value.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Stagelab Coop SCCL
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# SPDX-FileContributor: Ion Reguera <ion@stagelab.coop>
|
|
1
4
|
from typing import Tuple
|
|
2
5
|
|
|
3
6
|
from .Cue import Cue
|
|
@@ -237,6 +240,93 @@ class Media(CuemsDict):
|
|
|
237
240
|
|
|
238
241
|
regions: list[Region] = property(get_regions, set_regions)
|
|
239
242
|
|
|
243
|
+
# --- Stored size of the media file (869fat84r) --------------------------
|
|
244
|
+
#
|
|
245
|
+
# ``pixel_width`` / ``pixel_height`` are the original size of the file's
|
|
246
|
+
# first video stream, as ffprobe reports it, and ``file_size`` is the
|
|
247
|
+
# file's size in bytes when they were measured. ``file_md5`` (below)
|
|
248
|
+
# identifies the file itself. The editor stores them at
|
|
249
|
+
# upload so the engine does not have to probe the file when it arms a
|
|
250
|
+
# cue; ``file_size`` lets the engine notice a file replaced under the
|
|
251
|
+
# same name. All three are optional: absent means unknown.
|
|
252
|
+
#
|
|
253
|
+
# Parsing a project never calls these setters (the parsers assign keys
|
|
254
|
+
# raw), so the schema and MediaXmlBuilder, not these, guard what is read
|
|
255
|
+
# from and written to a file.
|
|
256
|
+
|
|
257
|
+
def _get_optional(self, key):
|
|
258
|
+
return self.get(key)
|
|
259
|
+
|
|
260
|
+
def _set_positive_int(self, key, value):
|
|
261
|
+
"""Store *value* as a positive ``int``; ``None`` removes the key.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: for anything that is not a positive integer or a
|
|
265
|
+
string of digits (``bool`` included: it is an ``int``).
|
|
266
|
+
"""
|
|
267
|
+
if value is None:
|
|
268
|
+
self.pop(key, None)
|
|
269
|
+
return
|
|
270
|
+
if isinstance(value, bool):
|
|
271
|
+
raise ValueError(f"Media {key} must be a positive integer, not {value!r}")
|
|
272
|
+
if isinstance(value, str) and value.isdigit():
|
|
273
|
+
value = int(value)
|
|
274
|
+
if not isinstance(value, int) or value <= 0:
|
|
275
|
+
raise ValueError(f"Media {key} must be a positive integer, not {value!r}")
|
|
276
|
+
super().__setitem__(key, value)
|
|
277
|
+
|
|
278
|
+
def get_pixel_width(self):
|
|
279
|
+
"""Width in pixels of the media's first video stream, or ``None``."""
|
|
280
|
+
return self._get_optional('pixel_width')
|
|
281
|
+
|
|
282
|
+
def set_pixel_width(self, pixel_width):
|
|
283
|
+
"""Set the width in pixels; ``None`` removes it."""
|
|
284
|
+
self._set_positive_int('pixel_width', pixel_width)
|
|
285
|
+
|
|
286
|
+
pixel_width = property(get_pixel_width, set_pixel_width)
|
|
287
|
+
|
|
288
|
+
def get_pixel_height(self):
|
|
289
|
+
"""Height in pixels of the media's first video stream, or ``None``."""
|
|
290
|
+
return self._get_optional('pixel_height')
|
|
291
|
+
|
|
292
|
+
def set_pixel_height(self, pixel_height):
|
|
293
|
+
"""Set the height in pixels; ``None`` removes it."""
|
|
294
|
+
self._set_positive_int('pixel_height', pixel_height)
|
|
295
|
+
|
|
296
|
+
pixel_height = property(get_pixel_height, set_pixel_height)
|
|
297
|
+
|
|
298
|
+
def get_file_size(self):
|
|
299
|
+
"""Size in bytes of the file the dimensions were measured on, or ``None``."""
|
|
300
|
+
return self._get_optional('file_size')
|
|
301
|
+
|
|
302
|
+
def set_file_size(self, file_size):
|
|
303
|
+
"""Set the file size in bytes; ``None`` removes it."""
|
|
304
|
+
self._set_positive_int('file_size', file_size)
|
|
305
|
+
|
|
306
|
+
file_size = property(get_file_size, set_file_size)
|
|
307
|
+
|
|
308
|
+
def get_file_md5(self):
|
|
309
|
+
"""The file's MD5 (32 lowercase hex digits), or ``None``. Every media
|
|
310
|
+
type; it comes from the upload, which already carries it (D18)."""
|
|
311
|
+
return self._get_optional('file_md5')
|
|
312
|
+
|
|
313
|
+
def set_file_md5(self, file_md5):
|
|
314
|
+
"""Set the file's MD5 (any case, stored lowercase); ``None`` removes it.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ValueError: for anything that is not 32 hex digits.
|
|
318
|
+
"""
|
|
319
|
+
if file_md5 is None:
|
|
320
|
+
self.pop('file_md5', None)
|
|
321
|
+
return
|
|
322
|
+
if not isinstance(file_md5, str) or len(file_md5) != 32 or any(
|
|
323
|
+
c not in '0123456789abcdef' for c in file_md5.lower()
|
|
324
|
+
):
|
|
325
|
+
raise ValueError(f"Media file_md5 must be 32 hex digits, not {file_md5!r}")
|
|
326
|
+
super().__setitem__('file_md5', file_md5.lower())
|
|
327
|
+
|
|
328
|
+
file_md5 = property(get_file_md5, set_file_md5)
|
|
329
|
+
|
|
240
330
|
class MediaCue(Cue):
|
|
241
331
|
"""Base class for media-related cues (audio and video).
|
|
242
332
|
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
from .MediaCue import MediaCue
|
|
2
2
|
from ..tools.CTimecode import CTimecode
|
|
3
|
+
from ..helpers import ensure_items
|
|
3
4
|
from ..log import Logger
|
|
4
5
|
|
|
6
|
+
REQ_ITEMS = {
|
|
7
|
+
'opacity': 100 # Default to fully opaque — 0-100 percent scale (cms:PercentType)
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class VideoCue(MediaCue):
|
|
8
12
|
"""A cue for handling video playback and control.
|
|
9
|
-
|
|
13
|
+
|
|
10
14
|
This class extends MediaCue to provide specific functionality for video playback,
|
|
11
15
|
including frame rate handling and OSC communication for video routing.
|
|
12
16
|
"""
|
|
13
|
-
|
|
17
|
+
|
|
14
18
|
def __init__(self, init_dict = None):
|
|
15
19
|
"""Initialize a VideoCue.
|
|
16
|
-
|
|
20
|
+
|
|
17
21
|
Args:
|
|
18
22
|
init_dict (dict, optional): Dictionary containing initialization values.
|
|
19
23
|
If provided, will be used to set initial properties.
|
|
20
24
|
"""
|
|
21
25
|
if not init_dict:
|
|
22
|
-
|
|
26
|
+
init_dict = REQ_ITEMS
|
|
23
27
|
else:
|
|
24
|
-
|
|
28
|
+
init_dict = ensure_items(init_dict, REQ_ITEMS)
|
|
29
|
+
super().__init__(init_dict)
|
|
25
30
|
|
|
26
31
|
self._player = None
|
|
27
32
|
self._osc_route = None
|
|
@@ -31,6 +36,36 @@ class VideoCue(MediaCue):
|
|
|
31
36
|
self._start_mtc = CTimecode(framerate=25)
|
|
32
37
|
self._end_mtc = CTimecode(framerate=25)
|
|
33
38
|
|
|
39
|
+
def get_opacity(self):
|
|
40
|
+
"""Get the cue's configured opacity level.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
int: The opacity level, 0-100 (applied uniformly across all
|
|
44
|
+
layers of this cue).
|
|
45
|
+
"""
|
|
46
|
+
return super().__getitem__('opacity')
|
|
47
|
+
|
|
48
|
+
def set_opacity(self, opacity):
|
|
49
|
+
"""Set the cue's configured opacity level.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
opacity (int): The new opacity level, 0-100.
|
|
53
|
+
"""
|
|
54
|
+
super().__setitem__('opacity', opacity)
|
|
55
|
+
|
|
56
|
+
opacity = property(get_opacity, set_opacity)
|
|
57
|
+
|
|
58
|
+
def items(self):
|
|
59
|
+
"""Get all items in the cue as a dictionary.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
dict_items: A view of the cue's items, with required items sorted first.
|
|
63
|
+
"""
|
|
64
|
+
x = dict(super().items())
|
|
65
|
+
for k in sorted(REQ_ITEMS.keys()):
|
|
66
|
+
x[k] = self[k]
|
|
67
|
+
return x.items()
|
|
68
|
+
|
|
34
69
|
def player(self, player):
|
|
35
70
|
"""Set the video player instance.
|
|
36
71
|
|
|
@@ -41,21 +76,12 @@ class VideoCue(MediaCue):
|
|
|
41
76
|
|
|
42
77
|
def osc_route(self, osc_route):
|
|
43
78
|
"""Set the OSC route for video control.
|
|
44
|
-
|
|
79
|
+
|
|
45
80
|
Args:
|
|
46
81
|
osc_route (str): The OSC route to use for video control.
|
|
47
82
|
"""
|
|
48
83
|
self._osc_route = osc_route
|
|
49
84
|
|
|
50
|
-
def items(self):
|
|
51
|
-
"""Get all items in the cue as a dictionary.
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
dict_items: A view of the cue's items.
|
|
55
|
-
"""
|
|
56
|
-
x = dict(super().items())
|
|
57
|
-
return x.items()
|
|
58
|
-
|
|
59
85
|
def stop(self):
|
|
60
86
|
"""Stop the video playback.
|
|
61
87
|
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from logging import getLogger, LoggerAdapter, StreamHandler, Formatter, DEBUG, INFO, ERROR, WARNING, CRITICAL
|
|
2
|
+
from logging import getLogger, LoggerAdapter, NullHandler, StreamHandler, Formatter, DEBUG, INFO, ERROR, WARNING, CRITICAL
|
|
3
3
|
from logging.handlers import SysLogHandler
|
|
4
4
|
from functools import wraps
|
|
5
5
|
from os import environ
|
|
6
|
+
from os.path import basename
|
|
6
7
|
import inspect
|
|
7
8
|
|
|
8
9
|
cuemsFormatter = Formatter('[%(asctime)s][%(levelname)s] \tFormitGo (PID: %(process)d)-(%(threadName)-9s)-(%(name)s:%(funcName)s:%(caller)s)> %(message)s')
|
|
9
10
|
|
|
11
|
+
# Third-party libraries sometimes call the module-level logging.info()/
|
|
12
|
+
# warning() helpers at import time (pyossia does, from cuemsengine.osc).
|
|
13
|
+
# Those helpers run logging.basicConfig() whenever the root logger has no
|
|
14
|
+
# handlers, which attaches a StreamHandler(stderr) using logging.BASIC_FORMAT
|
|
15
|
+
# to root. Since the loggers built below propagate, every CUEMS record then
|
|
16
|
+
# gets re-emitted through that handler as a second, differently formatted
|
|
17
|
+
# line — on top of the stdout and syslog copies. Under systemd all of them
|
|
18
|
+
# land in the same journal, so each message was recorded three times.
|
|
19
|
+
#
|
|
20
|
+
# Seeding root with a NullHandler makes the implicit basicConfig() a no-op:
|
|
21
|
+
# it returns early once root has any handler. Propagation is deliberately
|
|
22
|
+
# left enabled — pytest's caplog fixture captures through a root handler and
|
|
23
|
+
# depends on records reaching it.
|
|
24
|
+
getLogger().addHandler(NullHandler())
|
|
25
|
+
|
|
10
26
|
# Cache for module-specific loggers to avoid duplicate handlers
|
|
11
27
|
_logger_cache = {}
|
|
12
28
|
|
|
@@ -41,22 +57,42 @@ class CuemsLoggerAdapter(LoggerAdapter):
|
|
|
41
57
|
kwargs['extra'] = extra
|
|
42
58
|
return msg, kwargs
|
|
43
59
|
|
|
60
|
+
def _syslog_ident():
|
|
61
|
+
"""
|
|
62
|
+
Tag to prefix onto /dev/log datagrams, e.g. 'controller-engine: '.
|
|
63
|
+
|
|
64
|
+
journald derives SYSLOG_IDENTIFIER by parsing a leading 'TAG:' or
|
|
65
|
+
'TAG[PID]:' off the datagram. Without one the entry carries no
|
|
66
|
+
identifier and journalctl falls back to _COMM, which the kernel
|
|
67
|
+
truncates to 15 characters — 'controller-engine' rendered as
|
|
68
|
+
'controller-engi', looking like a second, unrelated process.
|
|
69
|
+
|
|
70
|
+
The PID is deliberately left out: journald fills _PID from the socket
|
|
71
|
+
credentials, so it is always right, whereas a tag built once at handler
|
|
72
|
+
creation would go stale across a fork.
|
|
73
|
+
"""
|
|
74
|
+
name = basename(sys.argv[0] or '') or 'cuems'
|
|
75
|
+
return f'{name}: '
|
|
76
|
+
|
|
44
77
|
def main_logger(module_name = None, with_syslog = True, with_stdout = True):
|
|
45
78
|
"""
|
|
46
79
|
Create a root logger with a custom formatter.
|
|
47
|
-
|
|
80
|
+
|
|
48
81
|
Args:
|
|
49
82
|
module_name: Name of the module to create logger for. Defaults to __name__ if None.
|
|
50
83
|
with_syslog: Whether to add syslog handler.
|
|
51
84
|
with_stdout: Whether to add stdout handler.
|
|
85
|
+
|
|
86
|
+
Under systemd both handlers terminate in the same journal, so only the
|
|
87
|
+
syslog one is attached there — see the comment on the stdout branch.
|
|
52
88
|
"""
|
|
53
89
|
if module_name is None:
|
|
54
90
|
module_name = __name__
|
|
55
|
-
|
|
91
|
+
|
|
56
92
|
# Return cached logger if it exists
|
|
57
93
|
if module_name in _logger_cache:
|
|
58
94
|
return _logger_cache[module_name]
|
|
59
|
-
|
|
95
|
+
|
|
60
96
|
logger = getLogger(module_name)
|
|
61
97
|
try:
|
|
62
98
|
log_level = log_level_to_obj(environ['CUEMS_LOG_LEVEL'].upper())
|
|
@@ -64,16 +100,31 @@ def main_logger(module_name = None, with_syslog = True, with_stdout = True):
|
|
|
64
100
|
log_level = DEBUG
|
|
65
101
|
logger.setLevel(log_level)
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
# Under systemd, stdout and /dev/log both terminate in journald, so
|
|
104
|
+
# attaching both handlers records every message twice. Keep the syslog
|
|
105
|
+
# one: it is the only copy that carries the record's real priority.
|
|
106
|
+
# systemd stamps every stdout line PRIORITY=6 (info) regardless of the
|
|
107
|
+
# Python level, so a journalctl -p filter — which is what
|
|
108
|
+
# `cuems-logs -l/--level` and `-e/--errors` are built on — cannot tell a
|
|
109
|
+
# DEBUG line from an ERROR one on the stdout copy.
|
|
110
|
+
#
|
|
111
|
+
# JOURNAL_STREAM is set by systemd exactly when stdout/stderr are wired
|
|
112
|
+
# to the journal. When it is absent (interactive runs, pytest, a
|
|
113
|
+
# container logging elsewhere) stdout is attached as before, so running
|
|
114
|
+
# a component by hand still prints to the terminal.
|
|
115
|
+
journald_stdout = with_syslog and 'JOURNAL_STREAM' in environ
|
|
116
|
+
|
|
117
|
+
if with_stdout and not journald_stdout:
|
|
68
118
|
sh = StreamHandler(sys.stdout)
|
|
69
119
|
sh.setFormatter(cuemsFormatter)
|
|
70
120
|
logger.addHandler(sh)
|
|
71
|
-
|
|
121
|
+
|
|
72
122
|
if with_syslog:
|
|
73
123
|
syslog_handler = SysLogHandler(
|
|
74
124
|
address = '/dev/log', facility = 'local0'
|
|
75
125
|
)
|
|
76
126
|
syslog_handler.setFormatter(cuemsFormatter)
|
|
127
|
+
syslog_handler.ident = _syslog_ident()
|
|
77
128
|
logger.addHandler(syslog_handler)
|
|
78
129
|
|
|
79
130
|
logger_adapter = CuemsLoggerAdapter(logger, {})
|
|
@@ -429,6 +429,8 @@ class NngBusHub(HubService):
|
|
|
429
429
|
The loop runs until cancelled or an unhandled exception occurs.
|
|
430
430
|
"""
|
|
431
431
|
while await asyncio.sleep(0, result=True):
|
|
432
|
+
if self._closing:
|
|
433
|
+
return
|
|
432
434
|
try:
|
|
433
435
|
pynng_message = await self.connection.arecv_msg()
|
|
434
436
|
|
|
@@ -463,6 +465,17 @@ class NngBusHub(HubService):
|
|
|
463
465
|
|
|
464
466
|
except pynng_exceptions.Timeout:
|
|
465
467
|
pass # Timeout is expected during polling
|
|
468
|
+
except pynng_exceptions.Closed:
|
|
469
|
+
# The socket is gone — either an orderly teardown, or pynng's
|
|
470
|
+
# atexit hook already ran nng_fini() and freed NNG's global
|
|
471
|
+
# state. Retrying would call nng_recv_aio() on that freed
|
|
472
|
+
# state, which aborts the whole process with
|
|
473
|
+
# "panic: pthread_mutex_lock: Invalid argument". Stop for good.
|
|
474
|
+
self._closing = True
|
|
475
|
+
Logger.info(
|
|
476
|
+
f"NNG {self.mode.value} receiver: socket closed, stopping loop"
|
|
477
|
+
)
|
|
478
|
+
return
|
|
466
479
|
except Exception as e:
|
|
467
480
|
Logger.error(f"Error in receiver handler: {e}, type: {type(e)}")
|
|
468
481
|
|
|
@@ -528,6 +541,8 @@ class NngBusHub(HubService):
|
|
|
528
541
|
Logger.info(f"Node connected, sender ready ({len(self.active_connections)} connections)")
|
|
529
542
|
|
|
530
543
|
while await asyncio.sleep(0, result=True):
|
|
544
|
+
if self._closing:
|
|
545
|
+
return
|
|
531
546
|
try:
|
|
532
547
|
message = await self.outgoing.get()
|
|
533
548
|
|
|
@@ -544,6 +559,14 @@ class NngBusHub(HubService):
|
|
|
544
559
|
Logger.debug(f"[SEND #{self._messages_sent_count}] {self.mode.value} sending: type={msg_type}")
|
|
545
560
|
|
|
546
561
|
await self.connection.asend(message.encode())
|
|
562
|
+
except pynng_exceptions.Closed:
|
|
563
|
+
# Same rule as the receiver: never touch NNG again once the
|
|
564
|
+
# socket is closed (see _receiver_handler).
|
|
565
|
+
self._closing = True
|
|
566
|
+
Logger.info(
|
|
567
|
+
f"NNG {self.mode.value} sender: socket closed, stopping loop"
|
|
568
|
+
)
|
|
569
|
+
return
|
|
547
570
|
except Exception as e:
|
|
548
571
|
Logger.error(f"Error in send handler: {e}, type: {type(e)}")
|
|
549
572
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Stagelab Coop SCCL
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# SPDX-FileContributor: Ion Reguera <ion@stagelab.coop>
|
|
1
4
|
from ..cues import *
|
|
2
5
|
from ..cues.FadeProfile import FadeFunctionParameter, FadeProfile
|
|
3
6
|
from ..cues.MediaCue import Media, Region
|
|
@@ -33,6 +36,9 @@ STRING_TYPED_KEYS = frozenset({
|
|
|
33
36
|
'name', 'description', 'file_name',
|
|
34
37
|
# defensive -- see above
|
|
35
38
|
'output_name', 'parameter_name', 'icon', 'color', 'unix_name',
|
|
39
|
+
# an MD5 is hex: about one in a million is all digits (-> int) or digits
|
|
40
|
+
# with one 'e' (-> float), and would no longer validate (869fat84r D18)
|
|
41
|
+
'file_md5',
|
|
36
42
|
})
|
|
37
43
|
|
|
38
44
|
class GenericDict(dict):
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Stagelab Coop SCCL
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# SPDX-FileContributor: Ion Reguera <ion@stagelab.coop>
|
|
1
4
|
from enum import Enum
|
|
2
5
|
from xml.etree.ElementTree import Element, ElementTree, SubElement, register_namespace
|
|
3
6
|
|
|
@@ -170,10 +173,34 @@ class GenericComplexSubObjectXmlBuilder(CuemsScriptXmlBuilder):
|
|
|
170
173
|
class CTimecodeXmlBuilder(GenericSimpleSubObjectXmlBuilder):
|
|
171
174
|
pass
|
|
172
175
|
|
|
176
|
+
#: ``MediaType``'s element order in script.xsd. The writer follows it, not the
|
|
177
|
+
#: dict's order: the editor appends the stored dimensions after whatever the
|
|
178
|
+
#: frontend sent, and the parsers assign keys in arrival order (869fat84r).
|
|
179
|
+
MEDIA_ELEMENT_ORDER = (
|
|
180
|
+
'file_name', 'id', 'duration', 'regions',
|
|
181
|
+
'pixel_width', 'pixel_height', 'file_size', 'file_md5',
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
#: Optional MediaType elements: a ``None`` value writes no element at all
|
|
185
|
+
#: (an empty ``<pixel_width/>`` would fail ``xs:positiveInteger``).
|
|
186
|
+
MEDIA_OPTIONAL_ELEMENTS = frozenset({'pixel_width', 'pixel_height', 'file_size', 'file_md5'})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _media_items_in_schema_order(media):
|
|
190
|
+
"""``media``'s items, known keys in schema order, then any others in
|
|
191
|
+
arrival order (so the validator, not this writer, rejects them)."""
|
|
192
|
+
known = [(k, media[k]) for k in MEDIA_ELEMENT_ORDER if k in media]
|
|
193
|
+
other = [(k, v) for k, v in media.items() if k not in MEDIA_ELEMENT_ORDER]
|
|
194
|
+
return [
|
|
195
|
+
(k, v) for k, v in known + other
|
|
196
|
+
if not (v is None and k in MEDIA_OPTIONAL_ELEMENTS)
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
173
200
|
class MediaXmlBuilder(GenericComplexSubObjectXmlBuilder):
|
|
174
201
|
def build(self):
|
|
175
202
|
if isinstance(self._object, dict):
|
|
176
|
-
for key, value in self._object
|
|
203
|
+
for key, value in _media_items_in_schema_order(self._object):
|
|
177
204
|
if isinstance(value, VALUE_TYPES):
|
|
178
205
|
cue_subelement = SubElement(self.xml_tree, key)
|
|
179
206
|
cue_subelement.text = str(value)
|
|
@@ -334,7 +361,7 @@ class MediaCueXmlBuilder(GenericCueXmlBuilder):
|
|
|
334
361
|
builder_class(value, xml_tree=cue_subelement).build()
|
|
335
362
|
cls_name = type(self._object).__name__
|
|
336
363
|
if key == 'master_vol' or (
|
|
337
|
-
key == '
|
|
364
|
+
key == 'opacity' and cls_name == 'VideoCue'
|
|
338
365
|
):
|
|
339
366
|
fps = self._object.get('fade_profiles')
|
|
340
367
|
if fps:
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
<?xml version="1.1" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
SPDX-FileCopyrightText: 2026 Stagelab Coop SCCL
|
|
4
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
SPDX-FileContributor: Ion Reguera <ion@stagelab.coop>
|
|
6
|
+
-->
|
|
2
7
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" vc:minVersion="1.1" targetNamespace="https://stagelab.coop/cuems/" xmlns:cms="https://stagelab.coop/cuems/">
|
|
3
8
|
<!-- The main capsule to make it a whole, named CueMs -->
|
|
4
9
|
<!-- Our main object the script-->
|
|
@@ -181,9 +186,31 @@
|
|
|
181
186
|
<xs:element name="id" type="cms:TargetType" />
|
|
182
187
|
<xs:element name="duration" type="cms:TimecodeType" />
|
|
183
188
|
<xs:element name="regions" type="cms:RegionsType" />
|
|
189
|
+
<!-- Stored size of the media file (869fat84r), measured by the editor
|
|
190
|
+
at upload so the engine does not probe the file when it arms a cue.
|
|
191
|
+
pixel_width / pixel_height: the ORIGINAL size of the file's first
|
|
192
|
+
video stream, as ffprobe reports it; not the layer's size on screen
|
|
193
|
+
(that would get its own names) and unrelated to CanvasRegionType's
|
|
194
|
+
unit-float width / height. file_size: the file's size in bytes when
|
|
195
|
+
they were measured; the engine compares it with its own copy to
|
|
196
|
+
notice a file replaced under the same name. Optional: absent means
|
|
197
|
+
unknown, and the engine then probes as before. The pixel size is
|
|
198
|
+
for VideoCues only; file_size and file_md5 for every media type.
|
|
199
|
+
file_md5: the file's MD5 (lowercase hex), from the upload, which
|
|
200
|
+
already carries it; identifies the file for integrity checks. -->
|
|
201
|
+
<xs:element name="pixel_width" type="xs:positiveInteger" minOccurs="0" />
|
|
202
|
+
<xs:element name="pixel_height" type="xs:positiveInteger" minOccurs="0" />
|
|
203
|
+
<xs:element name="file_size" type="xs:positiveInteger" minOccurs="0" />
|
|
204
|
+
<xs:element name="file_md5" type="cms:Md5Type" minOccurs="0" />
|
|
184
205
|
</xs:sequence>
|
|
185
206
|
</xs:complexType>
|
|
186
207
|
|
|
208
|
+
<xs:simpleType name="Md5Type">
|
|
209
|
+
<xs:restriction base="xs:string">
|
|
210
|
+
<xs:pattern value="[0-9a-f]{32}" />
|
|
211
|
+
</xs:restriction>
|
|
212
|
+
</xs:simpleType>
|
|
213
|
+
|
|
187
214
|
<xs:complexType name="OutputsType">
|
|
188
215
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
|
189
216
|
<xs:element name="AudioCueOutput" type="cms:AudioCueOutputsType" />
|
|
@@ -251,12 +278,18 @@
|
|
|
251
278
|
</xs:restriction>
|
|
252
279
|
</xs:simpleType>
|
|
253
280
|
|
|
281
|
+
<!-- linear/sigmoid/ease_in/ease_out are the curves gradient-motiond's
|
|
282
|
+
CurveFactory implements (the editor UI submits only these);
|
|
283
|
+
exponential/logarithmic are legacy names kept so old projects still
|
|
284
|
+
load — the frontend heals them to ease_in/ease_out on next save. -->
|
|
254
285
|
<xs:simpleType name="FadeCurveType">
|
|
255
286
|
<xs:restriction base="xs:string">
|
|
256
287
|
<xs:enumeration value="linear" />
|
|
257
288
|
<xs:enumeration value="exponential" />
|
|
258
289
|
<xs:enumeration value="logarithmic" />
|
|
259
290
|
<xs:enumeration value="sigmoid" />
|
|
291
|
+
<xs:enumeration value="ease_in" />
|
|
292
|
+
<xs:enumeration value="ease_out" />
|
|
260
293
|
</xs:restriction>
|
|
261
294
|
</xs:simpleType>
|
|
262
295
|
|
|
@@ -312,6 +345,10 @@
|
|
|
312
345
|
<xs:complexContent>
|
|
313
346
|
<xs:extension base="cms:MediaCueType">
|
|
314
347
|
<xs:sequence>
|
|
348
|
+
<!-- Optional (unlike AudioCue's master_vol) to stay backward-compatible
|
|
349
|
+
with existing project files authored before this field existed;
|
|
350
|
+
absent -> Python-side REQ_ITEMS default of 100 (fully opaque). -->
|
|
351
|
+
<xs:element name="opacity" type="cms:PercentType" minOccurs="0" />
|
|
315
352
|
<xs:element name="fade_profiles" type="cms:FadeProfilesWrapperType" minOccurs="0" />
|
|
316
353
|
</xs:sequence>
|
|
317
354
|
</xs:extension>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|