sprocket-systems.coda.sdk 1.3.2__py3-none-any.whl → 2.0.5__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.
- coda/__init__.py +2 -31
- coda/sdk/__init__.py +27 -0
- coda/sdk/constants.py +22 -0
- coda/sdk/enums.py +270 -0
- coda/sdk/essence.py +496 -0
- coda/sdk/job.py +582 -0
- coda/sdk/preset.py +215 -0
- coda/sdk/utils.py +282 -0
- coda/sdk/workflow.py +1402 -0
- {sprocket_systems_coda_sdk-1.3.2.dist-info → sprocket_systems_coda_sdk-2.0.5.dist-info}/METADATA +4 -3
- sprocket_systems_coda_sdk-2.0.5.dist-info/RECORD +15 -0
- {sprocket_systems_coda_sdk-1.3.2.dist-info → sprocket_systems_coda_sdk-2.0.5.dist-info}/WHEEL +1 -1
- coda/sdk.py +0 -1646
- sprocket_systems_coda_sdk-1.3.2.dist-info/RECORD +0 -8
- {sprocket_systems_coda_sdk-1.3.2.dist-info → sprocket_systems_coda_sdk-2.0.5.dist-info}/entry_points.txt +0 -0
- {sprocket_systems_coda_sdk-1.3.2.dist-info → sprocket_systems_coda_sdk-2.0.5.dist-info}/licenses/LICENSE +0 -0
coda/sdk/preset.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from typing import ClassVar, Dict, Any, List
|
|
5
|
+
from .enums import PresetType
|
|
6
|
+
from .utils import make_request, validate_group_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Preset:
|
|
10
|
+
"""A client for interacting with Coda's preset API endpoints.
|
|
11
|
+
|
|
12
|
+
This class provides methods to create, update, and retrieve various
|
|
13
|
+
types of presets, such as those for loudness, naming conventions, and
|
|
14
|
+
encoding profiles.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
routes (ClassVar[Dict[PresetType, str]]): A mapping of preset types
|
|
18
|
+
to their corresponding API endpoint routes.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
group_path = "groups/:group_id/"
|
|
23
|
+
|
|
24
|
+
routes: ClassVar[Dict[PresetType, str]] = {
|
|
25
|
+
PresetType.JOBS: group_path + "jobs",
|
|
26
|
+
PresetType.WORKFLOWS: group_path + "workflows",
|
|
27
|
+
PresetType.LOUDNESS: group_path + "presets/loudness",
|
|
28
|
+
PresetType.TIMECODE: group_path + "presets/timecode",
|
|
29
|
+
PresetType.GROUPS: "groups",
|
|
30
|
+
PresetType.NAMING: "naming-conventions",
|
|
31
|
+
PresetType.DOLBY: "presets/encoding/dolby",
|
|
32
|
+
PresetType.DTS: "presets/encoding/dts",
|
|
33
|
+
PresetType.SUPER_SESSION: "presets/super-session",
|
|
34
|
+
PresetType.IO_LOCATIONS: "io-locations",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __init__(self, preset_type: PresetType, value: dict[str, Any]) -> None:
|
|
38
|
+
"""Initialize the Preset object.
|
|
39
|
+
|
|
40
|
+
This constructor prepares a preset object that can be sent to the Coda
|
|
41
|
+
API to either create a new preset or update an existing one.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
preset_type (PresetType): The type of preset being handled (e.g.,
|
|
45
|
+
LOUDNESS, NAMING).
|
|
46
|
+
value (dict[str, Any]): The dictionary containing the preset's
|
|
47
|
+
definition and name.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
preset (PresetType): Stores the type of the preset.
|
|
51
|
+
value (dict[str, Any]): Stores the definition payload for the preset.
|
|
52
|
+
group_id (None): An initialized placeholder for the group ID.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
self.preset = preset_type
|
|
56
|
+
self.value = value
|
|
57
|
+
self.group_id = None
|
|
58
|
+
|
|
59
|
+
def register(self) -> requests.Response:
|
|
60
|
+
"""Register (creates or updates) a preset in Coda.
|
|
61
|
+
|
|
62
|
+
If a preset with the same name already exists, it will be updated.
|
|
63
|
+
Otherwise, a new preset will be created.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If the preset type is invalid, or if a preset name
|
|
67
|
+
is ambiguous (matches multiple existing presets).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
requests.Response: The Response object from the `requests` library.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
if self.preset not in Preset.routes:
|
|
74
|
+
raise ValueError(f"Invalid preset type provided: {self.preset}")
|
|
75
|
+
|
|
76
|
+
presets = Preset.get_presets(self.preset)
|
|
77
|
+
found_id = None
|
|
78
|
+
if presets and len(presets) > 0:
|
|
79
|
+
pf = [p for p in presets if p["name"] == self.value["name"]]
|
|
80
|
+
if len(pf) > 1:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Found multiple presets of type '{self.preset.value}' with the name '{self.value['name']}'. "
|
|
83
|
+
"Preset names must be unique."
|
|
84
|
+
)
|
|
85
|
+
if len(pf) == 1:
|
|
86
|
+
# Map preset type to its specific ID key
|
|
87
|
+
key_map = {
|
|
88
|
+
PresetType.DOLBY: "encoding_preset_id",
|
|
89
|
+
PresetType.DTS: "encoding_preset_id",
|
|
90
|
+
PresetType.LOUDNESS: "loudness_preset_id",
|
|
91
|
+
PresetType.TIMECODE: "timecode_preset_id",
|
|
92
|
+
PresetType.NAMING: "id",
|
|
93
|
+
PresetType.SUPER_SESSION: "super_session_preset_id",
|
|
94
|
+
PresetType.GROUPS: "group_id",
|
|
95
|
+
}
|
|
96
|
+
id_key = key_map.get(self.preset)
|
|
97
|
+
if id_key:
|
|
98
|
+
found_id = pf[0][id_key]
|
|
99
|
+
|
|
100
|
+
route = Preset.routes[self.preset]
|
|
101
|
+
if ":group_id" in route:
|
|
102
|
+
route = route.replace(":group_id", str(validate_group_id()))
|
|
103
|
+
|
|
104
|
+
if not found_id:
|
|
105
|
+
# Add preset with that name for the first time
|
|
106
|
+
print(f"creating new preset {self.value['name']}", file=sys.stderr)
|
|
107
|
+
ret = make_request(requests.post, f"/interface/v2/{route}", self.value)
|
|
108
|
+
else:
|
|
109
|
+
request_type = requests.patch
|
|
110
|
+
if self.preset in [PresetType.TIMECODE, PresetType.LOUDNESS]:
|
|
111
|
+
request_type = requests.put
|
|
112
|
+
|
|
113
|
+
# Update found preset
|
|
114
|
+
print(f"updating preset {self.value['name']}, id={found_id}", file=sys.stderr)
|
|
115
|
+
ret = make_request(request_type, f"/interface/v2/{route}/{found_id}", self.value)
|
|
116
|
+
|
|
117
|
+
return ret
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def get_presets(preset_type: PresetType) -> List[dict]:
|
|
121
|
+
"""Fetch a list of all existing presets of a specific type.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
preset_type (PresetType): The type of presets to retrieve from Coda.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If the API call returns an error, indicating the presets
|
|
128
|
+
could not be fetched.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List[dict]: A list of dictionaries, where each dictionary represents a preset.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
group_id = ""
|
|
135
|
+
if preset_type in (PresetType.LOUDNESS, PresetType.TIMECODE, PresetType.JOBS, PresetType.WORKFLOWS):
|
|
136
|
+
group_id = validate_group_id()
|
|
137
|
+
|
|
138
|
+
route = Preset.routes[preset_type].replace(":group_id", str(group_id))
|
|
139
|
+
ret = make_request(requests.get, f"/interface/v2/{route}")
|
|
140
|
+
j = ret.json()
|
|
141
|
+
if "error" in j:
|
|
142
|
+
raise ValueError(f"Unable to find preset '{preset_type}': {j}")
|
|
143
|
+
return j
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def get_group_id_by_name(group_name: str) -> str:
|
|
147
|
+
"""Get a group ID by its name.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
group_name (str): The name of the group to search for.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
str: The group ID (group_id field).
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If no group with the given name is found, or if multiple groups match.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
groups = Preset.get_presets(PresetType.GROUPS)
|
|
160
|
+
matches = [g["group_id"] for g in groups if g["name"] == group_name]
|
|
161
|
+
|
|
162
|
+
if len(matches) == 0:
|
|
163
|
+
raise ValueError(f"No group found with name '{group_name}'")
|
|
164
|
+
if len(matches) > 1:
|
|
165
|
+
raise ValueError(f"Multiple groups found with name '{group_name}': {matches}")
|
|
166
|
+
|
|
167
|
+
return matches[0]
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def get_io_location_id_by_name(io_location_name: str) -> str:
|
|
171
|
+
"""Get an IO Location ID by its name.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
io_location_name (str): The name of the IO location to search for.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
str: The IO location ID (id field).
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValueError: If no IO location with the given name is found, or if multiple match.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
io_locations = Preset.get_presets(PresetType.IO_LOCATIONS)
|
|
184
|
+
matches = [loc["id"] for loc in io_locations if loc["name"] == io_location_name]
|
|
185
|
+
|
|
186
|
+
if len(matches) == 0:
|
|
187
|
+
raise ValueError(f"No IO location found with name '{io_location_name}'")
|
|
188
|
+
if len(matches) > 1:
|
|
189
|
+
raise ValueError(f"Multiple IO locations found with name '{io_location_name}': {matches}")
|
|
190
|
+
|
|
191
|
+
return matches[0]
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def get_workflow_by_name(workflow_name: str) -> dict:
|
|
195
|
+
"""Get a workflow by its name.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
workflow_name (str): The name of the workflow to search for.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
dict: The complete workflow object.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If no workflow with the given name is found, or if multiple match.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
workflows = Preset.get_presets(PresetType.WORKFLOWS)
|
|
208
|
+
matches = [w for w in workflows if w["name"] == workflow_name]
|
|
209
|
+
|
|
210
|
+
if len(matches) == 0:
|
|
211
|
+
raise ValueError(f"No workflow found with name '{workflow_name}'")
|
|
212
|
+
if len(matches) > 1:
|
|
213
|
+
raise ValueError(f"Multiple workflows found with name '{workflow_name}': {[w['id'] for w in matches]}")
|
|
214
|
+
|
|
215
|
+
return matches[0]
|
coda/sdk/utils.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import re
|
|
4
|
+
import urllib3
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, List, Dict, Any, Callable
|
|
7
|
+
|
|
8
|
+
from .constants import (
|
|
9
|
+
ENV_CODA_API_GROUP_ID,
|
|
10
|
+
ENV_CODA_API_URL,
|
|
11
|
+
ENV_CODA_API_TOKEN,
|
|
12
|
+
ENV_CODA_API_INSECURE_SKIP_VERIFY,
|
|
13
|
+
DEFAULT_API_URL,
|
|
14
|
+
INSECURE_SKIP_VERIFY_VALUES,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..tc_tools import (
|
|
19
|
+
time_seconds_to_vid_frames,
|
|
20
|
+
vid_frames_to_tc,
|
|
21
|
+
tc_to_time_seconds,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_CHANNEL_CONFIGURATIONS: dict = {
|
|
25
|
+
"MONO": ["C"],
|
|
26
|
+
"2.0": ["L", "R"],
|
|
27
|
+
"3.0": ["L", "C", "R"],
|
|
28
|
+
"LCRS": ["L", "C", "R", "S"],
|
|
29
|
+
"5.0": ["L", "C", "R", "Ls", "Rs"],
|
|
30
|
+
"5.0.2": ["L", "C", "R", "Ls", "Rs", "Ltm", "Rtm"],
|
|
31
|
+
"7.0": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr"],
|
|
32
|
+
"7.1": ["L", "R", "C", "LFE", "Lss", "Rss", "Lsr", "Rsr"],
|
|
33
|
+
"5.1": ["L", "R", "C", "LFE", "Ls", "Rs"],
|
|
34
|
+
"5.1.2": ["L", "C", "R", "Ls", "Rs", "Ltm", "Rtm", "LFE"],
|
|
35
|
+
"5.0.4": ["L", "C", "R", "Ls", "Rs", "Ltf", "Rtf", "Ltr", "Rtr"],
|
|
36
|
+
"7.0.2": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Lts", "Rts"],
|
|
37
|
+
"5.1.4": ["L", "C", "R", "Ls", "Rs", "Ltf", "Rtf", "Ltr", "Rtr", "LFE"],
|
|
38
|
+
"7.1.2": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Lts", "Rts", "LFE"],
|
|
39
|
+
"7.0.4": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltr", "Rtr"],
|
|
40
|
+
"7.1.4": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltr", "Rtr", "LFE"],
|
|
41
|
+
"7.0.6": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr"],
|
|
42
|
+
"9.0.4": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltr", "Rtr", "Lw", "Rw"],
|
|
43
|
+
"7.1.6": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr", "LFE"],
|
|
44
|
+
"9.1.4": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltr", "Rtr", "Lw", "Rw", "LFE"],
|
|
45
|
+
"9.0.6": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr", "Lw", "Rw"],
|
|
46
|
+
"9.1.6": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltm", "Rtm", "Ltr", "Rtr", "Lw", "Rw", "LFE"],
|
|
47
|
+
"IMAX5": ["L", "C", "R", "Ls", "Rs"],
|
|
48
|
+
"IMAX6": ["L", "C", "R", "Ls", "Rs", "Ctf"],
|
|
49
|
+
"IMAX12": ["L", "C", "R", "Lss", "Rss", "Lsr", "Rsr", "Ltf", "Rtf", "Ltr", "Rtr", "Ctf"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_channels(channel_format: str) -> List[str] | None:
|
|
54
|
+
"""Get the standard channel labels for a given audio format.
|
|
55
|
+
|
|
56
|
+
This function looks up a channel format string (e.g., "5.1") in a
|
|
57
|
+
predefined dictionary and returns the corresponding list of standard
|
|
58
|
+
channel labels (e.g., ["L", "R", "C", "LFE", "Ls", "Rs"]).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
channel_format (str): The audio format (e.g., "5.1", "7.1").
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List[str] | None: A list of channel labels, or None if the format is unknown.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
return _CHANNEL_CONFIGURATIONS.get(channel_format)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def user_info() -> str:
|
|
71
|
+
"""Retrieve information about the authenticated user.
|
|
72
|
+
|
|
73
|
+
Makes an authenticated GET request to the Coda API's '/interface/v2/users/me'
|
|
74
|
+
endpoint to fetch details for the current user, determined by the API token.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: The JSON response from the API as a string.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
ret = make_request(requests.get, "/interface/v2/users/me")
|
|
81
|
+
return ret.json()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_group_id() -> str:
|
|
85
|
+
"""Get the Coda Group ID from environment variables.
|
|
86
|
+
|
|
87
|
+
Retrieves the Coda Group ID from the 'CODA_API_GROUP_ID' environment
|
|
88
|
+
variable. This ID is required for most API operations that are scoped
|
|
89
|
+
to a specific group.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If the 'CODA_API_GROUP_ID' environment variable is not set.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: The Coda Group ID.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
group_id = os.getenv(ENV_CODA_API_GROUP_ID)
|
|
99
|
+
if group_id is not None:
|
|
100
|
+
return group_id
|
|
101
|
+
raise ValueError("Error: CODA_API_GROUP_ID is not set.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def make_request(
|
|
105
|
+
func: Callable[..., requests.Response],
|
|
106
|
+
route: str,
|
|
107
|
+
payload: Dict[str, Any] | None = None,
|
|
108
|
+
) -> requests.Response:
|
|
109
|
+
"""Make an authenticated request to the Coda API.
|
|
110
|
+
|
|
111
|
+
This is a general-purpose helper function for interacting with the Coda API.
|
|
112
|
+
It constructs the full request URL, adds the necessary authentication
|
|
113
|
+
headers, and executes the request using the provided function (e.g.,
|
|
114
|
+
requests.get, requests.post).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
func (Callable[..., requests.Response]): The requests function to call
|
|
118
|
+
(e.g., requests.get, requests.post, requests.put).
|
|
119
|
+
route (str): The API endpoint route (e.g., "/interface/v2/users/me").
|
|
120
|
+
payload (Dict[str, Any], optional): A JSON serializable dictionary to
|
|
121
|
+
be sent as the request body. Defaults to None.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If the 'CODA_API_TOKEN' environment variable is not set.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
requests.Response: The Response object from the `requests` library.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
url = os.getenv(ENV_CODA_API_URL, DEFAULT_API_URL)
|
|
131
|
+
url += route
|
|
132
|
+
token = os.getenv(ENV_CODA_API_TOKEN)
|
|
133
|
+
if token:
|
|
134
|
+
verify = True
|
|
135
|
+
if os.getenv(ENV_CODA_API_INSECURE_SKIP_VERIFY, '').lower() in INSECURE_SKIP_VERIFY_VALUES:
|
|
136
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
137
|
+
verify = False
|
|
138
|
+
auth = {"Authorization": f"Bearer {token}"}
|
|
139
|
+
return func(url, json=payload, headers=auth, verify=verify)
|
|
140
|
+
raise ValueError("Error: CODA_API_TOKEN is not set.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def timing_info(
|
|
144
|
+
inputs: Dict[str, Any],
|
|
145
|
+
venue: str | None = None,
|
|
146
|
+
fps: str | None = None,
|
|
147
|
+
ffoa: str | None = None,
|
|
148
|
+
lfoa: str | None = None,
|
|
149
|
+
start_time: str | None = None,
|
|
150
|
+
) -> Dict[str, Any] | None:
|
|
151
|
+
"""Calculate various timing-related values for a set of inputs.
|
|
152
|
+
|
|
153
|
+
This function processes a job's input definition to extract or calculate
|
|
154
|
+
key timing metrics. It determines start time, duration, frame rate, and other
|
|
155
|
+
values from the source essence definitions. It can also accept overrides for
|
|
156
|
+
these values. The results are returned in a dictionary, including both
|
|
157
|
+
second-based and timecode-based representations.
|
|
158
|
+
|
|
159
|
+
Note:
|
|
160
|
+
This function dynamically imports from `..tc_tools` for timecode
|
|
161
|
+
conversions.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
inputs (Dict[str, Any]): The workflow input dictionary, typically containing
|
|
165
|
+
keys like 'sources', 'venue', and 'source_frame_rate'.
|
|
166
|
+
venue (str, optional): Overrides the venue specified in the inputs. Defaults to None.
|
|
167
|
+
fps (str, optional): Overrides the frame rate specified in the inputs. Defaults to None.
|
|
168
|
+
ffoa (str, optional): Overrides the FFOA timecode specified in the inputs. Defaults to None.
|
|
169
|
+
lfoa (str, optional): Overrides the LFOA timecode specified in the inputs. Defaults to None.
|
|
170
|
+
start_time (str, optional): Overrides the start time. Not currently used in the function.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict[str, Any] | None: A dictionary containing calculated timing information,
|
|
174
|
+
or None if the input dictionary is empty.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
from ..tc_tools import (
|
|
178
|
+
time_seconds_to_vid_frames,
|
|
179
|
+
vid_frames_to_tc,
|
|
180
|
+
tc_to_time_seconds,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not inputs:
|
|
184
|
+
return None
|
|
185
|
+
tinfo = {}
|
|
186
|
+
if venue:
|
|
187
|
+
tinfo["venue"] = venue
|
|
188
|
+
else:
|
|
189
|
+
tinfo["venue"] = inputs["venue"]
|
|
190
|
+
if not fps:
|
|
191
|
+
tinfo["source_frame_rate"] = inputs["source_frame_rate"]
|
|
192
|
+
fps = tinfo["source_frame_rate"]
|
|
193
|
+
else:
|
|
194
|
+
tinfo["source_frame_rate"] = fps
|
|
195
|
+
if not ffoa:
|
|
196
|
+
tinfo["ffoa_timecode"] = inputs["ffoa_timecode"]
|
|
197
|
+
else:
|
|
198
|
+
tinfo["ffoa_timecode"] = ffoa
|
|
199
|
+
if not lfoa:
|
|
200
|
+
tinfo["lfoa_timecode"] = inputs["lfoa_timecode"]
|
|
201
|
+
else:
|
|
202
|
+
tinfo["lfoa_timecode"] = lfoa
|
|
203
|
+
startt = -1
|
|
204
|
+
srate = -1
|
|
205
|
+
filelen = -1
|
|
206
|
+
sources = inputs["sources"]
|
|
207
|
+
for s in sources:
|
|
208
|
+
definition = s.get("definition", {})
|
|
209
|
+
if definition:
|
|
210
|
+
if "programme_timing" in definition:
|
|
211
|
+
srate = definition["sample_rate"]
|
|
212
|
+
filelen = definition["frames"] / srate
|
|
213
|
+
startt = (
|
|
214
|
+
definition["programme_timing"][
|
|
215
|
+
"audio_programme_start_time_reference"
|
|
216
|
+
]
|
|
217
|
+
/ srate
|
|
218
|
+
)
|
|
219
|
+
break
|
|
220
|
+
if "resources" in definition:
|
|
221
|
+
startt = (
|
|
222
|
+
definition["resources"][0]["bext_time_reference"]
|
|
223
|
+
/ definition["resources"][0]["sample_rate"]
|
|
224
|
+
)
|
|
225
|
+
srate = definition["resources"][0]["sample_rate"]
|
|
226
|
+
filelen = definition["resources"][0]["frames"] / srate
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
tinfo["start_time_sec"] = startt
|
|
230
|
+
tinfo["file_duration_sec"] = filelen
|
|
231
|
+
tinfo["file_duration"] = ""
|
|
232
|
+
if fps != "":
|
|
233
|
+
tinfo["file_duration"] = vid_frames_to_tc(
|
|
234
|
+
time_seconds_to_vid_frames(filelen, fps), fps
|
|
235
|
+
)
|
|
236
|
+
tinfo["start_timecode"] = ""
|
|
237
|
+
if fps != "":
|
|
238
|
+
tinfo["start_timecode"] = vid_frames_to_tc(
|
|
239
|
+
time_seconds_to_vid_frames(startt, fps), fps
|
|
240
|
+
)
|
|
241
|
+
tinfo["end_timecode"] = ""
|
|
242
|
+
if fps != "":
|
|
243
|
+
tinfo["end_timecode"] = vid_frames_to_tc(
|
|
244
|
+
time_seconds_to_vid_frames(startt + filelen, fps) - 1, fps
|
|
245
|
+
)
|
|
246
|
+
tinfo["ffoa_seconds"] = -1
|
|
247
|
+
tinfo["lfoa_seconds"] = -1
|
|
248
|
+
if fps != "":
|
|
249
|
+
tinfo["ffoa_seconds"] = tc_to_time_seconds(tinfo["ffoa_timecode"], fps)
|
|
250
|
+
tinfo["lfoa_seconds"] = tc_to_time_seconds(tinfo["lfoa_timecode"], fps)
|
|
251
|
+
tinfo["sample_rate"] = srate
|
|
252
|
+
|
|
253
|
+
return tinfo
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def is_key_value_comma_string(s: str) -> bool:
|
|
257
|
+
"""Validate a string formatted as KEY=VALUE,KEY=VALUE,...
|
|
258
|
+
|
|
259
|
+
The pattern expects:
|
|
260
|
+
- Uppercase letters, numbers, and underscores for KEYS.
|
|
261
|
+
- Letters, numbers, hyphens, and underscores for VALUES.
|
|
262
|
+
- Key-value pairs separated by a single comma.
|
|
263
|
+
- No leading/trailing commas, and no empty keys or values.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
bool: true if matched
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
# Regex breakdown:
|
|
270
|
+
# ^ # Start of the string
|
|
271
|
+
# ([A-Z0-9_]+) # Group 1: KEY - one or more uppercase letters, numbers, or underscores
|
|
272
|
+
# = # Literal equals sign
|
|
273
|
+
# ([a-zA-Z0-9_-]+) # Group 2: VALUE - one or more letters, numbers, hyphens, or underscores
|
|
274
|
+
# ( # Start of non-capturing group for subsequent pairs
|
|
275
|
+
# ,[A-Z0-9_]+= # Comma followed by KEY=
|
|
276
|
+
# [a-zA-Z0-9_-]+ # VALUE
|
|
277
|
+
# )* # Zero or more of the subsequent pairs
|
|
278
|
+
# $ # End of the string
|
|
279
|
+
|
|
280
|
+
pattern = r"^([A-Z0-9_]+=[a-zA-Z0-9_-]+)(,[A-Z0-9_]+=[a-zA-Z0-9_-]+)*$"
|
|
281
|
+
|
|
282
|
+
return re.fullmatch(pattern, s) is not None
|