pycmx 0.8__py3-none-any.whl → 1.2.1__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
@@ -1,17 +1,15 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- pycmx is a module for parsing CMX 3600-style EDLs. For more information and
4
- examples see README.md
3
+ pycmx is a parser for CMX 3600-style EDLs.
5
4
 
6
- This module (c) 2018 Jamie Hardt. For more information on your rights to
7
- copy and reuse this software, refer to the LICENSE file included with the
5
+ This module (c) 2023 Jamie Hardt. For more information on your rights to
6
+ copy and reuse this software, refer to the LICENSE file included with the
8
7
  distribution.
9
8
  """
10
9
 
11
- __version__ = '0.8'
12
- __author__ = 'Jamie Hardt'
10
+ __version__ = '1.2.1'
13
11
 
14
- from .parse_cmx_events import parse_cmx3600
12
+ from .parse_cmx_events import parse_cmx3600
15
13
  from .transition import Transition
16
14
  from .event import Event
17
15
  from .edit import Edit
pycmx/channel_map.py CHANGED
@@ -2,13 +2,14 @@
2
2
  # (c) 2018 Jamie Hardt
3
3
 
4
4
  from re import (compile, match)
5
+ from typing import Dict, Tuple, Generator
5
6
 
6
7
  class ChannelMap:
7
8
  """
8
9
  Represents a set of all the channels to which an event applies.
9
10
  """
10
11
 
11
- _chan_map = {
12
+ _chan_map : Dict[str, Tuple] = {
12
13
  "V" : (True, False, False),
13
14
  "A" : (False, True, False),
14
15
  "A2" : (False, False, True),
@@ -23,65 +24,70 @@ class ChannelMap:
23
24
  self.v = v
24
25
 
25
26
  @property
26
- def video(self):
27
+ def video(self) -> bool:
27
28
  'True if video is included'
28
29
  return self.v
29
30
 
30
31
  @property
31
- def channels(self):
32
+ def audio(self) -> bool:
33
+ 'True if an audio channel is included'
34
+ return len(self._audio_channel_set) > 0
35
+
36
+ @property
37
+ def channels(self) -> Generator[int, None, None]:
32
38
  'A generator for each audio channel'
33
39
  for c in self._audio_channel_set:
34
40
  yield c
35
41
 
36
42
  @property
37
- def a1(self):
38
- """True if A1 is included."""
43
+ def a1(self) -> bool:
44
+ """True if A1 is included"""
39
45
  return self.get_audio_channel(1)
40
46
 
41
47
  @a1.setter
42
- def a1(self,val):
48
+ def a1(self, val: bool):
43
49
  self.set_audio_channel(1,val)
44
50
 
45
51
  @property
46
- def a2(self):
47
- """True if A2 is included."""
52
+ def a2(self) -> bool:
53
+ """True if A2 is included"""
48
54
  return self.get_audio_channel(2)
49
55
 
50
56
  @a2.setter
51
- def a2(self,val):
57
+ def a2(self, val: bool):
52
58
  self.set_audio_channel(2,val)
53
59
 
54
60
  @property
55
- def a3(self):
56
- """True if A3 is included."""
61
+ def a3(self) -> bool:
62
+ """True if A3 is included"""
57
63
  return self.get_audio_channel(3)
58
64
 
59
65
  @a3.setter
60
- def a3(self,val):
66
+ def a3(self, val: bool):
61
67
  self.set_audio_channel(3,val)
62
68
 
63
69
  @property
64
- def a4(self):
65
- """True if A4 is included."""
70
+ def a4(self) -> bool:
71
+ """True if A4 is included"""
66
72
  return self.get_audio_channel(4)
67
73
 
68
74
  @a4.setter
69
- def a4(self,val):
75
+ def a4(self,val: bool):
70
76
  self.set_audio_channel(4,val)
71
77
 
72
- def get_audio_channel(self,chan_num):
73
- """True if chan_num is included."""
78
+ def get_audio_channel(self, chan_num) -> bool:
79
+ """True if chan_num is included"""
74
80
  return (chan_num in self._audio_channel_set)
75
81
 
76
- def set_audio_channel(self,chan_num,enabled):
77
- """If enabled is true, chan_num will be included."""
82
+ def set_audio_channel(self,chan_num, enabled: bool):
83
+ """If enabled is true, chan_num will be included"""
78
84
  if enabled:
79
85
  self._audio_channel_set.add(chan_num)
80
86
  elif self.get_audio_channel(chan_num):
81
87
  self._audio_channel_set.remove(chan_num)
82
88
 
83
89
  def _append_event(self, event_str):
84
- alt_channel_re = compile('^A(\d+)')
90
+ alt_channel_re = compile(r'^A(\d+)')
85
91
  if event_str in self._chan_map:
86
92
  channels = self._chan_map[event_str]
87
93
  self.v = channels[0]
@@ -93,6 +99,15 @@ class ChannelMap:
93
99
  self.set_audio_channel(int( matchresult.group(1)), True )
94
100
 
95
101
  def _append_ext(self, audio_ext):
96
- self.a3 = ext.audio3
97
- self.a4 = ext.audio4
102
+ self.a3 = audio_ext.audio3
103
+ self.a4 = audio_ext.audio4
104
+
105
+ def __or__(self, other):
106
+ """
107
+ the logical union of this channel map with another
108
+ """
109
+ out_v = self.video | other.video
110
+ out_a = self._audio_channel_set | other._audio_channel_set
111
+
112
+ return ChannelMap(v=out_v,audio_channels = out_a)
98
113
 
pycmx/edit.py CHANGED
@@ -3,7 +3,9 @@
3
3
 
4
4
  from .transition import Transition
5
5
  from .channel_map import ChannelMap
6
- from .parse_cmx_statements import StmtEffectsName
6
+ # from .parse_cmx_statements import StmtEffectsName
7
+
8
+ from typing import Optional
7
9
 
8
10
  class Edit:
9
11
  """
@@ -18,7 +20,7 @@ class Edit:
18
20
  self.trans_name_statement = trans_name_statement
19
21
 
20
22
  @property
21
- def line_number(self):
23
+ def line_number(self) -> int:
22
24
  """
23
25
  Get the line number for the "standard form" statement associated with
24
26
  this edit. Line numbers a zero-indexed, such that the
@@ -27,7 +29,7 @@ class Edit:
27
29
  return self.edit_statement.line_number
28
30
 
29
31
  @property
30
- def channels(self):
32
+ def channels(self) -> ChannelMap:
31
33
  """
32
34
  Get the :obj:`ChannelMap` object associated with this Edit.
33
35
  """
@@ -38,7 +40,7 @@ class Edit:
38
40
  return cm
39
41
 
40
42
  @property
41
- def transition(self):
43
+ def transition(self) -> Transition:
42
44
  """
43
45
  Get the :obj:`Transition` object associated with this edit.
44
46
  """
@@ -48,14 +50,14 @@ class Edit:
48
50
  return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None)
49
51
 
50
52
  @property
51
- def source_in(self):
53
+ def source_in(self) -> str:
52
54
  """
53
55
  Get the source in timecode.
54
56
  """
55
57
  return self.edit_statement.source_in
56
58
 
57
59
  @property
58
- def source_out(self):
60
+ def source_out(self) -> str:
59
61
  """
60
62
  Get the source out timecode.
61
63
  """
@@ -63,7 +65,7 @@ class Edit:
63
65
  return self.edit_statement.source_out
64
66
 
65
67
  @property
66
- def record_in(self):
68
+ def record_in(self) -> str:
67
69
  """
68
70
  Get the record in timecode.
69
71
  """
@@ -71,7 +73,7 @@ class Edit:
71
73
  return self.edit_statement.record_in
72
74
 
73
75
  @property
74
- def record_out(self):
76
+ def record_out(self) -> str:
75
77
  """
76
78
  Get the record out timecode.
77
79
  """
@@ -79,7 +81,7 @@ class Edit:
79
81
  return self.edit_statement.record_out
80
82
 
81
83
  @property
82
- def source(self):
84
+ def source(self) -> str:
83
85
  """
84
86
  Get the source column. This is the 8, 32 or 128-character string on the
85
87
  event record line, this usually references the tape name of the source.
@@ -87,21 +89,21 @@ class Edit:
87
89
  return self.edit_statement.source
88
90
 
89
91
  @property
90
- def black(self):
92
+ def black(self) -> bool:
91
93
  """
92
94
  Black video or silence should be used as the source for this event.
93
95
  """
94
96
  return self.source == "BL"
95
97
 
96
98
  @property
97
- def aux_source(self):
99
+ def aux_source(self) -> bool:
98
100
  """
99
101
  An auxiliary source is the source of this event.
100
102
  """
101
103
  return self.source == "AX"
102
104
 
103
105
  @property
104
- def source_file(self):
106
+ def source_file(self) -> Optional[str]:
105
107
  """
106
108
  Get the source file, as attested by a "* SOURCE FILE" remark on the
107
109
  EDL. This will return None if the information is not present.
@@ -112,7 +114,7 @@ class Edit:
112
114
  return self.source_file_statement.filename
113
115
 
114
116
  @property
115
- def clip_name(self):
117
+ def clip_name(self) -> Optional[str]:
116
118
  """
117
119
  Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
118
120
  NAME" remark on the EDL. This will return None if the information is
pycmx/edit_list.py CHANGED
@@ -1,30 +1,65 @@
1
1
  # pycmx
2
2
  # (c) 2018 Jamie Hardt
3
3
 
4
- from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent)
4
+ from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
5
5
  from .event import Event
6
+ from .channel_map import ChannelMap
7
+
8
+ from typing import Generator
6
9
 
7
10
  class EditList:
8
11
  """
9
- Represents an entire edit decision list as returned by `parse_cmx3600()`.
10
-
12
+ Represents an entire edit decision list as returned by :func:`~pycmx.parse_cmx3600()`.
11
13
  """
12
14
  def __init__(self, statements):
13
15
  self.title_statement = statements[0]
14
16
  self.event_statements = statements[1:]
15
17
 
16
18
  @property
17
- def title(self):
19
+ def format(self) -> str:
20
+ """
21
+ The detected format of the EDL. Possible values are: "3600", "File32",
22
+ "File128", and "unknown".
23
+ """
24
+ first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
25
+
26
+ if first_event:
27
+ if first_event.format == 8:
28
+ return '3600'
29
+ elif first_event.format == 32:
30
+ return 'File32'
31
+ elif first_event.format == 128:
32
+ return 'File128'
33
+ else:
34
+ return 'unknown'
35
+ else:
36
+ return 'unknown'
37
+
38
+
39
+ @property
40
+ def channels(self) -> ChannelMap:
18
41
  """
19
- The title of this edit list, as attensted by the 'TITLE:' statement on
20
- the first line.
42
+ Return the union of every channel channel.
43
+ """
44
+
45
+ retval = ChannelMap()
46
+ for event in self.events:
47
+ for edit in event.edits:
48
+ retval = retval | edit.channels
49
+
50
+ return retval
51
+
52
+
53
+ @property
54
+ def title(self) -> str:
55
+ """
56
+ The title of this edit list.
21
57
  """
22
- 'The title of the edit list'
23
58
  return self.title_statement.title
24
59
 
25
60
 
26
61
  @property
27
- def unrecognized_statements(self):
62
+ def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
28
63
  """
29
64
  A generator for all the unrecognized statements in the list.
30
65
  """
@@ -34,7 +69,7 @@ class EditList:
34
69
 
35
70
 
36
71
  @property
37
- def events(self):
72
+ def events(self) -> Generator[Event, None, None]:
38
73
  'A generator for all the events in the edit list'
39
74
  is_drop = None
40
75
  current_event_num = None
@@ -54,8 +89,21 @@ class EditList:
54
89
  else:
55
90
  event_statements.append(stmt)
56
91
 
92
+ elif type(stmt) is StmtSourceUMID:
93
+ break
57
94
  else:
58
95
  event_statements.append(stmt)
59
96
 
60
97
  yield Event(statements=event_statements)
61
98
 
99
+ @property
100
+ def sources(self) -> Generator[StmtSourceUMID, None, None]:
101
+ """
102
+ A generator for all of the sources in the list
103
+ """
104
+
105
+ for stmt in self.event_statements:
106
+ if type(stmt) is StmtSourceUMID:
107
+ yield stmt
108
+
109
+
pycmx/event.py CHANGED
@@ -1,26 +1,28 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2023 Jamie Hardt
3
3
 
4
4
  from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName)
5
- from .edit import Edit
5
+ from .edit import Edit
6
+
7
+ from typing import List, Generator, Optional, Tuple, Any
6
8
 
7
9
  class Event:
8
10
  """
9
- Represents a collection of :class:`Edit`s, all with the same event number.
11
+ Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same event number.
10
12
  """
11
13
 
12
14
  def __init__(self, statements):
13
15
  self.statements = statements
14
16
 
15
17
  @property
16
- def number(self):
18
+ def number(self) -> int:
17
19
  """
18
20
  Return the event number.
19
21
  """
20
22
  return int(self._edit_statements()[0].event)
21
23
 
22
24
  @property
23
- def edits(self):
25
+ def edits(self) -> List[Edit]:
24
26
  """
25
27
  Returns the edits. Most events will have a single edit, a single event
26
28
  will have multiple edits when a dissolve, wipe or key transition needs
@@ -30,17 +32,19 @@ class Event:
30
32
  clip_names = self._clip_name_statements()
31
33
  source_files= self._source_file_statements()
32
34
 
33
- the_zip = [edits_audio]
35
+ the_zip: List[List[Any]] = [edits_audio]
34
36
 
35
37
  if len(edits_audio) == 2:
36
- cn = [None, None]
38
+ start_name: Optional[StmtClipName] = None
39
+ end_name: Optional[StmtClipName] = None
40
+
37
41
  for clip_name in clip_names:
38
42
  if clip_name.affect == 'from':
39
- cn[0] = clip_name
43
+ start_name = clip_name
40
44
  elif clip_name.affect == 'to':
41
- cn[1] = clip_name
45
+ end_name = clip_name
42
46
 
43
- the_zip.append(cn)
47
+ the_zip.append([start_name, end_name])
44
48
  else:
45
49
  if len(edits_audio) == len(clip_names):
46
50
  the_zip.append(clip_names)
@@ -57,17 +61,20 @@ class Event:
57
61
  # attach trans name to last event
58
62
  try:
59
63
  trans_statement = self._trans_name_statements()[0]
60
- trans_names = [None] * (len(edits_audio) - 1)
64
+ trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
61
65
  trans_names.append(trans_statement)
62
66
  the_zip.append(trans_names)
63
67
  except IndexError:
64
68
  the_zip.append([None] * len(edits_audio) )
65
69
 
66
-
67
- return [ Edit(e1[0],e1[1],n1,s1,u1) for (e1,n1,s1,u1) in zip(*the_zip) ]
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) ]
68
75
 
69
76
  @property
70
- def unrecognized_statements(self):
77
+ def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
71
78
  """
72
79
  A generator for all the unrecognized statements in the event.
73
80
  """
@@ -75,19 +82,19 @@ class Event:
75
82
  if type(s) is StmtUnrecognized:
76
83
  yield s
77
84
 
78
- def _trans_name_statements(self):
85
+ def _trans_name_statements(self) -> List[StmtEffectsName]:
79
86
  return [s for s in self.statements if type(s) is StmtEffectsName]
80
87
 
81
- def _edit_statements(self):
88
+ def _edit_statements(self) -> List[StmtEvent]:
82
89
  return [s for s in self.statements if type(s) is StmtEvent]
83
90
 
84
- def _clip_name_statements(self):
91
+ def _clip_name_statements(self) -> List[StmtClipName]:
85
92
  return [s for s in self.statements if type(s) is StmtClipName]
86
93
 
87
- def _source_file_statements(self):
94
+ def _source_file_statements(self) -> List[StmtSourceFile]:
88
95
  return [s for s in self.statements if type(s) is StmtSourceFile]
89
96
 
90
- def _statements_with_audio_ext(self):
97
+ def _statements_with_audio_ext(self) -> Generator[Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
91
98
  for (s1, s2) in zip(self.statements, self.statements[1:]):
92
99
  if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
93
100
  yield (s1,s2)
pycmx/parse_cmx_events.py CHANGED
@@ -1,20 +1,19 @@
1
1
  # pycmx
2
2
  # (c) 2018 Jamie Hardt
3
3
 
4
- from collections import namedtuple
4
+ # from collections import namedtuple
5
5
 
6
- from .parse_cmx_statements import (parse_cmx3600_statements, StmtEvent,StmtFCM )
6
+ from .parse_cmx_statements import (parse_cmx3600_statements)
7
7
  from .edit_list import EditList
8
8
 
9
- def parse_cmx3600(f):
9
+ from typing import TextIO
10
+
11
+ def parse_cmx3600(f: TextIO):
10
12
  """
11
13
  Parse a CMX 3600 EDL.
12
14
 
13
- Args:
14
- f : a file-like object, anything that's readlines-able.
15
-
16
- Returns:
17
- An :class:`EditList`.
15
+ :param TextIO f: a file-like object, anything that's readlines-able.
16
+ :returns: An :class:`pycmx.edit_list.EditList`.
18
17
  """
19
18
  statements = parse_cmx3600_statements(f)
20
19
  return EditList(statements)
@@ -5,25 +5,27 @@ import re
5
5
  import sys
6
6
  from collections import namedtuple
7
7
  from itertools import count
8
+ from typing import TextIO, List
9
+
8
10
 
9
11
  from .util import collimate
10
12
 
11
13
  StmtTitle = namedtuple("Title",["title","line_number"])
12
14
  StmtFCM = namedtuple("FCM",["drop","line_number"])
13
15
  StmtEvent = namedtuple("Event",["event","source","channels","trans",\
14
- "trans_op","source_in","source_out","record_in","record_out","line_number"])
16
+ "trans_op","source_in","source_out","record_in","record_out","format","line_number"])
15
17
  StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
16
18
  StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
17
19
  StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
18
20
  StmtRemark = namedtuple("Remark",["text","line_number"])
19
21
  StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
20
- StmtTrailer = namedtuple("Trailer",["text","line_number"])
21
- StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"])
22
+ StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
23
+ StmtSplitEdit = namedtuple("SplitEdit",["video","magnitude", "line_number"])
22
24
  StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
23
25
  StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
24
26
 
25
27
 
26
- def parse_cmx3600_statements(file):
28
+ def parse_cmx3600_statements(file: TextIO) -> List[object]:
27
29
  """
28
30
  Return a list of every statement in the file argument.
29
31
  """
@@ -69,8 +71,8 @@ def _parse_cmx3600_line(line, line_number):
69
71
  return _parse_extended_audio_channels(line,line_number)
70
72
  elif line.startswith("*"):
71
73
  return _parse_remark( line[1:].strip(), line_number)
72
- elif line.startswith(">>>"):
73
- return _parse_trailer_statement(line, line_number)
74
+ elif line.startswith(">>> SOURCE"):
75
+ return _parse_source_umid_statement(line, line_number)
74
76
  elif line.startswith("EFFECTS NAME IS"):
75
77
  return _parse_effects_name(line, line_number)
76
78
  elif line.startswith("SPLIT:"):
@@ -109,7 +111,7 @@ def _parse_extended_audio_channels(line, line_number):
109
111
  else:
110
112
  return StmtUnrecognized(content=line, line_number=line_number)
111
113
 
112
- def _parse_remark(line, line_number):
114
+ def _parse_remark(line, line_number) -> object:
113
115
  if line.startswith("FROM CLIP NAME:"):
114
116
  return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
115
117
  elif line.startswith("TO CLIP NAME:"):
@@ -119,7 +121,7 @@ def _parse_remark(line, line_number):
119
121
  else:
120
122
  return StmtRemark(text=line, line_number=line_number)
121
123
 
122
- def _parse_effects_name(line, line_number):
124
+ def _parse_effects_name(line, line_number) -> StmtEffectsName:
123
125
  name = line[16:].strip()
124
126
  return StmtEffectsName(name=name, line_number=line_number)
125
127
 
@@ -157,10 +159,11 @@ def _parse_columns_for_standard_form(line, event_field_length, source_field_leng
157
159
  source_out=column_strings[12].strip(),
158
160
  record_in=column_strings[14].strip(),
159
161
  record_out=column_strings[16].strip(),
160
- line_number=line_number)
162
+ line_number=line_number,
163
+ format=source_field_length)
161
164
 
162
165
 
163
- def _parse_trailer_statement(line, line_number):
166
+ def _parse_source_umid_statement(line, line_number):
164
167
  trimmed = line[3:].strip()
165
- return StmtTrailer(trimmed, line_number=line_number)
168
+ return StmtSourceUMID(name=None, umid=None, line_number=line_number)
166
169
 
pycmx/transition.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # pycmx
2
- # (c) 2018 Jamie Hardt
2
+ # (c) 2023 Jamie Hardt
3
+
4
+ from typing import Optional
3
5
 
4
6
  class Transition:
5
7
  """
@@ -19,7 +21,7 @@ class Transition:
19
21
  self.name = name
20
22
 
21
23
  @property
22
- def kind(self):
24
+ def kind(self) -> Optional[str]:
23
25
  """
24
26
  Return the kind of transition: Cut, Wipe, etc
25
27
  """
@@ -37,22 +39,22 @@ class Transition:
37
39
  return Transition.KeyOut
38
40
 
39
41
  @property
40
- def cut(self):
42
+ def cut(self) -> bool:
41
43
  "`True` if this transition is a cut."
42
44
  return self.transition == 'C'
43
45
 
44
46
  @property
45
- def dissolve(self):
47
+ def dissolve(self) -> bool:
46
48
  "`True` if this traansition is a dissolve."
47
49
  return self.transition == 'D'
48
50
 
49
51
  @property
50
- def wipe(self):
52
+ def wipe(self) -> bool:
51
53
  "`True` if this transition is a wipe."
52
54
  return self.transition.startswith('W')
53
55
 
54
56
  @property
55
- def effect_duration(self):
57
+ def effect_duration(self) -> int:
56
58
  """The duration of this transition, in frames of the record target.
57
59
 
58
60
  In the event of a key event, this is the duration of the fade in.
@@ -60,7 +62,7 @@ class Transition:
60
62
  return int(self.operand)
61
63
 
62
64
  @property
63
- def wipe_number(self):
65
+ def wipe_number(self) -> Optional[int]:
64
66
  "Wipes are identified by a particular number."
65
67
  if self.wipe:
66
68
  return int(self.transition[1:])
@@ -68,19 +70,21 @@ class Transition:
68
70
  return None
69
71
 
70
72
  @property
71
- def key_background(self):
73
+ def key_background(self) -> bool:
72
74
  "`True` if this edit is a key background."
73
- return self.transition == KeyBackground
75
+ return self.transition == Transition.KeyBackground
74
76
 
75
77
  @property
76
- def key_foreground(self):
78
+ def key_foreground(self) -> bool:
77
79
  "`True` if this edit is a key foreground."
78
- return self.transition == Key
80
+ return self.transition == Transition.Key
79
81
 
80
82
  @property
81
- def key_out(self):
83
+ def key_out(self) -> bool:
82
84
  """
83
85
  `True` if this edit is a key out. This material will removed from
84
86
  the key foreground and replaced with the key background.
85
87
  """
86
- return self.transition == KeyOut
88
+ return self.transition == Transition.KeyOut
89
+
90
+
@@ -1,20 +1,32 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycmx
3
- Version: 0.8
4
- Summary: CMX 3600 Edit Decision List Parser
5
- Home-page: https://github.com/iluvcapra/pycmx
6
- Author: Jamie Hardt
7
- Author-email: jamiehardt@me.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
10
- Classifier: Development Status :: 4 - Beta
3
+ Version: 1.2.1
4
+ Summary: pycmx is a parser for CMX 3600-style EDLs.
5
+ Keywords: parser,film,broadcast
6
+ Author-email: Jamie Hardt <jamiehardt@me.com>
7
+ Requires-Python: ~=3.7
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: License :: OSI Approved :: MIT License
12
11
  Classifier: Topic :: Multimedia
13
12
  Classifier: Topic :: Multimedia :: Video
14
13
  Classifier: Topic :: Text Processing
15
- Description-Content-Type: text/markdown
16
-
17
- [![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx) [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest)
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Requires-Dist: sphinx >= 5.3.0 ; extra == "doc"
20
+ Requires-Dist: sphinx_rtd_theme >= 1.1.1 ; extra == "doc"
21
+ Project-URL: Documentation, https://pycmx.readthedocs.io/
22
+ Project-URL: Home, https://github.com/iluvcapra/pycmx
23
+ Project-URL: Issues, https://github.com/iluvcapra/pycmx/issues
24
+ Project-URL: Source, https://github.com/iluvcapra/pycmx.git
25
+ Provides-Extra: doc
26
+
27
+ [![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)
28
+ ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/pycmx)
29
+ [![Lint and Test](https://github.com/iluvcapra/pycmx/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/pycmx/actions/workflows/python-package.yml)
18
30
 
19
31
 
20
32
  # pycmx
@@ -83,7 +95,7 @@ True
83
95
  'TC R1 V1.2 TEMP1 DX M.WAV'
84
96
  >>> events[41].edits[1].clip_name
85
97
  'TC R1 V6 TEMP2 M DX.WAV'
86
-
98
+
87
99
  # parsed channel maps are also
88
100
  # available to the client
89
101
  >>> events[2].edits[0].channels.get_audio_channel(7)
@@ -98,34 +110,5 @@ Audio channel 7 is present
98
110
  False
99
111
  ```
100
112
 
101
- ## How is this different from `python-edl`?
102
-
103
- There are two important differences between `import edl` and `import pycmx`
104
- and motivated my development of this module.
105
-
106
- 1. The `pycmx` parser doesn't take timecode or framerates into account,
107
- and strictly treats timecodes like opaque values. As far as `pycmx` is
108
- concerend, they're just strings. This was done because in my experience,
109
- the frame rate of an EDL is often difficult to precisely determine and
110
- often the frame rate of various sources is different from the frame rate
111
- of the target track.
112
-
113
- In any event, timecodes in an EDL are a kind of *address* and are not
114
- exactly scalar, they're meant to point to a particular block of video or
115
- audio data on a medium and presuming that they refer to a real time, or
116
- duration, or are convertible, etc. isn't always safe.
117
-
118
- 2. The `pycmx` parser reads event numbers and keeps track of which EDL rows
119
- are meant to happen "at the same time," with two decks. This makes it
120
- easier to reconstruct transition A/B clips, and read clip names from
121
- such events appropriately.
122
-
123
- ## Should I Use This?
124
-
125
- At this time, this is (at best) beta software. I feel like the interface is
126
- about where where I'd like it to be but more testing is required.
127
-
128
- Contributions are welcome and will make this module production-ready all the
129
- faster! Please reach out or file a ticket!
130
113
 
131
114
 
@@ -0,0 +1,12 @@
1
+ pycmx/__init__.py,sha256=u4XOzFYiytZ0qQKTwpq615LxYtVct9DygbZzKz_qUNo,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.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
+ pycmx-1.2.1.dist-info/METADATA,sha256=y9fXe3qp9E_u_QXEtEep0Rc4PJj48ElaptkteaXvkgA,3877
12
+ pycmx-1.2.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.32.3)
2
+ Generator: flit 3.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
.DS_Store DELETED
Binary file
pycmx/cmx_event.py DELETED
@@ -1,93 +0,0 @@
1
- class CmxEvent:
2
- def __init__(self,title,number,clip_name,source_name,channels,
3
- transition,source_start,source_finish,
4
- record_start, record_finish, fcm_drop, remarks = [] ,
5
- unrecognized = []):
6
- self.title = title
7
- self.number = number
8
- self.clip_name = clip_name
9
- self.source_name = source_name
10
- self.channels = channels
11
- self.transition = transition
12
- self.source_start = source_start
13
- self.source_finish = source_finish
14
- self.record_start = record_start
15
- self.record_finish = record_finish
16
- self.fcm_drop = fcm_drop
17
- self.remarks = remarks
18
- self.unrecgonized = unrecognized
19
- self.black = (source_name == 'BL')
20
- self.aux_source = (source_name == 'AX')
21
-
22
-
23
- def can_accept(self):
24
- return {'AudioExt','Remark','SourceFile','ClipName','EffectsName'}
25
-
26
- def accept_statement(self, statement):
27
- statement_type = type(statement).__name__
28
- if statement_type == 'AudioExt':
29
- self.channels.appendExt(statement)
30
- elif statement_type == 'Remark':
31
- self.remarks.append(statement.text)
32
- elif statement_type == 'SourceFile':
33
- self.source_name = statement.filename
34
- elif statement_type == 'ClipName':
35
- self.clip_name = statement.name
36
- elif statement_type == 'EffectsName':
37
- self.transition.name = statement.name
38
-
39
- def __repr__(self):
40
- return f"""CmxEvent(title="{self.title}",number={self.number},\
41
- clip_name="{self.clip_name}",source_name="{self.source_name}",\
42
- channels={self.channels},transition={self.transition},\
43
- source_start="{self.source_start}",source_finish="{self.source_finish}",\
44
- record_start="{self.source_start}",record_finish="{self.record_finish}",\
45
- fcm_drop={self.fcm_drop},remarks={self.remarks})"""
46
-
47
-
48
- class CmxTransition:
49
- def __init__(self, transition, operand):
50
- self.transition = transition
51
- self.operand = operand
52
- self.name = ''
53
-
54
- @property
55
- def cut(self):
56
- return self.transition == 'C'
57
-
58
- @property
59
- def dissolve(self):
60
- return self.transition == 'D'
61
-
62
-
63
- @property
64
- def wipe(self):
65
- return self.transition.startswith('W')
66
-
67
-
68
- @property
69
- def effect_duration(self):
70
- return int(self.operand)
71
-
72
- @property
73
- def wipe_number(self):
74
- if self.wipe:
75
- return int(self.transition[1:])
76
- else:
77
- return None
78
-
79
- @property
80
- def key_background(self):
81
- return self.transition == 'KB'
82
-
83
- @property
84
- def key_foreground(self):
85
- return self.transition == 'K'
86
-
87
- @property
88
- def key_out(self):
89
- return self.transition == 'KO'
90
-
91
- def __repr__(self):
92
- return f"""CmxTransition(transition="{self.transition}",operand="{self.operand}")"""
93
-
pycmx/parse_cmx.py DELETED
@@ -1,172 +0,0 @@
1
- # pycmx
2
- # (c) 2018 Jamie Hardt
3
-
4
- from .parse_cmx_statements import parse_cmx3600_statements
5
- from .cmx_event import CmxEvent, CmxTransition
6
- from collections import namedtuple
7
-
8
- from re import compile, match
9
-
10
- class NamedTupleParser:
11
-
12
- def __init__(self, tuple_list):
13
- self.tokens = tuple_list
14
- self.current_token = None
15
-
16
- def peek(self):
17
- return self.tokens[0]
18
-
19
- def at_end(self):
20
- return len(self.tokens) == 0
21
-
22
- def next_token(self):
23
- self.current_token = self.peek()
24
- self.tokens = self.tokens[1:]
25
-
26
- def accept(self, type_name):
27
- if self.at_end():
28
- return False
29
- elif (type(self.peek()).__name__ == type_name ):
30
- self.next_token()
31
- return True
32
- else:
33
- return False
34
-
35
- def expect(self, type_name):
36
- assert( self.accept(type_name) )
37
-
38
-
39
- class CmxChannelMap:
40
- """
41
- Represents a set of all the channels to which an event applies.
42
- """
43
-
44
- chan_map = { "V" : (True, False, False),
45
- "A" : (False, True, False),
46
- "A2" : (False, False, True),
47
- "AA" : (False, True, True),
48
- "B" : (True, True, False),
49
- "AA/V" : (True, True, True),
50
- "A2/V" : (True, False, True)
51
- }
52
-
53
-
54
- def __init__(self, v=False, audio_channels=set()):
55
- self._audio_channel_set = audio_channels
56
- self.v = v
57
-
58
- @property
59
- def a1(self):
60
- return self.get_audio_channel(1)
61
-
62
- @a1.setter
63
- def a1(self,val):
64
- self.set_audio_channel(1,val)
65
-
66
- @property
67
- def a2(self):
68
- return self.get_audio_channel(2)
69
-
70
- @a2.setter
71
- def a2(self,val):
72
- self.set_audio_channel(2,val)
73
-
74
- @property
75
- def a3(self):
76
- return self.get_audio_channel(3)
77
-
78
- @a3.setter
79
- def a3(self,val):
80
- self.set_audio_channel(3,val)
81
-
82
- @property
83
- def a4(self):
84
- return self.get_audio_channel(4)
85
-
86
- @a4.setter
87
- def a4(self,val):
88
- self.set_audio_channel(4,val)
89
-
90
-
91
- def get_audio_channel(self,chan_num):
92
- return (chan_num in self._audio_channel_set)
93
-
94
- def set_audio_channel(self,chan_num,enabled):
95
- if enabled:
96
- self._audio_channel_set.add(chan_num)
97
- elif self.get_audio_channel(chan_num):
98
- self._audio_channel_set.remove(chan_num)
99
-
100
-
101
- def appendEvent(self, event_str):
102
- alt_channel_re = compile('^A(\d+)')
103
- if event_str in self.chan_map:
104
- channels = self.chan_map[event_str]
105
- self.v = channels[0]
106
- self.a1 = channels[1]
107
- self.a2 = channels[2]
108
- else:
109
- matchresult = match(alt_channel_re, event_str)
110
- if matchresult:
111
- self.set_audio_channel(int( matchresult.group(1)), True )
112
-
113
-
114
-
115
-
116
- def appendExt(self, audio_ext):
117
- self.a3 = ext.audio3
118
- self.a4 = ext.audio4
119
-
120
- def __repr__(self):
121
- return f"CmxChannelMap(v={self.v}, audio_channels={self._audio_channel_set})"
122
-
123
-
124
- def parse_cmx3600(file):
125
- """Accepts the path to a CMX EDL and returns a list of all events contained therein."""
126
- statements = parse_cmx3600_statements(file)
127
- parser = NamedTupleParser(statements)
128
- parser.expect('Title')
129
- title = parser.current_token.title
130
- return event_list(title, parser)
131
-
132
-
133
- def event_list(title, parser):
134
- state = {"fcm_drop" : False}
135
-
136
- events_result = []
137
- this_event = None
138
-
139
- while not parser.at_end():
140
- if parser.accept('FCM'):
141
- state['fcm_drop'] = parser.current_token.drop
142
- elif parser.accept('Event'):
143
- if this_event != None:
144
- events_result.append(this_event)
145
-
146
- raw_event = parser.current_token
147
- channels = CmxChannelMap(v=False, audio_channels=set([]))
148
- channels.appendEvent(raw_event.channels)
149
-
150
- this_event = CmxEvent(title=title,number=int(raw_event.event), clip_name=None ,
151
- source_name=raw_event.source,
152
- channels=channels,
153
- transition=CmxTransition(raw_event.trans, raw_event.trans_op),
154
- source_start= raw_event.source_in,
155
- source_finish= raw_event.source_out,
156
- record_start= raw_event.record_in,
157
- record_finish= raw_event.record_out,
158
- fcm_drop= state['fcm_drop'])
159
- elif parser.accept('AudioExt') or parser.accept('ClipName') or \
160
- parser.accept('SourceFile') or parser.accept('EffectsName') or \
161
- parser.accept('Remark'):
162
- this_event.accept_statement(parser.current_token)
163
- elif parser.accept('Trailer'):
164
- break
165
- else:
166
- parser.next_token()
167
-
168
- if this_event != None:
169
- events_result.append(this_event)
170
-
171
- return events_result
172
-
@@ -1,19 +0,0 @@
1
- Copyright (c) 2018 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,17 +0,0 @@
1
- .DS_Store,sha256=Z2BJOZDkCPzls2LQjijgx0p_1xFVMGt5h_O-oxxTW14,6148
2
- pycmx/__init__.py,sha256=4R7CYUo8uilCZeHeDEpbaaPNzPSDl5EYyKow6bJip1Y,474
3
- pycmx/channel_map.py,sha256=vLnzmFBQaQjNg2lU56rVCNGd7ReXPrHCZ53rWQaKBPs,2648
4
- pycmx/cmx_event.py,sha256=jcGGjO_TjrQdiWfDrftz1qAd3Ga8p7TpEhSnny-ZNP8,2856
5
- pycmx/edit.py,sha256=YGv93mlTVmtgzYgbr_xq0aXWBRC_pMw4bC27ZTaE37o,3624
6
- pycmx/edit_list.py,sha256=w6VPFUCiSMJk0He4oBOE4RABKpdL6yfINbSWfIQRg-A,1817
7
- pycmx/event.py,sha256=O5lrI5HpFAdznO0DKvvrinkX2w4McGdsgNXMxZqozd4,3156
8
- pycmx/parse_cmx.py,sha256=K4CdvMAD-upNcMK_i7jg-g05UNewxdp_Nz5urQ0fRyM,5135
9
- pycmx/parse_cmx_events.py,sha256=v36jGNSEj4T-yxkvV0o5xzdvasJFYAkwfphIqkuDmts,447
10
- pycmx/parse_cmx_statements.py,sha256=KVOoqVTHO7D8W3-2gkrJHsijqQtyFQPVgr7O-wasqbo,6571
11
- pycmx/transition.py,sha256=U6EaRXDkCfsJMObiljQMMZ2ul0UykGMU4m1vcxgkQyA,2225
12
- pycmx/util.py,sha256=alnJaV9vqH-KGtExrWjBiYMTiTILDXnF8Ju0sXWa69A,801
13
- pycmx-0.8.dist-info/LICENSE,sha256=vvrbZTJfj_3KDJbSYWvdO5N3RHiIXzBc3CUJ3PKzj-A,1056
14
- pycmx-0.8.dist-info/METADATA,sha256=HlxFrGMRKS2Bznf0h1ntg-HqyDnAcR8OwGeFLvfB2jA,4277
15
- pycmx-0.8.dist-info/WHEEL,sha256=_NOXIqFgOaYmlm9RJLPQZ13BJuEIrp5jx5ptRD5uh3Y,92
16
- pycmx-0.8.dist-info/top_level.txt,sha256=6L-dyNVQT8ahlV9WANz3kUO6fzFXgQGEM7j-BOJhE1o,6
17
- pycmx-0.8.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- pycmx