Merge branch 'master' of github.com:iluvcapra/pycmx

This commit is contained in:
Jamie Hardt
2018-12-07 10:00:30 -08:00
7 changed files with 102 additions and 86 deletions

7
.travis.yml Normal file
View File

@@ -0,0 +1,7 @@
language: python
python:
- "3.6"
script:
- "python3 setup.py test"
install:
- "pip3 install setuptools"

View File

@@ -1,3 +1,5 @@
[![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx)
# pycmx # pycmx
The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and its most most common variations. The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and its most most common variations.
@@ -8,39 +10,44 @@ The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and it
formats are automatically detected and properly read. formats are automatically detected and properly read.
* Remark or comment fields with common recognized forms are read and * Remark or comment fields with common recognized forms are read and
available to the client, including clip name and source file data. available to the client, including clip name and source file data.
* Symbolically decodes transitions
## Usage ## Usage
``` ```
>>> import pycmx >>> import pycmx
>>> events = pycmx.parse_cmx3600("INS4_R1_010417.edl") >>> result = pycmx.parse_cmx3600("STP R1 v082517.edl")
>>> print(events[5:8]) >>> print(resul[0:3])
[CmxEvent(title='INS4_R1_010417', number='000006', [CmxEvent(title='STP_Reel 1_082517',number=1,
clip_name='V1A-6A', source_name='A192C008_160909_R1BY', clip_name='FKI_LEADER_HEAD_1920X1080.MOV',
channels=CmxChannelMap(v=True, audio_channels=set()), source_name='FKI_LEADER_HEAD_1920X1080.MOV',
source_start='19:26:38:13', source_finish='19:27:12:03', channels=CmxChannelMap(v=True, audio_channels=set()),
record_start='01:00:57:15', record_finish='01:01:31:05', transition=CmxTransition(transition='C',operand=''),
fcm_drop=False), source_start='01:00:00:00',source_finish='01:00:08:00',
CmxEvent(title='INS4_R1_010417', number='000007', record_start='01:00:00:00',record_finish='01:00:08:00',
clip_name='1-4A', source_name='A188C004_160908_R1BY', fcm_drop=False,remarks=[],line_number=2),
channels=CmxChannelMap(v=True, audio_channels=set()), CmxEvent(title='STP_Reel 1_082517',number=2,
source_start='19:29:48:01', source_finish='19:30:01:00', clip_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
record_start='01:01:31:05', record_finish='01:01:44:04', source_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
fcm_drop=False), channels=CmxChannelMap(v=True, audio_channels=set()),
CmxEvent(title='INS4_R1_010417', number='000008', transition=CmxTransition(transition='C',operand=''),
clip_name='2G-3', source_name='A056C007_160819_R1BY', source_start='01:00:00:00',source_finish='01:00:14:23',
channels=CmxChannelMap(v=True, audio_channels=set()), record_start='01:00:00:00',record_finish='01:00:23:00',
source_start='19:56:27:14', source_finish='19:56:41:00', fcm_drop=False,remarks=[],line_number=5),
record_start='01:01:44:04', record_finish='01:01:57:14', CmxEvent(title='STP_Reel 1_082517',number=3,
fcm_drop=False)] clip_name='V4L-1*',
source_name='B116C001_150514_R0UR',
channels=CmxChannelMap(v=True, audio_channels=set()),
transition=CmxTransition(transition='C',operand=''),
source_start='16:37:29:06',source_finish='16:37:40:22',
record_start='16:37:29:06',record_finish='01:00:50:09',
fcm_drop=False,remarks=[],line_number=8)]
``` ```
## Known Issues/Roadmap ## Known Issues/Roadmap
To be addressed: To be addressed:
* Does not decode transitions.
* Does not decode "M2" speed changes. * Does not decode "M2" speed changes.
* Does not decode repair notes, audio notes or other Avid-specific notes. * Does not decode repair notes, audio notes or other Avid-specific notes.
* Does not decode Avid marker list. * Does not decode Avid marker list.

View File

@@ -2,4 +2,4 @@
from .parse_cmx import parse_cmx3600 from .parse_cmx import parse_cmx3600
__version__ = '0.5' __version__ = '0.6'

View File

@@ -12,7 +12,7 @@ class CmxEvent:
def __init__(self,title,number,clip_name,source_name,channels, def __init__(self,title,number,clip_name,source_name,channels,
transition,source_start,source_finish, transition,source_start,source_finish,
record_start, record_finish, fcm_drop, remarks = [] , record_start, record_finish, fcm_drop, remarks = [] ,
unrecognized = []): unrecognized = [], line_number = None):
self.title = title self.title = title
self.number = number self.number = number
self.clip_name = clip_name self.clip_name = clip_name
@@ -28,6 +28,7 @@ class CmxEvent:
self.unrecgonized = unrecognized self.unrecgonized = unrecognized
self.black = (source_name == 'BL') self.black = (source_name == 'BL')
self.aux_source = (source_name == 'AX') self.aux_source = (source_name == 'AX')
self.line_number = line_number
def accept_statement(self, statement): def accept_statement(self, statement):
@@ -45,12 +46,12 @@ class CmxEvent:
self.transition.name = statement.name self.transition.name = statement.name
def __repr__(self): def __repr__(self):
return f"""CmxEvent(title="{self.title}",number={self.number},\ return f"""CmxEvent(title={self.title.__repr__()},number={self.number.__repr__()},\
clip_name="{self.clip_name}",source_name="{self.source_name}",\ clip_name={self.clip_name.__repr__()},source_name={self.source_name.__repr__()},\
channels={self.channels},transition={self.transition},\ channels={self.channels.__repr__()},transition={self.transition.__repr__()},\
source_start="{self.source_start}",source_finish="{self.source_finish}",\ source_start={self.source_start.__repr__()},source_finish={self.source_finish.__repr__()},\
record_start="{self.source_start}",record_finish="{self.record_finish}",\ record_start={self.source_start.__repr__()},record_finish={self.record_finish.__repr__()},\
fcm_drop={self.fcm_drop},remarks={self.remarks})""" fcm_drop={self.fcm_drop.__repr__()},remarks={self.remarks.__repr__()},line_number={self.line_number.__repr__()})"""
class CmxTransition: class CmxTransition:
@@ -109,5 +110,5 @@ class CmxTransition:
return self.transition == 'KO' return self.transition == 'KO'
def __repr__(self): def __repr__(self):
return f"""CmxTransition(transition="{self.transition}",operand="{self.operand}")""" return f"""CmxTransition(transition={self.transition.__repr__()},operand={self.operand.__repr__()})"""

View File

@@ -110,15 +110,12 @@ class CmxChannelMap:
if matchresult: if matchresult:
self.set_audio_channel(int( matchresult.group(1)), True ) self.set_audio_channel(int( matchresult.group(1)), True )
def appendExt(self, audio_ext): def appendExt(self, audio_ext):
self.a3 = ext.audio3 self.a3 = ext.audio3
self.a4 = ext.audio4 self.a4 = ext.audio4
def __repr__(self): def __repr__(self):
return f"CmxChannelMap(v={self.v}, audio_channels={self._audio_channel_set})" return f"CmxChannelMap(v={self.v.__repr__()}, audio_channels={self._audio_channel_set.__repr__()})"
def parse_cmx3600(file): def parse_cmx3600(file):
@@ -155,7 +152,8 @@ def event_list(title, parser):
source_finish= raw_event.source_out, source_finish= raw_event.source_out,
record_start= raw_event.record_in, record_start= raw_event.record_in,
record_finish= raw_event.record_out, record_finish= raw_event.record_out,
fcm_drop= state['fcm_drop']) fcm_drop= state['fcm_drop'],
line_number = raw_event.line_number)
elif parser.accept('AudioExt') or parser.accept('ClipName') or \ elif parser.accept('AudioExt') or parser.accept('ClipName') or \
parser.accept('SourceFile') or parser.accept('EffectsName') or \ parser.accept('SourceFile') or parser.accept('EffectsName') or \
parser.accept('Remark'): parser.accept('Remark'):

View File

@@ -7,24 +7,26 @@ from .util import collimate
import re import re
import sys import sys
from collections import namedtuple from collections import namedtuple
from itertools import count
StmtTitle = namedtuple("Title",["title"]) StmtTitle = namedtuple("Title",["title","line_number"])
StmtFCM = namedtuple("FCM",["drop"]) StmtFCM = namedtuple("FCM",["drop","line_number"])
StmtEvent = namedtuple("Event",["event","source","channels","trans","trans_op","source_in","source_out","record_in","record_out"]) StmtEvent = namedtuple("Event",["event","source","channels","trans","trans_op","source_in","source_out","record_in","record_out","line_number"])
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4"]) StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
StmtClipName = namedtuple("ClipName",["name"]) StmtClipName = namedtuple("ClipName",["name","line_number"])
StmtSourceFile = namedtuple("SourceFile",["filename"]) StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
StmtRemark = namedtuple("Remark",["text"]) StmtRemark = namedtuple("Remark",["text","line_number"])
StmtEffectsName = namedtuple("EffectsName",["name"]) StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
StmtTrailer = namedtuple("Trailer",["text"]) StmtTrailer = namedtuple("Trailer",["text","line_number"])
StmtUnrecognized = namedtuple("Unrecognized",["content"]) StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
def parse_cmx3600_statements(path): def parse_cmx3600_statements(path):
with open(path,'r') as file: with open(path,'r') as file:
lines = file.readlines() lines = file.readlines()
return [parse_cmx3600_line(line.strip()) for line in lines] line_numbers = count()
return [parse_cmx3600_line(line.strip(), line_number) for (line, line_number) in zip(lines,line_numbers)]
def edl_column_widths(event_field_length, source_field_length): def edl_column_widths(event_field_length, source_field_length):
return [event_field_length,2, source_field_length,1, return [event_field_length,2, source_field_length,1,
@@ -36,83 +38,83 @@ def edl_column_widths(event_field_length, source_field_length):
11,1, 11,1,
11] 11]
def parse_cmx3600_line(line): def parse_cmx3600_line(line, line_number):
long_event_num_p = re.compile("^[0-9]{6} ") long_event_num_p = re.compile("^[0-9]{6} ")
short_event_num_p = re.compile("^[0-9]{3} ") short_event_num_p = re.compile("^[0-9]{3} ")
if isinstance(line,str): if isinstance(line,str):
if line.startswith("TITLE:"): if line.startswith("TITLE:"):
return parse_title(line) return parse_title(line,line_number)
elif line.startswith("FCM:"): elif line.startswith("FCM:"):
return parse_fcm(line) return parse_fcm(line, line_number)
elif long_event_num_p.match(line) != None: elif long_event_num_p.match(line) != None:
length_file_128 = sum(edl_column_widths(6,128)) length_file_128 = sum(edl_column_widths(6,128))
if len(line) < length_file_128: if len(line) < length_file_128:
return parse_long_standard_form(line, 32) return parse_long_standard_form(line, 32, line_number)
else: else:
return parse_long_standard_form(line, 128) return parse_long_standard_form(line, 128, line_number)
elif short_event_num_p.match(line) != None: elif short_event_num_p.match(line) != None:
return parse_standard_form(line) return parse_standard_form(line, line_number)
elif line.startswith("AUD"): elif line.startswith("AUD"):
return parse_extended_audio_channels(line) return parse_extended_audio_channels(line,line_number)
elif line.startswith("*"): elif line.startswith("*"):
return parse_remark( line[1:].strip()) return parse_remark( line[1:].strip(), line_number)
elif line.startswith(">>>"): elif line.startswith(">>>"):
return parse_trailer_statement(line) return parse_trailer_statement(line, line_number)
elif line.startswith("EFFECTS NAME IS"): elif line.startswith("EFFECTS NAME IS"):
return parse_effects_name(line) return parse_effects_name(line, line_number)
else: else:
return parse_unrecognized(line) return parse_unrecognized(line, line_number)
def parse_title(line): def parse_title(line, line_num):
title = line[6:].strip() title = line[6:].strip()
return StmtTitle(title=title) return StmtTitle(title=title,line_number=line_num)
def parse_fcm(line): def parse_fcm(line, line_num):
val = line[4:].strip() val = line[4:].strip()
if val == "DROP FRAME": if val == "DROP FRAME":
return StmtFCM(drop= True) return StmtFCM(drop= True, line_number=line_num)
else: else:
return StmtFCM(drop= False) return StmtFCM(drop= False, line_number=line_num)
def parse_long_standard_form(line,source_field_length): def parse_long_standard_form(line,source_field_length, line_number):
return parse_columns_for_standard_form(line, 6, source_field_length) return parse_columns_for_standard_form(line, 6, source_field_length, line_number)
def parse_standard_form(line): def parse_standard_form(line, line_number):
return parse_columns_for_standard_form(line, 3, 8) return parse_columns_for_standard_form(line, 3, 8, line_number)
def parse_extended_audio_channels(line): def parse_extended_audio_channels(line, line_number):
content = line.strip() content = line.strip()
if content == "AUD 3": if content == "AUD 3":
return StmtAudioExt(audio3=True, audio4=False) return StmtAudioExt(audio3=True, audio4=False, line_number=line_number)
elif content == "AUD 4": elif content == "AUD 4":
return StmtAudioExt(audio3=False, audio4=True) return StmtAudioExt(audio3=False, audio4=True, line_number=line_number)
elif content == "AUD 3 4": elif content == "AUD 3 4":
return StmtAudioExt(audio3=True, audio4=True) return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
else: else:
return StmtUnrecognized(content=line) return StmtUnrecognized(content=line, line_number=line_number)
def parse_remark(line): def parse_remark(line, line_number):
if line.startswith("FROM CLIP NAME:"): if line.startswith("FROM CLIP NAME:"):
return StmtClipName(name=line[15:].strip() ) return StmtClipName(name=line[15:].strip() , line_number=line_number)
elif line.startswith("SOURCE FILE:"): elif line.startswith("SOURCE FILE:"):
return StmtSourceFile(filename=line[12:].strip() ) return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
else: else:
return StmtRemark(text=line) return StmtRemark(text=line, line_number=line_number)
def parse_effects_name(line): def parse_effects_name(line, line_number):
name = line[16:].strip() name = line[16:].strip()
return StmtEffectsName(name=name) return StmtEffectsName(name=name, line_number=line_number)
def parse_unrecognized(line): def parse_unrecognized(line, line_number):
return StmtUnrecognized(content=line) return StmtUnrecognized(content=line, line_number=line_number)
def parse_columns_for_standard_form(line, event_field_length, source_field_length): def parse_columns_for_standard_form(line, event_field_length, source_field_length, line_number):
col_widths = edl_column_widths(event_field_length, source_field_length) col_widths = edl_column_widths(event_field_length, source_field_length)
if sum(col_widths) > len(line): if sum(col_widths) > len(line):
return StmtUnrecognized(content=line) return StmtUnrecognized(content=line, line_number=line_number)
column_strings = collimate(line,col_widths) column_strings = collimate(line,col_widths)
@@ -124,10 +126,11 @@ def parse_columns_for_standard_form(line, event_field_length, source_field_lengt
source_in=column_strings[10].strip(), source_in=column_strings[10].strip(),
source_out=column_strings[12].strip(), source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(), record_in=column_strings[14].strip(),
record_out=column_strings[16].strip()) record_out=column_strings[16].strip(),
line_number=line_number)
def parse_trailer_statement(line): def parse_trailer_statement(line, line_number):
trimmed = line[3:].strip() trimmed = line[3:].strip()
return StmtTrailer(trimmed) return StmtTrailer(trimmed, line_number=line_number)

View File

@@ -4,7 +4,7 @@ with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
setup(name='pycmx', setup(name='pycmx',
version='0.5', version='0.6',
author='Jamie Hardt', author='Jamie Hardt',
author_email='jamiehardt@me.com', author_email='jamiehardt@me.com',
description='CMX 3600 Edit Decision List Parser', description='CMX 3600 Edit Decision List Parser',