lightshark-parser 1.0.0__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.
Files changed (59) hide show
  1. lightshark_parser-1.0.0/LICENSE +21 -0
  2. lightshark_parser-1.0.0/PKG-INFO +81 -0
  3. lightshark_parser-1.0.0/README.md +71 -0
  4. lightshark_parser-1.0.0/lightshark_parser/__init__.py +13 -0
  5. lightshark_parser-1.0.0/lightshark_parser/__main__.py +6 -0
  6. lightshark_parser-1.0.0/lightshark_parser/classes/__init__.py +28 -0
  7. lightshark_parser-1.0.0/lightshark_parser/classes/action.py +11 -0
  8. lightshark_parser-1.0.0/lightshark_parser/classes/cue.py +176 -0
  9. lightshark_parser-1.0.0/lightshark_parser/classes/cuelist.py +271 -0
  10. lightshark_parser-1.0.0/lightshark_parser/classes/fileinfo.py +98 -0
  11. lightshark_parser-1.0.0/lightshark_parser/classes/fx.py +516 -0
  12. lightshark_parser-1.0.0/lightshark_parser/classes/general.py +117 -0
  13. lightshark_parser-1.0.0/lightshark_parser/classes/group.py +188 -0
  14. lightshark_parser-1.0.0/lightshark_parser/classes/lightshow.py +1124 -0
  15. lightshark_parser-1.0.0/lightshark_parser/classes/model.py +439 -0
  16. lightshark_parser-1.0.0/lightshark_parser/classes/order.py +97 -0
  17. lightshark_parser-1.0.0/lightshark_parser/classes/patch.py +140 -0
  18. lightshark_parser-1.0.0/lightshark_parser/classes/playback.py +201 -0
  19. lightshark_parser-1.0.0/lightshark_parser/classes/user_palette.py +102 -0
  20. lightshark_parser-1.0.0/lightshark_parser/main.py +129 -0
  21. lightshark_parser-1.0.0/lightshark_parser/parsers/__init__.py +6 -0
  22. lightshark_parser-1.0.0/lightshark_parser/parsers/attribute_parsers.py +178 -0
  23. lightshark_parser-1.0.0/lightshark_parser/parsers/file_parser.py +345 -0
  24. lightshark_parser-1.0.0/lightshark_parser/parsers/parse_dict.py +6 -0
  25. lightshark_parser-1.0.0/lightshark_parser/parsers/section_parsers.py +1121 -0
  26. lightshark_parser-1.0.0/lightshark_parser/serialisers/__init__.py +0 -0
  27. lightshark_parser-1.0.0/lightshark_parser/serialisers/attribute_serialisers.py +66 -0
  28. lightshark_parser-1.0.0/lightshark_parser/summariser.py +587 -0
  29. lightshark_parser-1.0.0/lightshark_parser/utils/__init__.py +7 -0
  30. lightshark_parser-1.0.0/lightshark_parser/utils/custom_errors.py +9 -0
  31. lightshark_parser-1.0.0/lightshark_parser/utils/json_mappings.py +85 -0
  32. lightshark_parser-1.0.0/lightshark_parser/utils/logger.py +22 -0
  33. lightshark_parser-1.0.0/lightshark_parser.egg-info/PKG-INFO +81 -0
  34. lightshark_parser-1.0.0/lightshark_parser.egg-info/SOURCES.txt +57 -0
  35. lightshark_parser-1.0.0/lightshark_parser.egg-info/dependency_links.txt +1 -0
  36. lightshark_parser-1.0.0/lightshark_parser.egg-info/entry_points.txt +2 -0
  37. lightshark_parser-1.0.0/lightshark_parser.egg-info/requires.txt +1 -0
  38. lightshark_parser-1.0.0/lightshark_parser.egg-info/top_level.txt +6 -0
  39. lightshark_parser-1.0.0/private_tests/analyze_fxs_channels.py +235 -0
  40. lightshark_parser-1.0.0/private_tests/analyze_sections.py +73 -0
  41. lightshark_parser-1.0.0/private_tests/analyze_show.py +316 -0
  42. lightshark_parser-1.0.0/private_tests/compare_lightshows.py +86 -0
  43. lightshark_parser-1.0.0/private_tests/test_names.py +34 -0
  44. lightshark_parser-1.0.0/private_tests/test_palette_edit.py +52 -0
  45. lightshark_parser-1.0.0/private_tests/test_parse_dict.py +35 -0
  46. lightshark_parser-1.0.0/private_tests/test_repr.py +14 -0
  47. lightshark_parser-1.0.0/pyproject.toml +25 -0
  48. lightshark_parser-1.0.0/setup.cfg +4 -0
  49. lightshark_parser-1.0.0/tests/conftest.py +293 -0
  50. lightshark_parser-1.0.0/tests/integration/test_real_data.py +141 -0
  51. lightshark_parser-1.0.0/tests/integration/test_summariser.py +71 -0
  52. lightshark_parser-1.0.0/tests/unit/test_cue.py +121 -0
  53. lightshark_parser-1.0.0/tests/unit/test_cuelist.py +131 -0
  54. lightshark_parser-1.0.0/tests/unit/test_group.py +102 -0
  55. lightshark_parser-1.0.0/tests/unit/test_lightshow.py +175 -0
  56. lightshark_parser-1.0.0/tests/unit/test_model.py +84 -0
  57. lightshark_parser-1.0.0/tests/unit/test_order.py +85 -0
  58. lightshark_parser-1.0.0/tests/unit/test_patch.py +91 -0
  59. lightshark_parser-1.0.0/tests/unit/test_user_palette.py +106 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fenixion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: lightshark-parser
3
+ Version: 1.0.0
4
+ Summary: A parser for Lightshark show files (.lshw)
5
+ Requires-Python: >=3.7
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: orjson>=3.9.0
9
+ Dynamic: license-file
10
+
11
+ # lightshark-parser
12
+
13
+ lightshark-parser is a Python library for parsing show files used in the [Lightshark](https://lightshark.es/) software (`.lshw`).
14
+
15
+ ## Features
16
+
17
+ ### Parsing .lshw files
18
+
19
+ Extracts details from .lshw files into a `Lightshow` object. Also supports :
20
+ - Serialising the `Lightshow` object into a .json file
21
+ - Summarising important show details into a .txt file
22
+
23
+ Currently, the following details are extracted:
24
+ - Fixture models
25
+ - Patched fixtures
26
+ - Groups
27
+ - Palettes
28
+ - Cues
29
+ - Cuelists
30
+ - Playbacks
31
+ - FXs
32
+ - Some settings
33
+
34
+
35
+ ### Edit without a Lightboard!
36
+
37
+ Lightshark Parser comes with a set of functions that allow you to edit the show's attributes through code without needing to use the lightboard directly, serving as a sort of offline editor.
38
+
39
+ Can also be used to create your own custom "macros".
40
+
41
+ After editing, you can use the `to_bytes()` method to write it back into a .lshw file, following a similar format to the one used by Lightshark.
42
+
43
+
44
+ #### Disclaimers:
45
+ - This is not official in any capacity. I reverse-engineered this based off a bunch of files I had, so it is nowhere near 100% accurate to how Lightshark formats their files.
46
+ - Some details of the show are currently not supported. The current ones implemented are the most commonly used, so it should work for most purposes.
47
+
48
+
49
+
50
+ ## Installation
51
+
52
+ You can install lightshark-parser using pip:
53
+
54
+ ```bash
55
+ pip install lightshark-parser
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ Lightshark Parser can be used both from the command line and using Python code.
61
+
62
+ ### Command line
63
+
64
+ ```bash
65
+ lightshark-parser -p/s <input_file> -o <output_file>
66
+ ```
67
+
68
+ - `-p` : Parse file
69
+ - `-s` : Summarise file
70
+
71
+ If no output file is provided, output_file will default to `<input_path>.json` or `<input_path>_summarised.txt` depending on the operation selected.
72
+
73
+ ### Code
74
+
75
+ ```python
76
+ from lightshark_parser import Lightshow, parse_file_bytes
77
+
78
+
79
+
80
+ ```
81
+
@@ -0,0 +1,71 @@
1
+ # lightshark-parser
2
+
3
+ lightshark-parser is a Python library for parsing show files used in the [Lightshark](https://lightshark.es/) software (`.lshw`).
4
+
5
+ ## Features
6
+
7
+ ### Parsing .lshw files
8
+
9
+ Extracts details from .lshw files into a `Lightshow` object. Also supports :
10
+ - Serialising the `Lightshow` object into a .json file
11
+ - Summarising important show details into a .txt file
12
+
13
+ Currently, the following details are extracted:
14
+ - Fixture models
15
+ - Patched fixtures
16
+ - Groups
17
+ - Palettes
18
+ - Cues
19
+ - Cuelists
20
+ - Playbacks
21
+ - FXs
22
+ - Some settings
23
+
24
+
25
+ ### Edit without a Lightboard!
26
+
27
+ Lightshark Parser comes with a set of functions that allow you to edit the show's attributes through code without needing to use the lightboard directly, serving as a sort of offline editor.
28
+
29
+ Can also be used to create your own custom "macros".
30
+
31
+ After editing, you can use the `to_bytes()` method to write it back into a .lshw file, following a similar format to the one used by Lightshark.
32
+
33
+
34
+ #### Disclaimers:
35
+ - This is not official in any capacity. I reverse-engineered this based off a bunch of files I had, so it is nowhere near 100% accurate to how Lightshark formats their files.
36
+ - Some details of the show are currently not supported. The current ones implemented are the most commonly used, so it should work for most purposes.
37
+
38
+
39
+
40
+ ## Installation
41
+
42
+ You can install lightshark-parser using pip:
43
+
44
+ ```bash
45
+ pip install lightshark-parser
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ Lightshark Parser can be used both from the command line and using Python code.
51
+
52
+ ### Command line
53
+
54
+ ```bash
55
+ lightshark-parser -p/s <input_file> -o <output_file>
56
+ ```
57
+
58
+ - `-p` : Parse file
59
+ - `-s` : Summarise file
60
+
61
+ If no output file is provided, output_file will default to `<input_path>.json` or `<input_path>_summarised.txt` depending on the operation selected.
62
+
63
+ ### Code
64
+
65
+ ```python
66
+ from lightshark_parser import Lightshow, parse_file_bytes
67
+
68
+
69
+
70
+ ```
71
+
@@ -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)