Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/iluvcapra/pycmx into feat…
Browse files Browse the repository at this point in the history
…-adobe
  • Loading branch information
iluvcapra committed Jan 5, 2025
2 parents 7871acd + 7ed423d commit 2dcfdab
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 238 deletions.
5 changes: 5 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[flake8]
per-file-ignores =
pycmx/__init__.py: F401
tests/__init__.py: F401

3 changes: 1 addition & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ jobs:
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
flake8 . --count --max-line-length=79 --statistics
- name: Test with pytest
run: |
pytest
68 changes: 39 additions & 29 deletions bin/edl2scenelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

log = logging.getLogger(__name__)


def all_video_edits(edl):
for event in edl.events:
for edit in event.edits:
Expand All @@ -30,46 +31,49 @@ def get_scene_name(edit, pattern):
else:
return edit.clip_name


def output_cmx(outfile, out_list):
outfile.write("TITLE: SCENE LIST\r\n")
outfile.write("FCM: NON-DROP FRAME\r\n")

for o in out_list:
line = "%03i AX V C 00:00:00:00 00:00:00:00 %s %s\r\n" % (0, o['start'],o['end'])
for i, o in enumerate(out_list):
line = '%03i AX V C ' % (i)
line += '00:00:00:00 00:00:00:00 %s %s\r\n' % (o['start'], o['end'])
outfile.write(line)
outfile.write("* FROM CLIP NAME: %s\r\n" % (o['scene']) )
outfile.write("* FROM CLIP NAME: %s\r\n" % (o['scene']))


def output_cols(outfile, out_list):
for o in out_list:
outfile.write("%-12s\t%-12s\t%s\n" % (o['start'], o['end'], o['scene'] ))
outfile.write("%-12s\t%-12s\t%s\n" %
(o['start'], o['end'], o['scene']))


def scene_list(infile, outfile, out_format, pattern):

edl = pycmx.parse_cmx3600(infile)

current_scene_name = None
grouped_edits = [ ]

grouped_edits = []

for edit in all_video_edits(edl):
this_scene_name = get_scene_name(edit, pattern)
if this_scene_name is not None:
if current_scene_name != this_scene_name:
grouped_edits.append([ ])
grouped_edits.append([])
current_scene_name = this_scene_name

grouped_edits[-1].append(edit)
out_list = [ ]

out_list = []
for group in grouped_edits:
out_list.append({
'start': group[0].record_in,
out_list.append({
'start': group[0].record_in,
'end': group[-1].record_out,
'scene': get_scene_name(group[0], pattern ) }
)
'scene': get_scene_name(group[0], pattern)}
)

if out_format == 'cmx':
output_cmx(outfile, out_list)
if out_format == 'cols':
Expand All @@ -80,23 +84,29 @@ def scene_list(infile, outfile, out_format, pattern):


def scene_list_cli():
parser = argparse.ArgumentParser(description=
'Read video events from an input CMX EDL and output events merged into scenes.')
parser.add_argument('-o','--outfile', default=sys.stdout, type=argparse.FileType('w'),
help='Output file. Default is stdout.')
parser.add_argument('-f','--format', default='cmx', type=str,
help='Output format. Options are cols and cmx, cmx is the default.')
parser.add_argument('-p','--pattern', default='V?([A-Z]*[0-9]+)',
help='RE pattern for extracting scene name from clip name. The default is "V?([A-Z]*[0-9]+)". ' + \
'This pattern will be matched case-insensitively.')
parser.add_argument('input_edl', default=sys.stdin, type=argparse.FileType('r'), nargs='?',
help='Input file. Default is stdin.')
parser = argparse.ArgumentParser(
description='Read video events from an input CMX EDL and output '
'events merged into scenes.')
parser.add_argument('-o', '--outfile', default=sys.stdout,
type=argparse.FileType('w'),
help='Output file. Default is stdout.')
parser.add_argument('-f', '--format', default='cmx', type=str,
help='Output format. Options are cols and cmx, cmx '
'is the default.')
parser.add_argument('-p', '--pattern', default='V?([A-Z]*[0-9]+)',
help='RE pattern for extracting scene name from clip '
'name. The default is "V?([A-Z]*[0-9]+)". ' +
'This pattern will be matched case-insensitively.')
parser.add_argument('input_edl', default=sys.stdin,
type=argparse.FileType('r'), nargs='?',
help='Input file. Default is stdin.')
args = parser.parse_args()

infile = args.input_edl

scene_list(infile=infile, outfile=args.outfile , out_format=args.format, pattern=args.pattern)
scene_list(infile=infile, outfile=args.outfile,
out_format=args.format, pattern=args.pattern)


if __name__ == '__main__':
scene_list_cli()
scene_list_cli()
6 changes: 2 additions & 4 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#

import os
import sys
sys.path.insert(0, os.path.abspath('../..'))

import pycmx

# -- Project information -----------------------------------------------------

project = u'pycmx'
copyright = u'(c) 2023, Jamie Hardt'
copyright = u'(c) 2025, Jamie Hardt'
author = u'Jamie Hardt'

# The short X.Y version
version = pycmx.__version__
# The full version, including alpha/beta/rc tags
release = pycmx.__version__


# -- General configuration ---------------------------------------------------
Expand Down
4 changes: 1 addition & 3 deletions pycmx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
"""
pycmx is a parser for CMX 3600-style EDLs.
This module (c) 2023 Jamie Hardt. For more information on your rights to
This module (c) 2025 Jamie Hardt. For more information on your rights to
copy and reuse this software, refer to the LICENSE file included with the
distribution.
"""

__version__ = '1.2.2'

from .parse_cmx_events import parse_cmx3600
from .transition import Transition
from .event import Event
Expand Down
42 changes: 21 additions & 21 deletions pycmx/channel_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@
from re import (compile, match)
from typing import Dict, Tuple, Generator


class ChannelMap:
"""
Represents a set of all the channels to which an event applies.
"""

_chan_map : Dict[str, Tuple] = {
"V" : (True, False, False),
"A" : (False, True, False),
"A2" : (False, False, True),
"AA" : (False, True, True),
"B" : (True, True, False),
"AA/V" : (True, True, True),
"A2/V" : (True, False, True)
}
_chan_map: Dict[str, Tuple] = {
"V": (True, False, False),
"A": (False, True, False),
"A2": (False, False, True),
"AA": (False, True, True),
"B": (True, True, False),
"AA/V": (True, True, True),
"A2/V": (True, False, True)
}

def __init__(self, v=False, audio_channels=set()):
self._audio_channel_set = audio_channels
self._audio_channel_set = audio_channels
self.v = v

@property
Expand All @@ -46,7 +47,7 @@ def a1(self) -> bool:

@a1.setter
def a1(self, val: bool):
self.set_audio_channel(1,val)
self.set_audio_channel(1, val)

@property
def a2(self) -> bool:
Expand All @@ -55,7 +56,7 @@ def a2(self) -> bool:

@a2.setter
def a2(self, val: bool):
self.set_audio_channel(2,val)
self.set_audio_channel(2, val)

@property
def a3(self) -> bool:
Expand All @@ -64,28 +65,28 @@ def a3(self) -> bool:

@a3.setter
def a3(self, val: bool):
self.set_audio_channel(3,val)
self.set_audio_channel(3, val)

@property
def a4(self) -> bool:
"""True if A4 is included"""
return self.get_audio_channel(4)

@a4.setter
def a4(self,val: bool):
self.set_audio_channel(4,val)
def a4(self, val: bool):
self.set_audio_channel(4, val)

def get_audio_channel(self, chan_num) -> bool:
"""True if chan_num is included"""
return (chan_num in self._audio_channel_set)

def set_audio_channel(self,chan_num, enabled: bool):
def set_audio_channel(self, chan_num, enabled: bool):
"""If enabled is true, chan_num will be included"""
if enabled:
self._audio_channel_set.add(chan_num)
elif self.get_audio_channel(chan_num):
self._audio_channel_set.remove(chan_num)

def _append_event(self, event_str):
alt_channel_re = compile(r'^A(\d+)')
if event_str in self._chan_map:
Expand All @@ -96,7 +97,7 @@ def _append_event(self, event_str):
else:
matchresult = match(alt_channel_re, event_str)
if matchresult:
self.set_audio_channel(int( matchresult.group(1)), True )
self.set_audio_channel(int(matchresult.group(1)), True)

def _append_ext(self, audio_ext):
self.a3 = audio_ext.audio3
Expand All @@ -109,5 +110,4 @@ def __or__(self, other):
out_v = self.video | other.video
out_a = self._audio_channel_set | other._audio_channel_set

return ChannelMap(v=out_v,audio_channels = out_a)

return ChannelMap(v=out_v, audio_channels=out_a)
27 changes: 16 additions & 11 deletions pycmx/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@

from typing import Optional


class Edit:
"""
An individual source-to-record operation, with a source roll, source and
An individual source-to-record operation, with a source roll, source and
recorder timecode in and out, a transition and channels.
"""
def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement, trans_name_statement = None):

def __init__(self, edit_statement, audio_ext_statement,
clip_name_statement, source_file_statement,
trans_name_statement=None):
self.edit_statement = edit_statement
self.audio_ext = audio_ext_statement
self.clip_name_statement = clip_name_statement
self.source_file_statement = source_file_statement
self.trans_name_statement = trans_name_statement
self.trans_name_statement = trans_name_statement

@property
def line_number(self) -> int:
"""
Get the line number for the "standard form" statement associated with
this edit. Line numbers a zero-indexed, such that the
this edit. Line numbers a zero-indexed, such that the
"TITLE:" record is line zero.
"""
return self.edit_statement.line_number
Expand All @@ -35,7 +39,7 @@ def channels(self) -> ChannelMap:
"""
cm = ChannelMap()
cm._append_event(self.edit_statement.channels)
if self.audio_ext != None:
if self.audio_ext is not None:
cm._append_ext(self.audio_ext)
return cm

Expand All @@ -45,10 +49,13 @@ def transition(self) -> Transition:
Get the :obj:`Transition` object associated with this edit.
"""
if self.trans_name_statement:
return Transition(self.edit_statement.trans, self.edit_statement.trans_op, self.trans_name_statement.name)
return Transition(self.edit_statement.trans,
self.edit_statement.trans_op,
self.trans_name_statement.name)
else:
return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None)

return Transition(self.edit_statement.trans,
self.edit_statement.trans_op, None)

@property
def source_in(self) -> str:
"""
Expand Down Expand Up @@ -116,13 +123,11 @@ def source_file(self) -> Optional[str]:
@property
def clip_name(self) -> Optional[str]:
"""
Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
NAME" remark on the EDL. This will return None if the information is
not present.
"""
if self.clip_name_statement is None:
return None
else:
return self.clip_name_statement.name


Loading

0 comments on commit 2dcfdab

Please sign in to comment.