lightshark-parser 1.0.0__py3-none-any.whl
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.
- lightshark_parser/__init__.py +13 -0
- lightshark_parser/__main__.py +6 -0
- lightshark_parser/classes/__init__.py +28 -0
- lightshark_parser/classes/action.py +11 -0
- lightshark_parser/classes/cue.py +176 -0
- lightshark_parser/classes/cuelist.py +271 -0
- lightshark_parser/classes/fileinfo.py +98 -0
- lightshark_parser/classes/fx.py +516 -0
- lightshark_parser/classes/general.py +117 -0
- lightshark_parser/classes/group.py +188 -0
- lightshark_parser/classes/lightshow.py +1124 -0
- lightshark_parser/classes/model.py +439 -0
- lightshark_parser/classes/order.py +97 -0
- lightshark_parser/classes/patch.py +140 -0
- lightshark_parser/classes/playback.py +201 -0
- lightshark_parser/classes/user_palette.py +102 -0
- lightshark_parser/main.py +129 -0
- lightshark_parser/parsers/__init__.py +6 -0
- lightshark_parser/parsers/attribute_parsers.py +178 -0
- lightshark_parser/parsers/file_parser.py +345 -0
- lightshark_parser/parsers/parse_dict.py +6 -0
- lightshark_parser/parsers/section_parsers.py +1121 -0
- lightshark_parser/serialisers/__init__.py +0 -0
- lightshark_parser/serialisers/attribute_serialisers.py +66 -0
- lightshark_parser/summariser.py +587 -0
- lightshark_parser/utils/__init__.py +7 -0
- lightshark_parser/utils/custom_errors.py +9 -0
- lightshark_parser/utils/json_mappings.py +85 -0
- lightshark_parser/utils/logger.py +22 -0
- lightshark_parser-1.0.0.dist-info/METADATA +81 -0
- lightshark_parser-1.0.0.dist-info/RECORD +54 -0
- lightshark_parser-1.0.0.dist-info/WHEEL +5 -0
- lightshark_parser-1.0.0.dist-info/entry_points.txt +2 -0
- lightshark_parser-1.0.0.dist-info/licenses/LICENSE +21 -0
- lightshark_parser-1.0.0.dist-info/top_level.txt +3 -0
- private_tests/analyze_fxs_channels.py +235 -0
- private_tests/analyze_sections.py +73 -0
- private_tests/analyze_show.py +316 -0
- private_tests/compare_lightshows.py +86 -0
- private_tests/test_names.py +34 -0
- private_tests/test_palette_edit.py +52 -0
- private_tests/test_parse_dict.py +35 -0
- private_tests/test_repr.py +14 -0
- tests/conftest.py +293 -0
- tests/integration/test_real_data.py +141 -0
- tests/integration/test_summariser.py +71 -0
- tests/unit/test_cue.py +121 -0
- tests/unit/test_cuelist.py +131 -0
- tests/unit/test_group.py +102 -0
- tests/unit/test_lightshow.py +175 -0
- tests/unit/test_model.py +84 -0
- tests/unit/test_order.py +85 -0
- tests/unit/test_patch.py +91 -0
- tests/unit/test_user_palette.py +106 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LightShark Parser - A Python library for parsing LightShark show files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
|
|
7
|
+
from lightshark_parser.classes import Lightshow
|
|
8
|
+
from lightshark_parser.parsers.file_parser import parse_file_bytes, parse_lshw
|
|
9
|
+
from lightshark_parser.parsers.parse_dict import parse_dict_to_lightshow
|
|
10
|
+
from lightshark_parser.utils.logger import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["parse_file_bytes", "parse_lshw", "parse_dict_to_lightshow", "Lightshow", "logger"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Lightshow Parser - Core data models for Lightshow files."""
|
|
2
|
+
|
|
3
|
+
from .lightshow import Lightshow
|
|
4
|
+
from .fileinfo import Version, FileInfo
|
|
5
|
+
from .model import Model, ModelPalette, ModelValue, ModelValueStep, ModelHardware, Macro, MacroStep
|
|
6
|
+
from .patch import Patch
|
|
7
|
+
from .group import Group
|
|
8
|
+
from .user_palette import UserPalette
|
|
9
|
+
from .cue import Cue, Order
|
|
10
|
+
from .cuelist import Cuelist, CuelistElement
|
|
11
|
+
from .playback import Playback
|
|
12
|
+
from .fx import FX, FXLayer, FXLayerStep, FXPalette
|
|
13
|
+
from .general import Config, General
|
|
14
|
+
from .action import Action
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'Lightshow',
|
|
18
|
+
'Version', 'FileInfo',
|
|
19
|
+
'Model', 'ModelPalette', 'ModelValue', 'ModelValueStep', 'ModelHardware', 'Macro', 'MacroStep',
|
|
20
|
+
'Patch',
|
|
21
|
+
'Group',
|
|
22
|
+
'UserPalette',
|
|
23
|
+
'Cue', 'Order', 'Action',
|
|
24
|
+
'Cuelist', 'CuelistElement',
|
|
25
|
+
'Playback',
|
|
26
|
+
'FX', 'FXLayer', 'FXLayerStep', 'FXPalette',
|
|
27
|
+
'Config', 'General',
|
|
28
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, List, Any, Optional, Union
|
|
3
|
+
from lightshark_parser.serialisers.attribute_serialisers import *
|
|
4
|
+
from lightshark_parser.classes.fx import FX
|
|
5
|
+
from lightshark_parser.classes.order import Order
|
|
6
|
+
from lightshark_parser.classes.action import Action
|
|
7
|
+
from lightshark_parser.utils.logger import logger
|
|
8
|
+
|
|
9
|
+
class Cue:
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NOTE: Actions not implemented yet
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
fx_palette: Optional[int|str] = None,
|
|
19
|
+
cue_id: Optional[int] = None,
|
|
20
|
+
description: Optional[str] = None,
|
|
21
|
+
visual_id: Optional[int] = None,
|
|
22
|
+
fxs: Optional[List["FX"]] = None,
|
|
23
|
+
fxs_channels: Optional[List[List[Union[int, str]]]] = None,
|
|
24
|
+
orders: Optional[dict[int, dict[int, Order]]] = None,
|
|
25
|
+
actions: Optional[List["Action"]] = None,
|
|
26
|
+
name: Optional[str] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.fx_palette: int|str = fx_palette
|
|
29
|
+
self.cue_id: int = cue_id
|
|
30
|
+
self.description: str = description
|
|
31
|
+
self.visual_id: int = visual_id
|
|
32
|
+
self.fxs: List[FX] = fxs if fxs is not None else []
|
|
33
|
+
self.fxs_channels: List[dict] = fxs_channels
|
|
34
|
+
self.orders: dict[int, dict[int, Order]] = orders
|
|
35
|
+
self.actions: List[Action] = actions
|
|
36
|
+
self.name: str = name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return (
|
|
42
|
+
f"Cue(fx_palette={self.fx_palette!r}, cue_id={self.cue_id!r}, "
|
|
43
|
+
f"description={self.description!r}, visual_id={self.visual_id!r}, "
|
|
44
|
+
f"fxs={self.fxs!r}, fxs_channels={self.fxs_channels!r}, "
|
|
45
|
+
f"orders={self.orders!r}, actions={self.actions!r}, name={self.name!r})"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict:
|
|
49
|
+
fxs_list = []
|
|
50
|
+
if self.fxs is not None:
|
|
51
|
+
for fx in self.fxs:
|
|
52
|
+
if hasattr(fx, "to_dict"):
|
|
53
|
+
fxs_list.append(fx.to_dict())
|
|
54
|
+
elif hasattr(fx, "__dict__"):
|
|
55
|
+
fxs_list.append(fx.__dict__)
|
|
56
|
+
else:
|
|
57
|
+
fxs_list.append(fx)
|
|
58
|
+
|
|
59
|
+
orders_dict = {}
|
|
60
|
+
if self.orders is not None:
|
|
61
|
+
orders_dict = {
|
|
62
|
+
patch_id: {
|
|
63
|
+
ftype: order.to_dict() if hasattr(order, "to_dict") else order
|
|
64
|
+
for ftype, order in ftype_orders.items()
|
|
65
|
+
}
|
|
66
|
+
for patch_id, ftype_orders in self.orders.items()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"fx_palette": self.fx_palette,
|
|
71
|
+
"cue_id": self.cue_id,
|
|
72
|
+
"description": self.description,
|
|
73
|
+
"visual_id": self.visual_id,
|
|
74
|
+
"actions": self.actions,
|
|
75
|
+
"fxs": fxs_list, # Always return the list (empty if no fxs)
|
|
76
|
+
"fxs_channels": self.fxs_channels,
|
|
77
|
+
"orders": orders_dict,
|
|
78
|
+
"name": self.name,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Cue":
|
|
83
|
+
if data is None:
|
|
84
|
+
return None
|
|
85
|
+
fxs = [FX.from_dict(fx) for fx in data.get("fxs", [])] if data.get("fxs") else []
|
|
86
|
+
orders_data = data.get("orders", {})
|
|
87
|
+
orders = {
|
|
88
|
+
int(patch_id): {
|
|
89
|
+
int(ftype): Order.from_dict(order_data)
|
|
90
|
+
for ftype, order_data in ftype_orders.items()
|
|
91
|
+
}
|
|
92
|
+
for patch_id, ftype_orders in orders_data.items()
|
|
93
|
+
}
|
|
94
|
+
actions = [Action.from_dict(action) for action in data.get("actions", [])] if data.get("actions") else None
|
|
95
|
+
return cls(
|
|
96
|
+
fx_palette=data.get("fx_palette"),
|
|
97
|
+
cue_id=data.get("cue_id"),
|
|
98
|
+
description=data.get("description"),
|
|
99
|
+
visual_id=data.get("visual_id"),
|
|
100
|
+
fxs=fxs,
|
|
101
|
+
fxs_channels=data.get("fxs_channels"),
|
|
102
|
+
orders=orders,
|
|
103
|
+
actions=actions,
|
|
104
|
+
name=data.get("name"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def to_bytes(self) -> bytes:
|
|
109
|
+
logger.debug(f"Starting serialization of Cue object {self.name} (id: {self.cue_id})")
|
|
110
|
+
bytestr = bytearray(serialise_section_header("cue"))
|
|
111
|
+
content = bytearray()
|
|
112
|
+
num_attr = 0
|
|
113
|
+
|
|
114
|
+
num_attrs = ["cue_id", "visual_id"]
|
|
115
|
+
string_attrs = ["description", "name"]
|
|
116
|
+
|
|
117
|
+
for attr_name, attr_value in self.__dict__.items():
|
|
118
|
+
if attr_value is not None:
|
|
119
|
+
content.extend(serialise_attr_name(attr_name))
|
|
120
|
+
num_attr += 1
|
|
121
|
+
logger.debug("Serialising attribute: " + attr_name)
|
|
122
|
+
if attr_name == "fx_palette":
|
|
123
|
+
if attr_value == "N/A":
|
|
124
|
+
content.extend(b'\xFF')
|
|
125
|
+
else:
|
|
126
|
+
content.extend(serialise_num_value(attr_value, cc_check=True))
|
|
127
|
+
elif attr_name in num_attrs:
|
|
128
|
+
content.extend(serialise_num_value(attr_value, cc_check=True))
|
|
129
|
+
elif attr_name in string_attrs:
|
|
130
|
+
content.extend(serialise_str_value(attr_value))
|
|
131
|
+
elif attr_name == 'fxs_channels':
|
|
132
|
+
num_lists = len(attr_value)
|
|
133
|
+
content.extend(serialise_objlist_len(num_lists))
|
|
134
|
+
logger.debug(f"value: {attr_value}")
|
|
135
|
+
logger.debug(f"Number of FX channel instances: {num_lists}")
|
|
136
|
+
|
|
137
|
+
for fx_channel in attr_value:
|
|
138
|
+
logger.debug(f"channel: {fx_channel}")
|
|
139
|
+
list_content = bytearray()
|
|
140
|
+
list_len = sum(1 for value in fx_channel if not isinstance(value, str))
|
|
141
|
+
if list_len != 16:
|
|
142
|
+
raise ValueError("FX channel list length should be 16 (NOTE: unconfirmed. though this error shouldn't happen either way...)")
|
|
143
|
+
list_content.extend(serialise_objlist_len(list_len))
|
|
144
|
+
for value in fx_channel:
|
|
145
|
+
# \xd1 \x?? instances
|
|
146
|
+
if isinstance(value, str):
|
|
147
|
+
list_content.extend(bytes.fromhex(value))
|
|
148
|
+
else:
|
|
149
|
+
list_content.extend(serialise_num_value(value, cc_check=True))
|
|
150
|
+
content.extend(list_content)
|
|
151
|
+
|
|
152
|
+
elif attr_name == 'fxs':
|
|
153
|
+
num_fxs = len(attr_value)
|
|
154
|
+
content.extend(serialise_objlist_len(num_fxs))
|
|
155
|
+
for fx in attr_value:
|
|
156
|
+
content.extend(fx.to_bytes())
|
|
157
|
+
elif attr_name == 'orders':
|
|
158
|
+
num_orders = sum(len(ftype_orders) for ftype_orders in attr_value.values())
|
|
159
|
+
content.extend(serialise_objlist_len(num_orders))
|
|
160
|
+
for ftype_orders in attr_value.values():
|
|
161
|
+
for order in ftype_orders.values():
|
|
162
|
+
content.extend(order.to_bytes())
|
|
163
|
+
elif attr_name == 'actions':
|
|
164
|
+
raise NotImplementedError("Actions not implemented yet")
|
|
165
|
+
# num_actions = len(attr_value)
|
|
166
|
+
# bytestr.extend(serialise_objlist_len(num_actions))
|
|
167
|
+
# for action in attr_value:
|
|
168
|
+
# bytestr.extend(action.to_bytes())
|
|
169
|
+
logger.debug(f"Serialized {attr_name} for Cue {self.name} (id: {self.cue_id})")
|
|
170
|
+
|
|
171
|
+
content[0:0] = serialise_num_attr(num_attr)
|
|
172
|
+
bytestr.extend(serialise_content_length(content))
|
|
173
|
+
bytestr.extend(content)
|
|
174
|
+
logger.info("Cue object serialised")
|
|
175
|
+
logger.debug(bytestr)
|
|
176
|
+
return bytes(bytestr)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, List, Any, Optional, Union
|
|
3
|
+
from lightshark_parser.serialisers.attribute_serialisers import *
|
|
4
|
+
from lightshark_parser.utils.logger import logger
|
|
5
|
+
|
|
6
|
+
class Cuelist:
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
ms_flash_attack: Optional[int] = 2000,
|
|
11
|
+
autoreset: Optional[bool] = True,
|
|
12
|
+
at_end_pause: Optional[bool] = False,
|
|
13
|
+
loops: Optional[int] = 1,
|
|
14
|
+
chase: Optional[bool] = False,
|
|
15
|
+
ms_chase_time: Optional[int] = 2000,
|
|
16
|
+
visual_id: Optional[int] = None,
|
|
17
|
+
bpm_chase: Optional[int] = 30000,
|
|
18
|
+
ms_flash_decay: Optional[int] = 2000,
|
|
19
|
+
pcrossfade: Optional[int] = 100000,
|
|
20
|
+
ms_fadeout: Optional[int] = 2000,
|
|
21
|
+
direction: Optional[int] = 0,
|
|
22
|
+
at_end_stop: Optional[bool] = False,
|
|
23
|
+
flash_mode: Optional[int] = 0,
|
|
24
|
+
cuelist_id: Optional[int] = None,
|
|
25
|
+
ms_fadein: Optional[int] = 2000,
|
|
26
|
+
ms_crossfade: Optional[int] = 2000,
|
|
27
|
+
no_first_fade: Optional[bool] = False,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
block_fx: Optional[bool] = False,
|
|
30
|
+
cuelist_elements: Optional[Dict[int, CuelistElement]] = None,
|
|
31
|
+
ms_flash_hold: Optional[int] = 2000,
|
|
32
|
+
ms_stop_time: Optional[int] = 2000,
|
|
33
|
+
) -> None:
|
|
34
|
+
|
|
35
|
+
if name is None:
|
|
36
|
+
name = f"Cuelist {cuelist_id}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
self.ms_flash_attack: Optional[int] = ms_flash_attack
|
|
40
|
+
self.autoreset: Optional[bool] = autoreset
|
|
41
|
+
self.at_end_pause: Optional[bool] = at_end_pause
|
|
42
|
+
self.loops: Optional[int] = loops
|
|
43
|
+
self.chase: Optional[bool] = chase
|
|
44
|
+
self.ms_chase_time: Optional[int] = ms_chase_time
|
|
45
|
+
self.visual_id: Optional[int] = visual_id
|
|
46
|
+
self.bpm_chase: Optional[int] = bpm_chase
|
|
47
|
+
self.ms_flash_decay: Optional[int] = ms_flash_decay
|
|
48
|
+
self.pcrossfade: Optional[int] = pcrossfade
|
|
49
|
+
self.ms_fadeout: Optional[int] = ms_fadeout
|
|
50
|
+
self.direction: Optional[int] = direction
|
|
51
|
+
self.at_end_stop: Optional[bool] = at_end_stop
|
|
52
|
+
self.flash_mode: Optional[int] = flash_mode
|
|
53
|
+
self.cuelist_id: Optional[int] = cuelist_id
|
|
54
|
+
self.ms_fadein: Optional[int] = ms_fadein
|
|
55
|
+
self.ms_crossfade: Optional[int] = ms_crossfade
|
|
56
|
+
self.no_first_fade: Optional[bool] = no_first_fade
|
|
57
|
+
self.name: Optional[str] = name
|
|
58
|
+
self.block_fx: Optional[bool] = block_fx
|
|
59
|
+
self.cuelist_elements: Dict[int, CuelistElement] = cuelist_elements if cuelist_elements is not None else {}
|
|
60
|
+
self.ms_flash_hold: Optional[int] = ms_flash_hold
|
|
61
|
+
self.ms_stop_time: Optional[int] = ms_stop_time
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
return (
|
|
65
|
+
f"Cuelist(ms_flash_attack={self.ms_flash_attack!r}, autoreset={self.autoreset!r}, "
|
|
66
|
+
f"at_end_pause={self.at_end_pause!r}, loops={self.loops!r}, chase={self.chase!r}, "
|
|
67
|
+
f"ms_chase_time={self.ms_chase_time!r}, visual_id={self.visual_id!r}, "
|
|
68
|
+
f"bpm_chase={self.bpm_chase!r}, ms_flash_decay={self.ms_flash_decay!r}, "
|
|
69
|
+
f"pcrossfade={self.pcrossfade!r}, ms_fadeout={self.ms_fadeout!r}, "
|
|
70
|
+
f"direction={self.direction!r}, at_end_stop={self.at_end_stop!r}, "
|
|
71
|
+
f"flash_mode={self.flash_mode!r}, cuelist_id={self.cuelist_id!r}, "
|
|
72
|
+
f"ms_fadein={self.ms_fadein!r}, ms_crossfade={self.ms_crossfade!r}, "
|
|
73
|
+
f"no_first_fade={self.no_first_fade!r}, name={self.name!r}, block_fx={self.block_fx!r}, "
|
|
74
|
+
f"cuelist_elements={self.cuelist_elements!r}, "
|
|
75
|
+
f"ms_flash_hold={self.ms_flash_hold!r}, ms_stop_time={self.ms_stop_time!r})"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"ms_flash_attack": self.ms_flash_attack,
|
|
82
|
+
"autoreset": self.autoreset,
|
|
83
|
+
"at_end_pause": self.at_end_pause,
|
|
84
|
+
"loops": self.loops,
|
|
85
|
+
"chase": self.chase,
|
|
86
|
+
"ms_chase_time": self.ms_chase_time,
|
|
87
|
+
"visual_id": self.visual_id,
|
|
88
|
+
"bpm_chase": self.bpm_chase,
|
|
89
|
+
"ms_flash_decay": self.ms_flash_decay,
|
|
90
|
+
"pcrossfade": self.pcrossfade,
|
|
91
|
+
"ms_fadeout": self.ms_fadeout,
|
|
92
|
+
"direction": self.direction,
|
|
93
|
+
"at_end_stop": self.at_end_stop,
|
|
94
|
+
"flash_mode": self.flash_mode,
|
|
95
|
+
"cuelist_id": self.cuelist_id,
|
|
96
|
+
"ms_fadein": self.ms_fadein,
|
|
97
|
+
"ms_crossfade": self.ms_crossfade,
|
|
98
|
+
"no_first_fade": self.no_first_fade,
|
|
99
|
+
"name": self.name,
|
|
100
|
+
"block_fx": self.block_fx,
|
|
101
|
+
"cuelist_elements": {dotted_id: elem.to_dict() for dotted_id, elem in self.cuelist_elements.items()},
|
|
102
|
+
"ms_flash_hold": self.ms_flash_hold,
|
|
103
|
+
"ms_stop_time": self.ms_stop_time,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Cuelist":
|
|
108
|
+
if data is None:
|
|
109
|
+
return None
|
|
110
|
+
cuelist_elements = {int(dotted_id): CuelistElement.from_dict(elem) for dotted_id, elem in data.get("cuelist_elements", {}).items()}
|
|
111
|
+
return cls(
|
|
112
|
+
ms_flash_attack=data.get("ms_flash_attack"),
|
|
113
|
+
autoreset=data.get("autoreset"),
|
|
114
|
+
at_end_pause=data.get("at_end_pause"),
|
|
115
|
+
loops=data.get("loops"),
|
|
116
|
+
chase=data.get("chase"),
|
|
117
|
+
ms_chase_time=data.get("ms_chase_time"),
|
|
118
|
+
visual_id=data.get("visual_id"),
|
|
119
|
+
bpm_chase=data.get("bpm_chase"),
|
|
120
|
+
ms_flash_decay=data.get("ms_flash_decay"),
|
|
121
|
+
pcrossfade=data.get("pcrossfade"),
|
|
122
|
+
ms_fadeout=data.get("ms_fadeout"),
|
|
123
|
+
direction=data.get("direction"),
|
|
124
|
+
at_end_stop=data.get("at_end_stop"),
|
|
125
|
+
flash_mode=data.get("flash_mode"),
|
|
126
|
+
cuelist_id=data.get("cuelist_id"),
|
|
127
|
+
ms_fadein=data.get("ms_fadein"),
|
|
128
|
+
ms_crossfade=data.get("ms_crossfade"),
|
|
129
|
+
no_first_fade=data.get("no_first_fade"),
|
|
130
|
+
name=data.get("name"),
|
|
131
|
+
block_fx=data.get("block_fx"),
|
|
132
|
+
cuelist_elements=cuelist_elements,
|
|
133
|
+
ms_flash_hold=data.get("ms_flash_hold"),
|
|
134
|
+
ms_stop_time=data.get("ms_stop_time"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def to_bytes(self) -> bytes:
|
|
138
|
+
logger.debug(f"Starting serialisation of Cuelist object {self.name}")
|
|
139
|
+
bytestr = bytearray(serialise_section_header("cuelist"))
|
|
140
|
+
content = bytearray()
|
|
141
|
+
num_attr = 0
|
|
142
|
+
|
|
143
|
+
num_attrs = [
|
|
144
|
+
"ms_flash_attack", "ms_flash_decay", "ms_flash_hold", "ms_chase_time", "ms_fadein", "ms_fadeout",
|
|
145
|
+
"ms_crossfade", "ms_stop_time", "visual_id", "bpm_chase", "pcrossfade", "direction", "flash_mode", "cuelist_id", "loops"
|
|
146
|
+
]
|
|
147
|
+
bool_attrs = ["autoreset", "chase", "at_end_pause", "at_end_stop", "no_first_fade", "block_fx"]
|
|
148
|
+
string_attrs = ["name"]
|
|
149
|
+
|
|
150
|
+
for attr_name, attr_value in self.__dict__.items():
|
|
151
|
+
if attr_value is not None:
|
|
152
|
+
content.extend(serialise_attr_name(attr_name))
|
|
153
|
+
num_attr += 1
|
|
154
|
+
|
|
155
|
+
if attr_name in num_attrs:
|
|
156
|
+
content.extend(serialise_num_value(attr_value, cc_check=True))
|
|
157
|
+
elif attr_name in bool_attrs:
|
|
158
|
+
content.extend(serialise_bool_value(attr_value))
|
|
159
|
+
elif attr_name in string_attrs:
|
|
160
|
+
content.extend(serialise_str_value(attr_value))
|
|
161
|
+
elif attr_name == "cuelist_elements":
|
|
162
|
+
num_elements = len(attr_value) if attr_value else 0
|
|
163
|
+
content.extend(serialise_objlist_len(num_elements))
|
|
164
|
+
if attr_value: # Only process if there are elements
|
|
165
|
+
# Sort by dotted_id to maintain consistent order
|
|
166
|
+
for dotted_id in sorted(attr_value.keys()):
|
|
167
|
+
element = attr_value[dotted_id]
|
|
168
|
+
content.extend(element.to_bytes())
|
|
169
|
+
|
|
170
|
+
content[0:0] = serialise_num_attr(num_attr)
|
|
171
|
+
bytestr.extend(serialise_content_length(content))
|
|
172
|
+
bytestr.extend(content)
|
|
173
|
+
logger.info("Cuelist object serialised")
|
|
174
|
+
logger.debug(bytestr)
|
|
175
|
+
return bytes(bytestr)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class CuelistElement:
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
ms_fadeout: Optional[int] = None,
|
|
182
|
+
cue_id: Optional[int] = None,
|
|
183
|
+
ms_delay: Optional[int] = None,
|
|
184
|
+
next: Optional[Union[int, str]] = "Next",
|
|
185
|
+
dotted_id: Optional[int] = None,
|
|
186
|
+
ms_fadein: Optional[int] = None,
|
|
187
|
+
ms_crossfade: Optional[int] = None,
|
|
188
|
+
ms_duration: Optional[int] = None,
|
|
189
|
+
halt: Optional[bool] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.ms_fadeout: Optional[int] = ms_fadeout
|
|
192
|
+
self.cue_id: Optional[int] = cue_id
|
|
193
|
+
self.ms_delay: Optional[int] = ms_delay
|
|
194
|
+
self.next: Optional[Union[int, str]] = next
|
|
195
|
+
self.dotted_id: Optional[int] = dotted_id
|
|
196
|
+
self.ms_fadein: Optional[int] = ms_fadein
|
|
197
|
+
self.ms_crossfade: Optional[int] = ms_crossfade
|
|
198
|
+
self.ms_duration: Optional[int] = ms_duration
|
|
199
|
+
self.halt: Optional[bool] = halt
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def __repr__(self):
|
|
203
|
+
return (
|
|
204
|
+
f"CuelistElement(ms_fadeout={self.ms_fadeout!r}, cue_id={self.cue_id!r}, "
|
|
205
|
+
f"ms_delay={self.ms_delay!r}, next={self.next!r}, dotted_id={self.dotted_id!r}, "
|
|
206
|
+
f"ms_fadein={self.ms_fadein!r}, ms_crossfade={self.ms_crossfade!r}, "
|
|
207
|
+
f"ms_duration={self.ms_duration!r}, halt={self.halt!r})"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def to_dict(self) -> dict:
|
|
211
|
+
return {
|
|
212
|
+
"ms_fadeout": self.ms_fadeout,
|
|
213
|
+
"cue_id": self.cue_id,
|
|
214
|
+
"ms_delay": self.ms_delay,
|
|
215
|
+
"next": self.next,
|
|
216
|
+
"dotted_id": self.dotted_id,
|
|
217
|
+
"ms_fadein": self.ms_fadein,
|
|
218
|
+
"ms_crossfade": self.ms_crossfade,
|
|
219
|
+
"ms_duration": self.ms_duration,
|
|
220
|
+
"halt": self.halt,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CuelistElement":
|
|
225
|
+
# Handle migration from "N/A" to "Next"
|
|
226
|
+
next_value = data.get("next")
|
|
227
|
+
if next_value == "N/A":
|
|
228
|
+
next_value = "Next"
|
|
229
|
+
|
|
230
|
+
return cls(
|
|
231
|
+
ms_fadeout=data.get("ms_fadeout"),
|
|
232
|
+
cue_id=data.get("cue_id"),
|
|
233
|
+
ms_delay=data.get("ms_delay"),
|
|
234
|
+
next=next_value,
|
|
235
|
+
dotted_id=data.get("dotted_id"),
|
|
236
|
+
ms_fadein=data.get("ms_fadein"),
|
|
237
|
+
ms_crossfade=data.get("ms_crossfade"),
|
|
238
|
+
ms_duration=data.get("ms_duration"),
|
|
239
|
+
halt=data.get("halt"),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def to_bytes(self) -> bytes:
|
|
243
|
+
logger.debug(f"Starting serialization of CuelistElement object {self.cue_id}")
|
|
244
|
+
bytestr = bytearray()
|
|
245
|
+
num_attr = 0
|
|
246
|
+
|
|
247
|
+
num_attrs = [
|
|
248
|
+
"ms_fadeout", "cue_id", "ms_delay", "dotted_id", "ms_fadein", "ms_crossfade",
|
|
249
|
+
"ms_duration"
|
|
250
|
+
]
|
|
251
|
+
bool_attrs = ["halt"]
|
|
252
|
+
|
|
253
|
+
for attr_name, attr_value in self.__dict__.items():
|
|
254
|
+
if attr_value is not None:
|
|
255
|
+
bytestr.extend(serialise_attr_name(attr_name))
|
|
256
|
+
num_attr += 1
|
|
257
|
+
|
|
258
|
+
if attr_name == "next":
|
|
259
|
+
if attr_value == "Next" or attr_value == "N/A":
|
|
260
|
+
bytestr.extend(b'\xFF')
|
|
261
|
+
else:
|
|
262
|
+
bytestr.extend(serialise_num_value(attr_value, cc_check=True))
|
|
263
|
+
elif attr_name in num_attrs:
|
|
264
|
+
bytestr.extend(serialise_num_value(attr_value, cc_check=True))
|
|
265
|
+
elif attr_name in bool_attrs:
|
|
266
|
+
bytestr.extend(serialise_bool_value(attr_value))
|
|
267
|
+
|
|
268
|
+
bytestr[0:0] = serialise_num_attr(num_attr)
|
|
269
|
+
logger.info("CuelistElement object serialised")
|
|
270
|
+
logger.debug(bytestr)
|
|
271
|
+
return bytes(bytestr)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Dict, List, Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from lightshark_parser.serialisers.attribute_serialisers import *
|
|
4
|
+
from lightshark_parser.utils.logger import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Version:
|
|
10
|
+
subversion: int = None
|
|
11
|
+
version: int = None
|
|
12
|
+
autoload: bool = None
|
|
13
|
+
software: str = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return f"Version(subversion={self.subversion!r}, version={self.version!r}, autoload={self.autoload!r}, software={self.software!r})"
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"subversion": self.subversion,
|
|
22
|
+
"version": self.version,
|
|
23
|
+
"autoload": self.autoload,
|
|
24
|
+
"software": self.software,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Version":
|
|
29
|
+
return cls(
|
|
30
|
+
subversion=data.get("subversion"),
|
|
31
|
+
version=data.get("version"),
|
|
32
|
+
autoload=data.get("autoload"),
|
|
33
|
+
software=data.get("software"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def to_bytes(self):
|
|
37
|
+
logger.debug(f"Starting serialization of Version object")
|
|
38
|
+
bytestr = bytearray(serialise_section_header("version"))
|
|
39
|
+
|
|
40
|
+
content = bytearray()
|
|
41
|
+
num_attr = 0
|
|
42
|
+
|
|
43
|
+
num_attrs = ["subversion", "version"]
|
|
44
|
+
bool_attrs = ["autoload"]
|
|
45
|
+
string_attrs = ["software"]
|
|
46
|
+
|
|
47
|
+
for attr_name, value in self.__dict__.items():
|
|
48
|
+
if value is not None:
|
|
49
|
+
content.extend(serialise_attr_name(attr_name))
|
|
50
|
+
num_attr += 1
|
|
51
|
+
|
|
52
|
+
if attr_name in num_attrs:
|
|
53
|
+
content.extend(serialise_num_value(value))
|
|
54
|
+
elif attr_name in bool_attrs:
|
|
55
|
+
content.extend(serialise_bool_value(value))
|
|
56
|
+
elif attr_name in string_attrs:
|
|
57
|
+
content.extend(serialise_str_value(value))
|
|
58
|
+
|
|
59
|
+
content[0:0] = serialise_num_attr(num_attr)
|
|
60
|
+
|
|
61
|
+
bytestr.extend(serialise_content_length(content))
|
|
62
|
+
bytestr.extend(content)
|
|
63
|
+
logger.info("FileInfo version object serialised")
|
|
64
|
+
logger.debug(bytestr)
|
|
65
|
+
return bytes(bytestr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FileInfo:
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __init__(self, version: Optional[Version] = None):
|
|
72
|
+
self.version = version if version is not None else self.Version()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __repr__(self):
|
|
76
|
+
return f"FileInfo(version={self.version!r})"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
81
|
+
return {"version": self.version.to_dict() if self.version else None}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FileInfo":
|
|
85
|
+
if data is None:
|
|
86
|
+
return None
|
|
87
|
+
version = Version.from_dict(data.get("version")) if data.get("version") else None
|
|
88
|
+
return cls(version=version)
|
|
89
|
+
|
|
90
|
+
def to_bytes(self):
|
|
91
|
+
logger.debug(f"Starting serialization of FileInfo section")
|
|
92
|
+
bytestr = bytearray(serialise_section_header("#fileinfo#"))
|
|
93
|
+
for obj in self.__dict__.values():
|
|
94
|
+
if obj is not None:
|
|
95
|
+
bytestr.extend(obj.to_bytes())
|
|
96
|
+
logger.info("FileInfo section serialised")
|
|
97
|
+
logger.debug(bytestr)
|
|
98
|
+
return bytes(bytestr)
|