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.
Files changed (54) hide show
  1. lightshark_parser/__init__.py +13 -0
  2. lightshark_parser/__main__.py +6 -0
  3. lightshark_parser/classes/__init__.py +28 -0
  4. lightshark_parser/classes/action.py +11 -0
  5. lightshark_parser/classes/cue.py +176 -0
  6. lightshark_parser/classes/cuelist.py +271 -0
  7. lightshark_parser/classes/fileinfo.py +98 -0
  8. lightshark_parser/classes/fx.py +516 -0
  9. lightshark_parser/classes/general.py +117 -0
  10. lightshark_parser/classes/group.py +188 -0
  11. lightshark_parser/classes/lightshow.py +1124 -0
  12. lightshark_parser/classes/model.py +439 -0
  13. lightshark_parser/classes/order.py +97 -0
  14. lightshark_parser/classes/patch.py +140 -0
  15. lightshark_parser/classes/playback.py +201 -0
  16. lightshark_parser/classes/user_palette.py +102 -0
  17. lightshark_parser/main.py +129 -0
  18. lightshark_parser/parsers/__init__.py +6 -0
  19. lightshark_parser/parsers/attribute_parsers.py +178 -0
  20. lightshark_parser/parsers/file_parser.py +345 -0
  21. lightshark_parser/parsers/parse_dict.py +6 -0
  22. lightshark_parser/parsers/section_parsers.py +1121 -0
  23. lightshark_parser/serialisers/__init__.py +0 -0
  24. lightshark_parser/serialisers/attribute_serialisers.py +66 -0
  25. lightshark_parser/summariser.py +587 -0
  26. lightshark_parser/utils/__init__.py +7 -0
  27. lightshark_parser/utils/custom_errors.py +9 -0
  28. lightshark_parser/utils/json_mappings.py +85 -0
  29. lightshark_parser/utils/logger.py +22 -0
  30. lightshark_parser-1.0.0.dist-info/METADATA +81 -0
  31. lightshark_parser-1.0.0.dist-info/RECORD +54 -0
  32. lightshark_parser-1.0.0.dist-info/WHEEL +5 -0
  33. lightshark_parser-1.0.0.dist-info/entry_points.txt +2 -0
  34. lightshark_parser-1.0.0.dist-info/licenses/LICENSE +21 -0
  35. lightshark_parser-1.0.0.dist-info/top_level.txt +3 -0
  36. private_tests/analyze_fxs_channels.py +235 -0
  37. private_tests/analyze_sections.py +73 -0
  38. private_tests/analyze_show.py +316 -0
  39. private_tests/compare_lightshows.py +86 -0
  40. private_tests/test_names.py +34 -0
  41. private_tests/test_palette_edit.py +52 -0
  42. private_tests/test_parse_dict.py +35 -0
  43. private_tests/test_repr.py +14 -0
  44. tests/conftest.py +293 -0
  45. tests/integration/test_real_data.py +141 -0
  46. tests/integration/test_summariser.py +71 -0
  47. tests/unit/test_cue.py +121 -0
  48. tests/unit/test_cuelist.py +131 -0
  49. tests/unit/test_group.py +102 -0
  50. tests/unit/test_lightshow.py +175 -0
  51. tests/unit/test_model.py +84 -0
  52. tests/unit/test_order.py +85 -0
  53. tests/unit/test_patch.py +91 -0
  54. 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,6 @@
1
+ try:
2
+ from main import main
3
+ except (NameError, FileNotFoundError, ModuleNotFoundError):
4
+ from lightshark_parser.main import main
5
+
6
+ main()
@@ -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,11 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Any
3
+
4
+ class Action:
5
+ # NOT IMPLEMENTED #
6
+ def __init__():
7
+ pass
8
+
9
+ @classmethod
10
+ def from_dict(cls, data: Dict[str, Any]) -> "Action":
11
+ return cls()
@@ -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)