diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..55e8777 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.6" +script: + - "python3 setup.py test" +install: + - "pip3 install setuptools" diff --git a/README.md b/README.md index 9921b8d..1b68712 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx) + # pycmx 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. * Remark or comment fields with common recognized forms are read and available to the client, including clip name and source file data. +* Symbolically decodes transitions ## Usage ``` >>> import pycmx ->>> events = pycmx.parse_cmx3600("INS4_R1_010417.edl") ->>> print(events[5:8]) - [CmxEvent(title='INS4_R1_010417', number='000006', - clip_name='V1A-6A', source_name='A192C008_160909_R1BY', - channels=CmxChannelMap(v=True, audio_channels=set()), - source_start='19:26:38:13', source_finish='19:27:12:03', - record_start='01:00:57:15', record_finish='01:01:31:05', - fcm_drop=False), - CmxEvent(title='INS4_R1_010417', number='000007', - clip_name='1-4A', source_name='A188C004_160908_R1BY', - channels=CmxChannelMap(v=True, audio_channels=set()), - source_start='19:29:48:01', source_finish='19:30:01:00', - record_start='01:01:31:05', record_finish='01:01:44:04', - fcm_drop=False), - CmxEvent(title='INS4_R1_010417', number='000008', - clip_name='2G-3', source_name='A056C007_160819_R1BY', - channels=CmxChannelMap(v=True, audio_channels=set()), - source_start='19:56:27:14', source_finish='19:56:41:00', - record_start='01:01:44:04', record_finish='01:01:57:14', - fcm_drop=False)] - +>>> result = pycmx.parse_cmx3600("STP R1 v082517.edl") +>>> print(resul[0:3]) +[CmxEvent(title='STP_Reel 1_082517',number=1, + clip_name='FKI_LEADER_HEAD_1920X1080.MOV', + source_name='FKI_LEADER_HEAD_1920X1080.MOV', + channels=CmxChannelMap(v=True, audio_channels=set()), + transition=CmxTransition(transition='C',operand=''), + source_start='01:00:00:00',source_finish='01:00:08:00', + record_start='01:00:00:00',record_finish='01:00:08:00', + fcm_drop=False,remarks=[],line_number=2), +CmxEvent(title='STP_Reel 1_082517',number=2, + clip_name='BH_PRODUCTIONS_1.85_PRORES.MOV', + source_name='BH_PRODUCTIONS_1.85_PRORES.MOV', + channels=CmxChannelMap(v=True, audio_channels=set()), + transition=CmxTransition(transition='C',operand=''), + source_start='01:00:00:00',source_finish='01:00:14:23', + record_start='01:00:00:00',record_finish='01:00:23:00', + fcm_drop=False,remarks=[],line_number=5), +CmxEvent(title='STP_Reel 1_082517',number=3, + 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 To be addressed: -* Does not decode transitions. * Does not decode "M2" speed changes. * Does not decode repair notes, audio notes or other Avid-specific notes. * Does not decode Avid marker list. diff --git a/pycmx/__init__.py b/pycmx/__init__.py index d6140fe..5305336 100644 --- a/pycmx/__init__.py +++ b/pycmx/__init__.py @@ -2,4 +2,4 @@ from .parse_cmx import parse_cmx3600 -__version__ = '0.5' +__version__ = '0.6' diff --git a/pycmx/cmx_event.py b/pycmx/cmx_event.py index f5993c9..732e7e2 100644 --- a/pycmx/cmx_event.py +++ b/pycmx/cmx_event.py @@ -12,7 +12,7 @@ class CmxEvent: def __init__(self,title,number,clip_name,source_name,channels, transition,source_start,source_finish, record_start, record_finish, fcm_drop, remarks = [] , - unrecognized = []): + unrecognized = [], line_number = None): self.title = title self.number = number self.clip_name = clip_name @@ -28,6 +28,7 @@ class CmxEvent: self.unrecgonized = unrecognized self.black = (source_name == 'BL') self.aux_source = (source_name == 'AX') + self.line_number = line_number def accept_statement(self, statement): @@ -45,12 +46,12 @@ class CmxEvent: self.transition.name = statement.name def __repr__(self): - return f"""CmxEvent(title="{self.title}",number={self.number},\ -clip_name="{self.clip_name}",source_name="{self.source_name}",\ -channels={self.channels},transition={self.transition},\ -source_start="{self.source_start}",source_finish="{self.source_finish}",\ -record_start="{self.source_start}",record_finish="{self.record_finish}",\ -fcm_drop={self.fcm_drop},remarks={self.remarks})""" + return f"""CmxEvent(title={self.title.__repr__()},number={self.number.__repr__()},\ +clip_name={self.clip_name.__repr__()},source_name={self.source_name.__repr__()},\ +channels={self.channels.__repr__()},transition={self.transition.__repr__()},\ +source_start={self.source_start.__repr__()},source_finish={self.source_finish.__repr__()},\ +record_start={self.source_start.__repr__()},record_finish={self.record_finish.__repr__()},\ +fcm_drop={self.fcm_drop.__repr__()},remarks={self.remarks.__repr__()},line_number={self.line_number.__repr__()})""" class CmxTransition: @@ -109,5 +110,5 @@ class CmxTransition: return self.transition == 'KO' def __repr__(self): - return f"""CmxTransition(transition="{self.transition}",operand="{self.operand}")""" + return f"""CmxTransition(transition={self.transition.__repr__()},operand={self.operand.__repr__()})""" diff --git a/pycmx/parse_cmx.py b/pycmx/parse_cmx.py index bfd0a1a..82f9cab 100644 --- a/pycmx/parse_cmx.py +++ b/pycmx/parse_cmx.py @@ -110,15 +110,12 @@ class CmxChannelMap: if matchresult: self.set_audio_channel(int( matchresult.group(1)), True ) - - - def appendExt(self, audio_ext): self.a3 = ext.audio3 self.a4 = ext.audio4 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): @@ -155,7 +152,8 @@ def event_list(title, parser): source_finish= raw_event.source_out, record_start= raw_event.record_in, 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 \ parser.accept('SourceFile') or parser.accept('EffectsName') or \ parser.accept('Remark'): diff --git a/pycmx/parse_cmx_statements.py b/pycmx/parse_cmx_statements.py index fd39607..3bc6e9e 100644 --- a/pycmx/parse_cmx_statements.py +++ b/pycmx/parse_cmx_statements.py @@ -7,24 +7,26 @@ from .util import collimate import re import sys from collections import namedtuple +from itertools import count -StmtTitle = namedtuple("Title",["title"]) -StmtFCM = namedtuple("FCM",["drop"]) -StmtEvent = namedtuple("Event",["event","source","channels","trans","trans_op","source_in","source_out","record_in","record_out"]) -StmtAudioExt = namedtuple("AudioExt",["audio3","audio4"]) -StmtClipName = namedtuple("ClipName",["name"]) -StmtSourceFile = namedtuple("SourceFile",["filename"]) -StmtRemark = namedtuple("Remark",["text"]) -StmtEffectsName = namedtuple("EffectsName",["name"]) -StmtTrailer = namedtuple("Trailer",["text"]) -StmtUnrecognized = namedtuple("Unrecognized",["content"]) +StmtTitle = namedtuple("Title",["title","line_number"]) +StmtFCM = namedtuple("FCM",["drop","line_number"]) +StmtEvent = namedtuple("Event",["event","source","channels","trans","trans_op","source_in","source_out","record_in","record_out","line_number"]) +StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"]) +StmtClipName = namedtuple("ClipName",["name","line_number"]) +StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) +StmtRemark = namedtuple("Remark",["text","line_number"]) +StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) +StmtTrailer = namedtuple("Trailer",["text","line_number"]) +StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"]) def parse_cmx3600_statements(path): with open(path,'r') as file: 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): 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] -def parse_cmx3600_line(line): +def parse_cmx3600_line(line, line_number): long_event_num_p = re.compile("^[0-9]{6} ") short_event_num_p = re.compile("^[0-9]{3} ") if isinstance(line,str): if line.startswith("TITLE:"): - return parse_title(line) + return parse_title(line,line_number) elif line.startswith("FCM:"): - return parse_fcm(line) + return parse_fcm(line, line_number) elif long_event_num_p.match(line) != None: length_file_128 = sum(edl_column_widths(6,128)) if len(line) < length_file_128: - return parse_long_standard_form(line, 32) + return parse_long_standard_form(line, 32, line_number) 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: - return parse_standard_form(line) + return parse_standard_form(line, line_number) elif line.startswith("AUD"): - return parse_extended_audio_channels(line) + return parse_extended_audio_channels(line,line_number) elif line.startswith("*"): - return parse_remark( line[1:].strip()) + return parse_remark( line[1:].strip(), line_number) elif line.startswith(">>>"): - return parse_trailer_statement(line) + return parse_trailer_statement(line, line_number) elif line.startswith("EFFECTS NAME IS"): - return parse_effects_name(line) + return parse_effects_name(line, line_number) 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() - 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() if val == "DROP FRAME": - return StmtFCM(drop= True) + return StmtFCM(drop= True, line_number=line_num) else: - return StmtFCM(drop= False) + return StmtFCM(drop= False, line_number=line_num) -def parse_long_standard_form(line,source_field_length): - return parse_columns_for_standard_form(line, 6, 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, line_number) -def parse_standard_form(line): - return parse_columns_for_standard_form(line, 3, 8) +def parse_standard_form(line, line_number): + 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() if content == "AUD 3": - return StmtAudioExt(audio3=True, audio4=False) + return StmtAudioExt(audio3=True, audio4=False, line_number=line_number) 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": - return StmtAudioExt(audio3=True, audio4=True) + return StmtAudioExt(audio3=True, audio4=True, line_number=line_number) 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:"): - return StmtClipName(name=line[15:].strip() ) + return StmtClipName(name=line[15:].strip() , line_number=line_number) elif line.startswith("SOURCE FILE:"): - return StmtSourceFile(filename=line[12:].strip() ) + return StmtSourceFile(filename=line[12:].strip() , line_number=line_number) 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() - return StmtEffectsName(name=name) + return StmtEffectsName(name=name, line_number=line_number) -def parse_unrecognized(line): - return StmtUnrecognized(content=line) +def parse_unrecognized(line, line_number): + 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) if sum(col_widths) > len(line): - return StmtUnrecognized(content=line) + return StmtUnrecognized(content=line, line_number=line_number) 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_out=column_strings[12].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() - return StmtTrailer(trimmed) + return StmtTrailer(trimmed, line_number=line_number) diff --git a/setup.py b/setup.py index 759e52c..600ab01 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ with open("README.md", "r") as fh: long_description = fh.read() setup(name='pycmx', - version='0.5', + version='0.6', author='Jamie Hardt', author_email='jamiehardt@me.com', description='CMX 3600 Edit Decision List Parser',