mirror of
https://github.com/iluvcapra/pycmx.git
synced 2025-12-31 08:50:54 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c56d2066ad | ||
|
|
8b49a788ae | ||
|
|
b31450f03d | ||
|
|
5d14c3177a | ||
|
|
08dea6031d | ||
|
|
d23fa33558 | ||
|
|
fcc4732d1a | ||
|
|
47c1ad96f0 | ||
|
|
804f649570 | ||
|
|
58483198c3 | ||
|
|
aa01e9ad2d | ||
|
|
464052f510 | ||
|
|
3ba28a61dd | ||
|
|
b80339267a | ||
|
|
27d1073f8c | ||
|
|
0840ade312 | ||
|
|
a52e4329ce | ||
|
|
47772b21d0 | ||
|
|
7b4a76448e | ||
|
|
c6c5d15e09 | ||
|
|
c439ea1fbe | ||
|
|
68f118ab6b | ||
|
|
a20491297e | ||
|
|
9d57c2d374 | ||
|
|
1882cc5308 | ||
|
|
c57fe94335 | ||
|
|
007661ef38 | ||
|
|
f34c6dd4db | ||
|
|
eb89708bab | ||
|
|
9fde608fa0 | ||
|
|
4c4ca428f2 | ||
|
|
fe4e3b9d85 | ||
|
|
119467a884 | ||
|
|
b5a3285e64 | ||
|
|
af1c532a67 |
14
.travis.yml
14
.travis.yml
@@ -1,7 +1,17 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- "3.7"
|
||||||
- "3.6"
|
- "3.6"
|
||||||
|
- "3.5"
|
||||||
|
- "2.7"
|
||||||
script:
|
script:
|
||||||
- "python3 setup.py test"
|
- "python setup.py test"
|
||||||
|
- "py.test tests/ -v --cov pycmx --cov-report term-missing"
|
||||||
install:
|
install:
|
||||||
- "pip3 install setuptools"
|
- "pip install setuptools"
|
||||||
|
before_install:
|
||||||
|
- "pip install coverage"
|
||||||
|
- "pip install codecov"
|
||||||
|
- "pip install pytest-cov==2.5.0"
|
||||||
|
after_success:
|
||||||
|
- "codecov"
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -1,4 +1,6 @@
|
|||||||
[](https://travis-ci.com/iluvcapra/pycmx) [](https://pycmx.readthedocs.io/en/latest/?badge=latest)
|
[](https://travis-ci.com/iluvcapra/pycmx)
|
||||||
|
[](https://codecov.io/gh/iluvcapra/pycmx)
|
||||||
|
[](https://pycmx.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/pycmx/) 
|
||||||
|
|
||||||
|
|
||||||
# pycmx
|
# pycmx
|
||||||
@@ -81,33 +83,3 @@ Audio channel 7 is present
|
|||||||
>>> events[2].edits[0].channels.video
|
>>> events[2].edits[0].channels.video
|
||||||
False
|
False
|
||||||
```
|
```
|
||||||
|
|
||||||
## How is this different from `python-edl`?
|
|
||||||
|
|
||||||
There are two important differences between `import edl` and `import pycmx`
|
|
||||||
and motivated my development of this module.
|
|
||||||
|
|
||||||
1. The `pycmx` parser doesn't take timecode or framerates into account,
|
|
||||||
and strictly treats timecodes like opaque values. As far as `pycmx` is
|
|
||||||
concerend, they're just strings. This was done because in my experience,
|
|
||||||
the frame rate of an EDL is often difficult to precisely determine and
|
|
||||||
often the frame rate of various sources is different from the frame rate
|
|
||||||
of the target track.
|
|
||||||
|
|
||||||
In any event, timecodes in an EDL are a kind of *address* and are not
|
|
||||||
exactly scalar, they're meant to point to a particular block of video or
|
|
||||||
audio data on a medium and presuming that they refer to a real time, or
|
|
||||||
duration, or are convertible, etc. isn't always safe.
|
|
||||||
|
|
||||||
2. The `pycmx` parser reads event numbers and keeps track of which EDL rows
|
|
||||||
are meant to happen "at the same time," with two decks. This makes it
|
|
||||||
easier to reconstruct transition A/B clips, and read clip names from
|
|
||||||
such events appropriately.
|
|
||||||
|
|
||||||
## Should I Use This?
|
|
||||||
|
|
||||||
At this time, this is (at best) beta software. I feel like the interface is
|
|
||||||
about where where I'd like it to be but more testing is required.
|
|
||||||
|
|
||||||
Contributions are welcome and will make this module production-ready all the
|
|
||||||
faster! Please reach out or file a ticket!
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
pycmx is a module for parsing CMX 3600-style EDLs. For more information and
|
pycmx is a module for parsing CMX 3600-style EDLs. For more information and
|
||||||
examples see README.md
|
examples see README.md
|
||||||
|
|
||||||
This module (c) 2018 Jamie Hardt. For more information on your rights to
|
This module (c) 2018 Jamie Hardt. For more information on your rights to
|
||||||
copy and reuse this software, refer to the LICENSE file included with the
|
copy and reuse this software, refer to the LICENSE file included with the
|
||||||
distribution.
|
distribution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '0.8'
|
__version__ = '1.0'
|
||||||
__author__ = 'Jamie Hardt'
|
__author__ = 'Jamie Hardt'
|
||||||
|
|
||||||
from .parse_cmx_events import parse_cmx3600
|
from .parse_cmx_events import parse_cmx3600
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .edit import Edit
|
from .edit import Edit
|
||||||
|
|||||||
@@ -96,3 +96,11 @@ class ChannelMap:
|
|||||||
self.a3 = ext.audio3
|
self.a3 = ext.audio3
|
||||||
self.a4 = ext.audio4
|
self.a4 = ext.audio4
|
||||||
|
|
||||||
|
def __or__(self, other):
|
||||||
|
"""
|
||||||
|
Return the logical union of this channel map with another
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2018 Jamie Hardt
|
# (c) 2018 Jamie Hardt
|
||||||
|
|
||||||
from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent)
|
from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
from .channel_map import ChannelMap
|
||||||
|
|
||||||
class EditList:
|
class EditList:
|
||||||
"""
|
"""
|
||||||
@@ -13,6 +14,42 @@ class EditList:
|
|||||||
self.title_statement = statements[0]
|
self.title_statement = statements[0]
|
||||||
self.event_statements = statements[1:]
|
self.event_statements = statements[1:]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format(self):
|
||||||
|
"""
|
||||||
|
The detected format of the EDL. Possible values are: `3600`,`File32`,
|
||||||
|
`File128`, and `unknown`
|
||||||
|
"""
|
||||||
|
first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
|
||||||
|
|
||||||
|
if first_event:
|
||||||
|
if first_event.format == 8:
|
||||||
|
return '3600'
|
||||||
|
elif first_event.format == 32:
|
||||||
|
return 'File32'
|
||||||
|
elif first_event.format == 128:
|
||||||
|
return 'File128'
|
||||||
|
else:
|
||||||
|
return 'unknown'
|
||||||
|
else:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channels(self):
|
||||||
|
"""
|
||||||
|
Return the union of every channel channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
retval = ChannelMap()
|
||||||
|
for event in self.events:
|
||||||
|
for edit in event.edits:
|
||||||
|
retval = retval | edit.channels
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
"""
|
"""
|
||||||
@@ -54,8 +91,21 @@ class EditList:
|
|||||||
else:
|
else:
|
||||||
event_statements.append(stmt)
|
event_statements.append(stmt)
|
||||||
|
|
||||||
|
elif type(stmt) is StmtSourceUMID:
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
event_statements.append(stmt)
|
event_statements.append(stmt)
|
||||||
|
|
||||||
yield Event(statements=event_statements)
|
yield Event(statements=event_statements)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sources(self):
|
||||||
|
"""
|
||||||
|
A generator for all of the sources in the list
|
||||||
|
"""
|
||||||
|
|
||||||
|
for stmt in self.event_statements:
|
||||||
|
if type(stmt) is StmtSourceUMID:
|
||||||
|
yield stmt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ from .util import collimate
|
|||||||
StmtTitle = namedtuple("Title",["title","line_number"])
|
StmtTitle = namedtuple("Title",["title","line_number"])
|
||||||
StmtFCM = namedtuple("FCM",["drop","line_number"])
|
StmtFCM = namedtuple("FCM",["drop","line_number"])
|
||||||
StmtEvent = namedtuple("Event",["event","source","channels","trans",\
|
StmtEvent = namedtuple("Event",["event","source","channels","trans",\
|
||||||
"trans_op","source_in","source_out","record_in","record_out","line_number"])
|
"trans_op","source_in","source_out","record_in","record_out","format","line_number"])
|
||||||
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
|
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
|
||||||
StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
|
StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
|
||||||
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
|
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
|
||||||
StmtRemark = namedtuple("Remark",["text","line_number"])
|
StmtRemark = namedtuple("Remark",["text","line_number"])
|
||||||
StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
|
StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
|
||||||
StmtTrailer = namedtuple("Trailer",["text","line_number"])
|
StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
|
||||||
StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"])
|
StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"])
|
||||||
StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
|
StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
|
||||||
StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
|
StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
|
||||||
@@ -69,8 +69,8 @@ def _parse_cmx3600_line(line, line_number):
|
|||||||
return _parse_extended_audio_channels(line,line_number)
|
return _parse_extended_audio_channels(line,line_number)
|
||||||
elif line.startswith("*"):
|
elif line.startswith("*"):
|
||||||
return _parse_remark( line[1:].strip(), line_number)
|
return _parse_remark( line[1:].strip(), line_number)
|
||||||
elif line.startswith(">>>"):
|
elif line.startswith(">>> SOURCE"):
|
||||||
return _parse_trailer_statement(line, line_number)
|
return _parse_source_umid_statement(line, line_number)
|
||||||
elif line.startswith("EFFECTS NAME IS"):
|
elif line.startswith("EFFECTS NAME IS"):
|
||||||
return _parse_effects_name(line, line_number)
|
return _parse_effects_name(line, line_number)
|
||||||
elif line.startswith("SPLIT:"):
|
elif line.startswith("SPLIT:"):
|
||||||
@@ -157,10 +157,11 @@ def _parse_columns_for_standard_form(line, event_field_length, source_field_leng
|
|||||||
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)
|
line_number=line_number,
|
||||||
|
format=source_field_length)
|
||||||
|
|
||||||
|
|
||||||
def _parse_trailer_statement(line, line_number):
|
def _parse_source_umid_statement(line, line_number):
|
||||||
trimmed = line[3:].strip()
|
trimmed = line[3:].strip()
|
||||||
return StmtTrailer(trimmed, line_number=line_number)
|
return StmtSourceUMID(name=None, umid=None, line_number=line_number)
|
||||||
|
|
||||||
|
|||||||
2
pypi_upload.sh
Executable file
2
pypi_upload.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
|
||||||
11
setup.py
11
setup.py
@@ -4,16 +4,21 @@ with open("README.md", "r") as fh:
|
|||||||
long_description = fh.read()
|
long_description = fh.read()
|
||||||
|
|
||||||
setup(name='pycmx',
|
setup(name='pycmx',
|
||||||
version='0.8',
|
version='1.0.1',
|
||||||
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',
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
url='https://github.com/iluvcapra/pycmx',
|
url='https://github.com/iluvcapra/pycmx',
|
||||||
classifiers=['Development Status :: 4 - Beta',
|
classifiers=['Development Status :: 5 - Production/Stable',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Topic :: Multimedia',
|
'Topic :: Multimedia',
|
||||||
'Topic :: Multimedia :: Video',
|
'Topic :: Multimedia :: Video',
|
||||||
'Topic :: Text Processing'],
|
'Topic :: Text Processing',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
],
|
||||||
packages=['pycmx'])
|
packages=['pycmx'])
|
||||||
|
|||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_parse
|
||||||
@@ -18,22 +18,24 @@ class TestParse(TestCase):
|
|||||||
counts = [ 287, 466, 250 , 376, 120 , 3 , 466 ]
|
counts = [ 287, 466, 250 , 376, 120 , 3 , 466 ]
|
||||||
|
|
||||||
for fn, count in zip(type(self).files, counts):
|
for fn, count in zip(type(self).files, counts):
|
||||||
with open(f"tests/edls/{fn}" ,'r') as f:
|
with open("tests/edls/" + fn ,'r') as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
actual = len( list( edl.events ))
|
actual = len( list( edl.events ))
|
||||||
self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}")
|
self.assertTrue( actual == count ,
|
||||||
|
"expected %i in file %s but found %i" % (count, fn, actual))
|
||||||
|
|
||||||
def test_list_sanity(self):
|
def test_list_sanity(self):
|
||||||
for fn in type(self).files:
|
for fn in type(self).files:
|
||||||
with open(f"tests/edls/{fn}" ,'r') as f:
|
with open("tests/edls/" + fn ,'r') as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
self.assertTrue( type(edl.title) is str )
|
self.assertTrue( type(edl.title) is str )
|
||||||
self.assertTrue( len(edl.title) > 0 )
|
self.assertTrue( len(edl.title) > 0 )
|
||||||
|
|
||||||
|
|
||||||
def test_event_sanity(self):
|
def test_event_sanity(self):
|
||||||
for fn in type(self).files:
|
for fn in type(self).files:
|
||||||
with open(f"tests/edls/{fn}" ,'r') as f:
|
path = "tests/edls/" + fn
|
||||||
|
with open(path ,'r') as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
for index, event in enumerate(edl.events):
|
for index, event in enumerate(edl.events):
|
||||||
self.assertTrue( len(event.edits) > 0 )
|
self.assertTrue( len(event.edits) > 0 )
|
||||||
@@ -45,7 +47,7 @@ class TestParse(TestCase):
|
|||||||
with open("tests/edls/TEST.edl",'r') as f:
|
with open("tests/edls/TEST.edl",'r') as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list( edl.events )
|
events = list( edl.events )
|
||||||
|
|
||||||
self.assertEqual( events[0].number , 1)
|
self.assertEqual( events[0].number , 1)
|
||||||
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
|
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
|
||||||
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
|
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
|
||||||
@@ -104,3 +106,7 @@ class TestParse(TestCase):
|
|||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" )
|
self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" )
|
||||||
|
|
||||||
|
|
||||||
|
# add test for edit_list.channels
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user