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.
- lightshark_parser-1.0.0/LICENSE +21 -0
- lightshark_parser-1.0.0/PKG-INFO +81 -0
- lightshark_parser-1.0.0/README.md +71 -0
- lightshark_parser-1.0.0/lightshark_parser/__init__.py +13 -0
- lightshark_parser-1.0.0/lightshark_parser/__main__.py +6 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/__init__.py +28 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/action.py +11 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/cue.py +176 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/cuelist.py +271 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/fileinfo.py +98 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/fx.py +516 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/general.py +117 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/group.py +188 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/lightshow.py +1124 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/model.py +439 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/order.py +97 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/patch.py +140 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/playback.py +201 -0
- lightshark_parser-1.0.0/lightshark_parser/classes/user_palette.py +102 -0
- lightshark_parser-1.0.0/lightshark_parser/main.py +129 -0
- lightshark_parser-1.0.0/lightshark_parser/parsers/__init__.py +6 -0
- lightshark_parser-1.0.0/lightshark_parser/parsers/attribute_parsers.py +178 -0
- lightshark_parser-1.0.0/lightshark_parser/parsers/file_parser.py +345 -0
- lightshark_parser-1.0.0/lightshark_parser/parsers/parse_dict.py +6 -0
- lightshark_parser-1.0.0/lightshark_parser/parsers/section_parsers.py +1121 -0
- lightshark_parser-1.0.0/lightshark_parser/serialisers/__init__.py +0 -0
- lightshark_parser-1.0.0/lightshark_parser/serialisers/attribute_serialisers.py +66 -0
- lightshark_parser-1.0.0/lightshark_parser/summariser.py +587 -0
- lightshark_parser-1.0.0/lightshark_parser/utils/__init__.py +7 -0
- lightshark_parser-1.0.0/lightshark_parser/utils/custom_errors.py +9 -0
- lightshark_parser-1.0.0/lightshark_parser/utils/json_mappings.py +85 -0
- lightshark_parser-1.0.0/lightshark_parser/utils/logger.py +22 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/PKG-INFO +81 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/SOURCES.txt +57 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/dependency_links.txt +1 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/entry_points.txt +2 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/requires.txt +1 -0
- lightshark_parser-1.0.0/lightshark_parser.egg-info/top_level.txt +6 -0
- lightshark_parser-1.0.0/private_tests/analyze_fxs_channels.py +235 -0
- lightshark_parser-1.0.0/private_tests/analyze_sections.py +73 -0
- lightshark_parser-1.0.0/private_tests/analyze_show.py +316 -0
- lightshark_parser-1.0.0/private_tests/compare_lightshows.py +86 -0
- lightshark_parser-1.0.0/private_tests/test_names.py +34 -0
- lightshark_parser-1.0.0/private_tests/test_palette_edit.py +52 -0
- lightshark_parser-1.0.0/private_tests/test_parse_dict.py +35 -0
- lightshark_parser-1.0.0/private_tests/test_repr.py +14 -0
- lightshark_parser-1.0.0/pyproject.toml +25 -0
- lightshark_parser-1.0.0/setup.cfg +4 -0
- lightshark_parser-1.0.0/tests/conftest.py +293 -0
- lightshark_parser-1.0.0/tests/integration/test_real_data.py +141 -0
- lightshark_parser-1.0.0/tests/integration/test_summariser.py +71 -0
- lightshark_parser-1.0.0/tests/unit/test_cue.py +121 -0
- lightshark_parser-1.0.0/tests/unit/test_cuelist.py +131 -0
- lightshark_parser-1.0.0/tests/unit/test_group.py +102 -0
- lightshark_parser-1.0.0/tests/unit/test_lightshow.py +175 -0
- lightshark_parser-1.0.0/tests/unit/test_model.py +84 -0
- lightshark_parser-1.0.0/tests/unit/test_order.py +85 -0
- lightshark_parser-1.0.0/tests/unit/test_patch.py +91 -0
- 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,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)
|