pycmx 1.2.2__py3-none-any.whl → 1.3.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.
pycmx/__init__.py CHANGED
@@ -2,13 +2,11 @@
2
2
  """
3
3
  pycmx is a parser for CMX 3600-style EDLs.
4
4
 
5
- This module (c) 2023 Jamie Hardt. For more information on your rights to
5
+ This module (c) 2025 Jamie Hardt. For more information on your rights to
6
6
  copy and reuse this software, refer to the LICENSE file included with the
7
7
  distribution.
8
8
  """
9
9
 
10
- __version__ = '1.2.2'
11
-
12
10
  from .parse_cmx_events import parse_cmx3600
13
11
  from .transition import Transition
14
12
  from .event import Event
pycmx/channel_map.py CHANGED
@@ -4,23 +4,24 @@
4
4
  from re import (compile, match)
5
5
  from typing import Dict, Tuple, Generator
6
6
 
7
+
7
8
  class ChannelMap:
8
9
  """
9
10
  Represents a set of all the channels to which an event applies.
10
11
  """
11
12
 
12
- _chan_map : Dict[str, Tuple] = {
13
- "V" : (True, False, False),
14
- "A" : (False, True, False),
15
- "A2" : (False, False, True),
16
- "AA" : (False, True, True),
17
- "B" : (True, True, False),
18
- "AA/V" : (True, True, True),
19
- "A2/V" : (True, False, True)
20
- }
13
+ _chan_map: Dict[str, Tuple] = {
14
+ "V": (True, False, False),
15
+ "A": (False, True, False),
16
+ "A2": (False, False, True),
17
+ "AA": (False, True, True),
18
+ "B": (True, True, False),
19
+ "AA/V": (True, True, True),
20
+ "A2/V": (True, False, True)
21
+ }
21
22
 
22
23
  def __init__(self, v=False, audio_channels=set()):
23
- self._audio_channel_set = audio_channels
24
+ self._audio_channel_set = audio_channels
24
25
  self.v = v
25
26
 
26
27
  @property
@@ -46,7 +47,7 @@ class ChannelMap:
46
47
 
47
48
  @a1.setter
48
49
  def a1(self, val: bool):
49
- self.set_audio_channel(1,val)
50
+ self.set_audio_channel(1, val)
50
51
 
51
52
  @property
52
53
  def a2(self) -> bool:
@@ -55,7 +56,7 @@ class ChannelMap:
55
56
 
56
57
  @a2.setter
57
58
  def a2(self, val: bool):
58
- self.set_audio_channel(2,val)
59
+ self.set_audio_channel(2, val)
59
60
 
60
61
  @property
61
62
  def a3(self) -> bool:
@@ -64,28 +65,28 @@ class ChannelMap:
64
65
 
65
66
  @a3.setter
66
67
  def a3(self, val: bool):
67
- self.set_audio_channel(3,val)
68
-
68
+ self.set_audio_channel(3, val)
69
+
69
70
  @property
70
71
  def a4(self) -> bool:
71
72
  """True if A4 is included"""
72
73
  return self.get_audio_channel(4)
73
74
 
74
75
  @a4.setter
75
- def a4(self,val: bool):
76
- self.set_audio_channel(4,val)
76
+ def a4(self, val: bool):
77
+ self.set_audio_channel(4, val)
77
78
 
78
79
  def get_audio_channel(self, chan_num) -> bool:
79
80
  """True if chan_num is included"""
80
81
  return (chan_num in self._audio_channel_set)
81
82
 
82
- def set_audio_channel(self,chan_num, enabled: bool):
83
+ def set_audio_channel(self, chan_num, enabled: bool):
83
84
  """If enabled is true, chan_num will be included"""
84
85
  if enabled:
85
86
  self._audio_channel_set.add(chan_num)
86
87
  elif self.get_audio_channel(chan_num):
87
88
  self._audio_channel_set.remove(chan_num)
88
-
89
+
89
90
  def _append_event(self, event_str):
90
91
  alt_channel_re = compile(r'^A(\d+)')
91
92
  if event_str in self._chan_map:
@@ -96,7 +97,7 @@ class ChannelMap:
96
97
  else:
97
98
  matchresult = match(alt_channel_re, event_str)
98
99
  if matchresult:
99
- self.set_audio_channel(int( matchresult.group(1)), True )
100
+ self.set_audio_channel(int(matchresult.group(1)), True)
100
101
 
101
102
  def _append_ext(self, audio_ext):
102
103
  self.a3 = audio_ext.audio3
@@ -109,5 +110,4 @@ class ChannelMap:
109
110
  out_v = self.video | other.video
110
111
  out_a = self._audio_channel_set | other._audio_channel_set
111
112
 
112
- return ChannelMap(v=out_v,audio_channels = out_a)
113
-
113
+ return ChannelMap(v=out_v, audio_channels=out_a)
pycmx/edit.py CHANGED
@@ -7,23 +7,27 @@ from .channel_map import ChannelMap
7
7
 
8
8
  from typing import Optional
9
9
 
10
+
10
11
  class Edit:
11
12
  """
12
- An individual source-to-record operation, with a source roll, source and
13
+ An individual source-to-record operation, with a source roll, source and
13
14
  recorder timecode in and out, a transition and channels.
14
15
  """
15
- def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement, trans_name_statement = None):
16
+
17
+ def __init__(self, edit_statement, audio_ext_statement,
18
+ clip_name_statement, source_file_statement,
19
+ trans_name_statement=None):
16
20
  self.edit_statement = edit_statement
17
21
  self.audio_ext = audio_ext_statement
18
22
  self.clip_name_statement = clip_name_statement
19
23
  self.source_file_statement = source_file_statement
20
- self.trans_name_statement = trans_name_statement
24
+ self.trans_name_statement = trans_name_statement
21
25
 
22
26
  @property
23
27
  def line_number(self) -> int:
24
28
  """
25
29
  Get the line number for the "standard form" statement associated with
26
- this edit. Line numbers a zero-indexed, such that the
30
+ this edit. Line numbers a zero-indexed, such that the
27
31
  "TITLE:" record is line zero.
28
32
  """
29
33
  return self.edit_statement.line_number
@@ -35,7 +39,7 @@ class Edit:
35
39
  """
36
40
  cm = ChannelMap()
37
41
  cm._append_event(self.edit_statement.channels)
38
- if self.audio_ext != None:
42
+ if self.audio_ext is not None:
39
43
  cm._append_ext(self.audio_ext)
40
44
  return cm
41
45
 
@@ -45,10 +49,13 @@ class Edit:
45
49
  Get the :obj:`Transition` object associated with this edit.
46
50
  """
47
51
  if self.trans_name_statement:
48
- return Transition(self.edit_statement.trans, self.edit_statement.trans_op, self.trans_name_statement.name)
52
+ return Transition(self.edit_statement.trans,
53
+ self.edit_statement.trans_op,
54
+ self.trans_name_statement.name)
49
55
  else:
50
- return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None)
51
-
56
+ return Transition(self.edit_statement.trans,
57
+ self.edit_statement.trans_op, None)
58
+
52
59
  @property
53
60
  def source_in(self) -> str:
54
61
  """
@@ -116,7 +123,7 @@ class Edit:
116
123
  @property
117
124
  def clip_name(self) -> Optional[str]:
118
125
  """
119
- Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
126
+ Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
120
127
  NAME" remark on the EDL. This will return None if the information is
121
128
  not present.
122
129
  """
@@ -124,5 +131,3 @@ class Edit:
124
131
  return None
125
132
  else:
126
133
  return self.clip_name_statement.name
127
-
128
-
pycmx/edit_list.py CHANGED
@@ -1,16 +1,20 @@
1
1
  # pycmx
2
2
  # (c) 2018 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
4
+ from .parse_cmx_statements import (
5
+ StmtUnrecognized, StmtEvent, StmtSourceUMID)
5
6
  from .event import Event
6
7
  from .channel_map import ChannelMap
7
8
 
8
9
  from typing import Generator
9
10
 
11
+
10
12
  class EditList:
11
13
  """
12
- Represents an entire edit decision list as returned by :func:`~pycmx.parse_cmx3600()`.
14
+ Represents an entire edit decision list as returned by
15
+ :func:`~pycmx.parse_cmx3600()`.
13
16
  """
17
+
14
18
  def __init__(self, statements):
15
19
  self.title_statement = statements[0]
16
20
  self.event_statements = statements[1:]
@@ -20,8 +24,11 @@ class EditList:
20
24
  """
21
25
  The detected format of the EDL. Possible values are: "3600", "File32",
22
26
  "File128", and "unknown".
27
+
28
+ Adobe EDLs with more than 999 events will be reported as "3600".
23
29
  """
24
- first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
30
+ first_event = next(
31
+ (s for s in self.event_statements if type(s) is StmtEvent), None)
25
32
 
26
33
  if first_event:
27
34
  if first_event.format == 8:
@@ -34,7 +41,6 @@ class EditList:
34
41
  return 'unknown'
35
42
  else:
36
43
  return 'unknown'
37
-
38
44
 
39
45
  @property
40
46
  def channels(self) -> ChannelMap:
@@ -48,7 +54,6 @@ class EditList:
48
54
  retval = retval | edit.channels
49
55
 
50
56
  return retval
51
-
52
57
 
53
58
  @property
54
59
  def title(self) -> str:
@@ -57,27 +62,23 @@ class EditList:
57
62
  """
58
63
  return self.title_statement.title
59
64
 
60
-
61
65
  @property
62
- def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
66
+ def unrecognized_statements(self) -> Generator[StmtUnrecognized,
67
+ None, None]:
63
68
  """
64
69
  A generator for all the unrecognized statements in the list.
65
70
  """
66
71
  for s in self.event_statements:
67
72
  if type(s) is StmtUnrecognized:
68
73
  yield s
69
-
70
74
 
71
75
  @property
72
76
  def events(self) -> Generator[Event, None, None]:
73
77
  'A generator for all the events in the edit list'
74
- is_drop = None
75
78
  current_event_num = None
76
79
  event_statements = []
77
80
  for stmt in self.event_statements:
78
- if type(stmt) is StmtFCM:
79
- is_drop = stmt.drop
80
- elif type(stmt) is StmtEvent:
81
+ if type(stmt) is StmtEvent:
81
82
  if current_event_num is None:
82
83
  current_event_num = stmt.event
83
84
  event_statements.append(stmt)
@@ -101,9 +102,7 @@ class EditList:
101
102
  """
102
103
  A generator for all of the sources in the list
103
104
  """
104
-
105
+
105
106
  for stmt in self.event_statements:
106
107
  if type(stmt) is StmtSourceUMID:
107
108
  yield stmt
108
-
109
-
pycmx/event.py CHANGED
@@ -1,19 +1,22 @@
1
1
  # pycmx
2
2
  # (c) 2023 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName)
5
- from .edit import Edit
4
+ from .parse_cmx_statements import (
5
+ StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized,
6
+ StmtEffectsName)
7
+ from .edit import Edit
6
8
 
7
9
  from typing import List, Generator, Optional, Tuple, Any
8
10
 
11
+
9
12
  class Event:
10
13
  """
11
- Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same event number.
12
- """
14
+ Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
15
+ event number. """
13
16
 
14
17
  def __init__(self, statements):
15
18
  self.statements = statements
16
-
19
+
17
20
  @property
18
21
  def number(self) -> int:
19
22
  """
@@ -28,10 +31,10 @@ class Event:
28
31
  will have multiple edits when a dissolve, wipe or key transition needs
29
32
  to be performed.
30
33
  """
31
- edits_audio = list( self._statements_with_audio_ext() )
32
- clip_names = self._clip_name_statements()
33
- source_files= self._source_file_statements()
34
-
34
+ edits_audio = list(self._statements_with_audio_ext())
35
+ clip_names = self._clip_name_statements()
36
+ source_files = self._source_file_statements()
37
+
35
38
  the_zip: List[List[Any]] = [edits_audio]
36
39
 
37
40
  if len(edits_audio) == 2:
@@ -45,19 +48,19 @@ class Event:
45
48
  end_name = clip_name
46
49
 
47
50
  the_zip.append([start_name, end_name])
48
- else:
51
+ else:
49
52
  if len(edits_audio) == len(clip_names):
50
53
  the_zip.append(clip_names)
51
54
  else:
52
- the_zip.append([None] * len(edits_audio) )
55
+ the_zip.append([None] * len(edits_audio))
53
56
 
54
57
  if len(edits_audio) == len(source_files):
55
58
  the_zip.append(source_files)
56
59
  elif len(source_files) == 1:
57
- the_zip.append( source_files * len(edits_audio) )
60
+ the_zip.append(source_files * len(edits_audio))
58
61
  else:
59
- the_zip.append([None] * len(edits_audio) )
60
-
62
+ the_zip.append([None] * len(edits_audio))
63
+
61
64
  # attach trans name to last event
62
65
  try:
63
66
  trans_statement = self._trans_name_statements()[0]
@@ -65,23 +68,25 @@ class Event:
65
68
  trans_names.append(trans_statement)
66
69
  the_zip.append(trans_names)
67
70
  except IndexError:
68
- the_zip.append([None] * len(edits_audio) )
69
-
70
- return [ Edit(edit_statement=e1[0],
71
- audio_ext_statement=e1[1],
72
- clip_name_statement=n1,
73
- source_file_statement=s1,
74
- trans_name_statement=u1) for (e1,n1,s1,u1) in zip(*the_zip) ]
75
-
71
+ the_zip.append([None] * len(edits_audio))
72
+
73
+ return [Edit(edit_statement=e1[0],
74
+ audio_ext_statement=e1[1],
75
+ clip_name_statement=n1,
76
+ source_file_statement=s1,
77
+ trans_name_statement=u1)
78
+ for (e1, n1, s1, u1) in zip(*the_zip)]
79
+
76
80
  @property
77
- def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
81
+ def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
82
+ None]:
78
83
  """
79
84
  A generator for all the unrecognized statements in the event.
80
85
  """
81
86
  for s in self.statements:
82
87
  if type(s) is StmtUnrecognized:
83
88
  yield s
84
-
89
+
85
90
  def _trans_name_statements(self) -> List[StmtEffectsName]:
86
91
  return [s for s in self.statements if type(s) is StmtEffectsName]
87
92
 
@@ -90,15 +95,14 @@ class Event:
90
95
 
91
96
  def _clip_name_statements(self) -> List[StmtClipName]:
92
97
  return [s for s in self.statements if type(s) is StmtClipName]
93
-
98
+
94
99
  def _source_file_statements(self) -> List[StmtSourceFile]:
95
100
  return [s for s in self.statements if type(s) is StmtSourceFile]
96
-
97
- def _statements_with_audio_ext(self) -> Generator[Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
101
+
102
+ def _statements_with_audio_ext(self) -> Generator[
103
+ Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
98
104
  for (s1, s2) in zip(self.statements, self.statements[1:]):
99
105
  if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
100
- yield (s1,s2)
106
+ yield (s1, s2)
101
107
  elif type(s1) is StmtEvent:
102
108
  yield (s1, None)
103
-
104
-
pycmx/parse_cmx_events.py CHANGED
@@ -6,15 +6,15 @@
6
6
  from .parse_cmx_statements import (parse_cmx3600_statements)
7
7
  from .edit_list import EditList
8
8
 
9
- from typing import TextIO
9
+ from typing import TextIO
10
10
 
11
- def parse_cmx3600(f: TextIO):
11
+
12
+ def parse_cmx3600(f: TextIO) -> EditList:
12
13
  """
13
14
  Parse a CMX 3600 EDL.
14
15
 
15
- :param TextIO f: a file-like object, anything that's readlines-able.
16
+ :param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
16
17
  :returns: An :class:`pycmx.edit_list.EditList`.
17
18
  """
18
19
  statements = parse_cmx3600_statements(f)
19
20
  return EditList(statements)
20
-
@@ -2,27 +2,28 @@
2
2
  # (c) 2018 Jamie Hardt
3
3
 
4
4
  import re
5
- import sys
6
5
  from collections import namedtuple
7
- from itertools import count
8
6
  from typing import TextIO, List
9
7
 
10
8
 
11
9
  from .util import collimate
12
10
 
13
- StmtTitle = namedtuple("Title",["title","line_number"])
14
- StmtFCM = namedtuple("FCM",["drop","line_number"])
15
- StmtEvent = namedtuple("Event",["event","source","channels","trans",\
16
- "trans_op","source_in","source_out","record_in","record_out","format","line_number"])
17
- StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
18
- StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
19
- StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
20
- StmtRemark = namedtuple("Remark",["text","line_number"])
21
- StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
22
- StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
23
- StmtSplitEdit = namedtuple("SplitEdit",["video","magnitude", "line_number"])
24
- StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
25
- StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
11
+ StmtTitle = namedtuple("Title", ["title", "line_number"])
12
+ StmtFCM = namedtuple("FCM", ["drop", "line_number"])
13
+ StmtEvent = namedtuple("Event", ["event", "source", "channels", "trans",
14
+ "trans_op", "source_in", "source_out",
15
+ "record_in", "record_out", "format",
16
+ "line_number"])
17
+ StmtAudioExt = namedtuple("AudioExt", ["audio3", "audio4", "line_number"])
18
+ StmtClipName = namedtuple("ClipName", ["name", "affect", "line_number"])
19
+ StmtSourceFile = namedtuple("SourceFile", ["filename", "line_number"])
20
+ StmtRemark = namedtuple("Remark", ["text", "line_number"])
21
+ StmtEffectsName = namedtuple("EffectsName", ["name", "line_number"])
22
+ StmtSourceUMID = namedtuple("Source", ["name", "umid", "line_number"])
23
+ StmtSplitEdit = namedtuple("SplitEdit", ["video", "magnitude", "line_number"])
24
+ StmtMotionMemory = namedtuple(
25
+ "MotionMemory", ["source", "fps"]) # FIXME needs more fields
26
+ StmtUnrecognized = namedtuple("Unrecognized", ["content", "line_number"])
26
27
 
27
28
 
28
29
  def parse_cmx3600_statements(file: TextIO) -> List[object]:
@@ -30,76 +31,92 @@ def parse_cmx3600_statements(file: TextIO) -> List[object]:
30
31
  Return a list of every statement in the file argument.
31
32
  """
32
33
  lines = file.readlines()
33
- line_numbers = count()
34
- return [_parse_cmx3600_line(line.strip(), line_number) \
35
- for (line, line_number) in zip(lines,line_numbers)]
36
-
37
- def _edl_column_widths(event_field_length, source_field_length):
38
- return [event_field_length,2, source_field_length,1,
39
- 4,2, # chans
40
- 4,1, # trans
41
- 3,1, # trans op
42
- 11,1,
43
- 11,1,
44
- 11,1,
45
- 11]
46
-
47
- def _edl_m2_column_widths():
48
- return [2, # "M2"
49
- 3,3, #
50
- 8,8,1,4,2,1,4,13,3,1,1]
51
-
52
-
53
- def _parse_cmx3600_line(line, line_number):
54
- long_event_num_p = re.compile("^[0-9]{6} ")
34
+ return [_parse_cmx3600_line(line.strip(), line_number)
35
+ for (line_number, line) in enumerate(lines)]
36
+
37
+
38
+ def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
39
+ return [event_field_length, 2, source_field_length, 1,
40
+ 4, 2, # chans
41
+ 4, 1, # trans
42
+ 3, 1, # trans op
43
+ 11, 1,
44
+ 11, 1,
45
+ 11, 1,
46
+ 11]
47
+
48
+ # def _edl_m2_column_widths():
49
+ # return [2, # "M2"
50
+ # 3,3, #
51
+ # 8,8,1,4,2,1,4,13,3,1,1]
52
+
53
+
54
+ def _parse_cmx3600_line(line: str, line_number: int) -> object:
55
+ """
56
+ Parses a single CMX EDL line.
57
+
58
+ :param line: A single EDL line.
59
+ :param line_number: The index of this line in the file.
60
+ """
61
+ long_event_num_p = re.compile("^[0-9]{6} ")
55
62
  short_event_num_p = re.compile("^[0-9]{3} ")
56
-
57
- if isinstance(line,str):
58
- if line.startswith("TITLE:"):
59
- return _parse_title(line,line_number)
60
- elif line.startswith("FCM:"):
61
- return _parse_fcm(line, line_number)
62
- elif long_event_num_p.match(line) != None:
63
- length_file_128 = sum(_edl_column_widths(6,128))
64
- if len(line) < length_file_128:
65
- return _parse_long_standard_form(line, 32, line_number)
66
- else:
67
- return _parse_long_standard_form(line, 128, line_number)
68
- elif short_event_num_p.match(line) != None:
69
- return _parse_standard_form(line, line_number)
70
- elif line.startswith("AUD"):
71
- return _parse_extended_audio_channels(line,line_number)
72
- elif line.startswith("*"):
73
- return _parse_remark( line[1:].strip(), line_number)
74
- elif line.startswith(">>> SOURCE"):
75
- return _parse_source_umid_statement(line, line_number)
76
- elif line.startswith("EFFECTS NAME IS"):
77
- return _parse_effects_name(line, line_number)
78
- elif line.startswith("SPLIT:"):
79
- return _parse_split(line, line_number)
80
- elif line.startswith("M2"):
81
- return _parse_motion_memory(line, line_number)
63
+ x_event_form_p = re.compile("^([0-9]{4,5}) ")
64
+
65
+ if line.startswith("TITLE:"):
66
+ return _parse_title(line, line_number)
67
+ elif line.startswith("FCM:"):
68
+ return _parse_fcm(line, line_number)
69
+ elif long_event_num_p.match(line) is not None:
70
+ length_file_128 = sum(_edl_column_widths(6, 128))
71
+ if len(line) < length_file_128:
72
+ return _parse_long_standard_form(line, 32, line_number)
82
73
  else:
83
- return _parse_unrecognized(line, line_number)
74
+ return _parse_long_standard_form(line, 128, line_number)
75
+ elif (m := x_event_form_p.match(line)) is not None:
76
+ assert m is not None
77
+ event_field_length = len(m[1])
78
+ return _parse_columns_for_standard_form(line, event_field_length,
79
+ 8, line_number)
80
+ elif short_event_num_p.match(line) is not None:
81
+ return _parse_standard_form(line, line_number)
82
+ elif line.startswith("AUD"):
83
+ return _parse_extended_audio_channels(line, line_number)
84
+ elif line.startswith("*"):
85
+ return _parse_remark(line[1:].strip(), line_number)
86
+ elif line.startswith(">>> SOURCE"):
87
+ return _parse_source_umid_statement(line, line_number)
88
+ elif line.startswith("EFFECTS NAME IS"):
89
+ return _parse_effects_name(line, line_number)
90
+ elif line.startswith("SPLIT:"):
91
+ return _parse_split(line, line_number)
92
+ elif line.startswith("M2"):
93
+ return _parse_motion_memory(line, line_number)
94
+ else:
95
+ return _parse_unrecognized(line, line_number)
96
+
84
97
 
85
-
86
- def _parse_title(line, line_num):
98
+ def _parse_title(line, line_num) -> StmtTitle:
87
99
  title = line[6:].strip()
88
- return StmtTitle(title=title,line_number=line_num)
100
+ return StmtTitle(title=title, line_number=line_num)
89
101
 
90
- def _parse_fcm(line, line_num):
102
+
103
+ def _parse_fcm(line, line_num) -> StmtFCM:
91
104
  val = line[4:].strip()
92
105
  if val == "DROP FRAME":
93
- return StmtFCM(drop= True, line_number=line_num)
106
+ return StmtFCM(drop=True, line_number=line_num)
94
107
  else:
95
- return StmtFCM(drop= False, line_number=line_num)
108
+ return StmtFCM(drop=False, line_number=line_num)
109
+
110
+
111
+ def _parse_long_standard_form(line, source_field_length, line_number):
112
+ return _parse_columns_for_standard_form(line, 6, source_field_length,
113
+ line_number)
114
+
96
115
 
97
- def _parse_long_standard_form(line,source_field_length, line_number):
98
- return _parse_columns_for_standard_form(line, 6, source_field_length, line_number)
99
-
100
116
  def _parse_standard_form(line, line_number):
101
117
  return _parse_columns_for_standard_form(line, 3, 8, line_number)
102
-
118
+
119
+
103
120
  def _parse_extended_audio_channels(line, line_number):
104
121
  content = line.strip()
105
122
  if content == "AUD 3":
@@ -110,60 +127,67 @@ def _parse_extended_audio_channels(line, line_number):
110
127
  return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
111
128
  else:
112
129
  return StmtUnrecognized(content=line, line_number=line_number)
113
-
130
+
131
+
114
132
  def _parse_remark(line, line_number) -> object:
115
133
  if line.startswith("FROM CLIP NAME:"):
116
- return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
134
+ return StmtClipName(name=line[15:].strip(), affect="from",
135
+ line_number=line_number)
117
136
  elif line.startswith("TO CLIP NAME:"):
118
- return StmtClipName(name=line[13:].strip(), affect="to", line_number=line_number)
137
+ return StmtClipName(name=line[13:].strip(), affect="to",
138
+ line_number=line_number)
119
139
  elif line.startswith("SOURCE FILE:"):
120
- return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
140
+ return StmtSourceFile(filename=line[12:].strip(),
141
+ line_number=line_number)
121
142
  else:
122
143
  return StmtRemark(text=line, line_number=line_number)
123
144
 
145
+
124
146
  def _parse_effects_name(line, line_number) -> StmtEffectsName:
125
147
  name = line[16:].strip()
126
148
  return StmtEffectsName(name=name, line_number=line_number)
127
149
 
150
+
128
151
  def _parse_split(line, line_number):
129
152
  split_type = line[10:21]
130
153
  is_video = False
131
154
  if split_type.startswith("VIDEO"):
132
155
  is_video = True
133
156
 
134
- split_mag = line[24:35]
135
- return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number)
157
+ split_mag = line[24:35]
158
+ return StmtSplitEdit(video=is_video, magnitude=split_mag,
159
+ line_number=line_number)
136
160
 
137
161
 
138
162
  def _parse_motion_memory(line, line_number):
139
- return StmtMotionMemory(source = "", fps="")
163
+ return StmtMotionMemory(source="", fps="")
140
164
 
141
165
 
142
166
  def _parse_unrecognized(line, line_number):
143
167
  return StmtUnrecognized(content=line, line_number=line_number)
144
168
 
145
- def _parse_columns_for_standard_form(line, event_field_length, source_field_length, line_number):
169
+
170
+ def _parse_columns_for_standard_form(line, event_field_length,
171
+ source_field_length, line_number):
146
172
  col_widths = _edl_column_widths(event_field_length, source_field_length)
147
-
173
+
148
174
  if sum(col_widths) > len(line):
149
175
  return StmtUnrecognized(content=line, line_number=line_number)
150
-
151
- column_strings = collimate(line,col_widths)
152
-
153
- return StmtEvent(event=column_strings[0],
154
- source=column_strings[2].strip(),
155
- channels=column_strings[4].strip(),
156
- trans=column_strings[6].strip(),
176
+
177
+ column_strings = collimate(line, col_widths)
178
+
179
+ return StmtEvent(event=column_strings[0],
180
+ source=column_strings[2].strip(),
181
+ channels=column_strings[4].strip(),
182
+ trans=column_strings[6].strip(),
157
183
  trans_op=column_strings[8].strip(),
158
184
  source_in=column_strings[10].strip(),
159
185
  source_out=column_strings[12].strip(),
160
186
  record_in=column_strings[14].strip(),
161
187
  record_out=column_strings[16].strip(),
162
- line_number=line_number,
163
- format=source_field_length)
188
+ line_number=line_number, format=source_field_length)
164
189
 
165
190
 
166
191
  def _parse_source_umid_statement(line, line_number):
167
- trimmed = line[3:].strip()
192
+ # trimmed = line[3:].strip()
168
193
  return StmtSourceUMID(name=None, umid=None, line_number=line_number)
169
-
pycmx/transition.py CHANGED
@@ -3,11 +3,12 @@
3
3
 
4
4
  from typing import Optional
5
5
 
6
+
6
7
  class Transition:
7
8
  """
8
9
  A CMX transition: a wipe, dissolve or cut.
9
10
  """
10
-
11
+
11
12
  Cut = "C"
12
13
  Dissolve = "D"
13
14
  Wipe = "W"
@@ -41,7 +42,7 @@ class Transition:
41
42
  @property
42
43
  def cut(self) -> bool:
43
44
  "`True` if this transition is a cut."
44
- return self.transition == 'C'
45
+ return self.transition == 'C'
45
46
 
46
47
  @property
47
48
  def dissolve(self) -> bool:
@@ -56,7 +57,7 @@ class Transition:
56
57
  @property
57
58
  def effect_duration(self) -> int:
58
59
  """The duration of this transition, in frames of the record target.
59
-
60
+
60
61
  In the event of a key event, this is the duration of the fade in.
61
62
  """
62
63
  return int(self.operand)
@@ -86,5 +87,3 @@ class Transition:
86
87
  the key foreground and replaced with the key background.
87
88
  """
88
89
  return self.transition == Transition.KeyOut
89
-
90
-
pycmx/util.py CHANGED
@@ -3,29 +3,28 @@
3
3
 
4
4
  # Utility functions
5
5
 
6
- def collimate(a_string, column_widths):
6
+ def collimate(a_string, column_widths):
7
7
  """
8
- Split a list-type thing, like a string, into slices that are column_widths
8
+ Split a list-type thing, like a string, into slices that are column_widths
9
9
  length.
10
-
10
+
11
11
  >>> collimate("a b1 c2345",[2,3,3,2])
12
12
  ['a ','b1 ','c23','45']
13
13
 
14
14
  Args:
15
15
  a_string: The string to split. This parameter can actually be anything
16
16
  sliceable.
17
- column_widths: A list of integers, each one is the length of a column.
17
+ column_widths: A list of integers, each one is the length of a column.
18
18
 
19
19
  Returns:
20
- A list of slices. The len() of the returned list will *always* equal
21
- len(:column_widths:).
20
+ A list of slices. The len() of the returned list will *always* equal
21
+ len(:column_widths:).
22
22
  """
23
-
23
+
24
24
  if len(column_widths) == 0:
25
25
  return []
26
-
26
+
27
27
  width = column_widths[0]
28
28
  element = a_string[:width]
29
29
  rest = a_string[width:]
30
- return [element] + collimate(rest, column_widths[1:])
31
-
30
+ return [element] + collimate(rest, column_widths[1:])
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2022 Jamie Hardt.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -1,29 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pycmx
3
- Version: 1.2.2
4
- Summary: pycmx is a parser for CMX 3600-style EDLs.
3
+ Version: 1.3.0
4
+ Summary: Python CMX 3600 Edit Decision List Parser
5
+ Home-page: https://github.com/iluvcapra/pycmx
6
+ License: MIT
5
7
  Keywords: parser,film,broadcast
6
- Author-email: Jamie Hardt <jamiehardt@me.com>
7
- Requires-Python: ~=3.7
8
- Description-Content-Type: text/markdown
8
+ Author: Jamie Hardt
9
+ Author-email: jamiehardt@me.com
10
+ Requires-Python: >=3.8,<4.0
9
11
  Classifier: Development Status :: 5 - Production/Stable
10
12
  Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Topic :: Multimedia
12
- Classifier: Topic :: Multimedia :: Video
13
- Classifier: Topic :: Text Processing
14
- Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3
15
14
  Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
- Requires-Dist: sphinx >= 5.3.0 ; extra == "doc"
21
- Requires-Dist: sphinx_rtd_theme >= 1.1.1 ; extra == "doc"
22
- Project-URL: Documentation, https://pycmx.readthedocs.io/
23
- Project-URL: Home, https://github.com/iluvcapra/pycmx
24
- Project-URL: Issues, https://github.com/iluvcapra/pycmx/issues
25
- Project-URL: Source, https://github.com/iluvcapra/pycmx.git
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Multimedia
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Classifier: Topic :: Text Processing
26
23
  Provides-Extra: doc
24
+ Requires-Dist: sphinx (>=5.3.0) ; extra == "doc"
25
+ Requires-Dist: sphinx_rtd_theme (>=1.1.1) ; extra == "doc"
26
+ Project-URL: Documentation, https://pycmx.readthedocs.io/
27
+ Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
28
+ Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
29
+ Description-Content-Type: text/markdown
27
30
 
28
31
  [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg)
29
32
  ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/pycmx)
@@ -32,12 +35,14 @@ Provides-Extra: doc
32
35
 
33
36
  # pycmx
34
37
 
35
- The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and its most most common variations.
38
+ The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and
39
+ its most most common variations.
36
40
 
37
41
  ## Features
38
42
 
39
- * The major variations of the CMX 3600: the standard, "File32" and "File128"
40
- formats are automatically detected and properly read.
43
+ * The major variations of the CMX 3600: the standard, "File32", "File128" and
44
+ long Adobe Premiere event numbers are automatically detected and properly
45
+ read.
41
46
  * Preserves relationship between events and individual edits/clips.
42
47
  * Remark or comment fields with common recognized forms are read and
43
48
  available to the client, including clip name and source file data.
@@ -111,5 +116,3 @@ Audio channel 7 is present
111
116
  False
112
117
  ```
113
118
 
114
-
115
-
@@ -0,0 +1,13 @@
1
+ pycmx/__init__.py,sha256=u_VU3f3WltGoZzg_iZBLQZUtFqGFVZgBX3trqM7v3D4,365
2
+ pycmx/channel_map.py,sha256=vkGonW2CY5F_TCbcJm9jFp6O7JsNUW4hew5q8KiVWLo,3169
3
+ pycmx/edit.py,sha256=6AfQAHh8f4aRFs211d2-Jmkv-a8CmoIduOFL3N2lrQg,3893
4
+ pycmx/edit_list.py,sha256=hjOsCxrGwQemjpmUGdfnJa0_rSYs-LSYkfOrjlqU-gw,3170
5
+ pycmx/event.py,sha256=6fwbCjo1mX8Zkmbd6Maw4-msWm2zYCUGRIGSeqfFWdw,3809
6
+ pycmx/parse_cmx_events.py,sha256=9d0jCvOXCt1RPSEBLZhLScSsGq3oDVa2bBDar8x4j5U,477
7
+ pycmx/parse_cmx_statements.py,sha256=Wi0wjlJsphR3WXWXcJn7fPRDKANitZpQwvOSZ84HY2A,7242
8
+ pycmx/transition.py,sha256=IlVz2X3Z7CttJccqzzjhPPUNY2Pe8ZmVMIBnCrVLIG0,2364
9
+ pycmx/util.py,sha256=z1QYA4qUxiaCeKPhCQEmgIyhr5MHxSknW2xjl6qs1kA,782
10
+ pycmx-1.3.0.dist-info/LICENSE,sha256=JS087ZFloGaV7LHygeUaXmT-fGLx0VF30W62wDTLRCc,1056
11
+ pycmx-1.3.0.dist-info/METADATA,sha256=drKnB4Am-o2JUh_dFsDItdFV5JQYarvgOfl88icsBKY,4027
12
+ pycmx-1.3.0.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
13
+ pycmx-1.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.9.0
2
+ Generator: poetry-core 2.0.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,12 +0,0 @@
1
- pycmx/__init__.py,sha256=yS4sYKFfw3ScPaCr0_g0qSmhhZRxOLkgwR5kQ9nxYnc,388
2
- pycmx/channel_map.py,sha256=M7P3b0S2cBrmQjfSP5sixFxGv_uBjvie0f9PVdy9cMI,3248
3
- pycmx/edit.py,sha256=sipPmT2W0hPoYyRyZzwohohjO1fqwEU_mxsQheZxWC0,3775
4
- pycmx/edit_list.py,sha256=zZaOTEPgubHxetQF01BhMf6d097jXjvohYrMXtbP1a4,3172
5
- pycmx/event.py,sha256=-wlOdOwOVVk_4Wus0D8PVxRiEPBv1XZrRiQwkoRIOY4,3781
6
- pycmx/parse_cmx_events.py,sha256=_KeowR6jm4LlFWtejgZ0SL5jX-sMu8GKgx7ferUuRbE,472
7
- pycmx/parse_cmx_statements.py,sha256=UqS-p1kAFR8cp1pgiiBZLlyIhF-8hOWykYIS6-JuEBg,6755
8
- pycmx/transition.py,sha256=fAbWMqBZmZIcpZUobTlYuTd4WxercHMik_zqp7yK4mA,2378
9
- pycmx/util.py,sha256=alnJaV9vqH-KGtExrWjBiYMTiTILDXnF8Ju0sXWa69A,801
10
- pycmx-1.2.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
- pycmx-1.2.2.dist-info/METADATA,sha256=WbFuxIxB-PIjdcqaQtdsT597a9gWlZR54hSXktzPcrI,3928
12
- pycmx-1.2.2.dist-info/RECORD,,