mirror of
https://github.com/iluvcapra/pycmx.git
synced 2025-12-31 08:50:54 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50d48708e9 | ||
|
|
f67c4ac2c5 | ||
|
|
1b8a3c3288 | ||
|
|
b37b57d7c9 | ||
|
|
a9937683e5 | ||
|
|
4fae65fa8d | ||
|
|
566e6257f4 | ||
|
|
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 |
26
.github/workflows/pythonpublish.yml
vendored
Normal file
26
.github/workflows/pythonpublish.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/pycmx.iml" filepath="$PROJECT_DIR$/.idea/pycmx.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/pycmx.iml
generated
Normal file
11
.idea/pycmx.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,7 +1,18 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.8"
|
||||
- "3.7"
|
||||
- "3.6"
|
||||
- "3.5"
|
||||
- "2.7"
|
||||
script:
|
||||
- "python3 setup.py test"
|
||||
- "python setup.py test"
|
||||
- "py.test tests/ -v --cov pycmx --cov-report term-missing"
|
||||
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
|
||||
@@ -81,33 +83,3 @@ Audio channel 7 is present
|
||||
>>> events[2].edits[0].channels.video
|
||||
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!
|
||||
|
||||
@@ -8,7 +8,7 @@ copy and reuse this software, refer to the LICENSE file included with the
|
||||
distribution.
|
||||
"""
|
||||
|
||||
__version__ = '0.8'
|
||||
__version__ = '1.1.1'
|
||||
__author__ = 'Jamie Hardt'
|
||||
|
||||
from .parse_cmx_events import parse_cmx3600
|
||||
|
||||
@@ -27,6 +27,11 @@ class ChannelMap:
|
||||
'True if video is included'
|
||||
return self.v
|
||||
|
||||
@property
|
||||
def audio(self):
|
||||
'True if an audio channel is included'
|
||||
return len(self._audio_channel_set) > 0
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
'A generator for each audio channel'
|
||||
@@ -96,3 +101,11 @@ class ChannelMap:
|
||||
self.a3 = ext.audio3
|
||||
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
|
||||
# (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 .channel_map import ChannelMap
|
||||
|
||||
class EditList:
|
||||
"""
|
||||
@@ -13,6 +14,42 @@ class EditList:
|
||||
self.title_statement = statements[0]
|
||||
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
|
||||
def title(self):
|
||||
"""
|
||||
@@ -54,8 +91,21 @@ class EditList:
|
||||
else:
|
||||
event_statements.append(stmt)
|
||||
|
||||
elif type(stmt) is StmtSourceUMID:
|
||||
break
|
||||
else:
|
||||
event_statements.append(stmt)
|
||||
|
||||
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"])
|
||||
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"])
|
||||
"trans_op","source_in","source_out","record_in","record_out","format","line_number"])
|
||||
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
|
||||
StmtClipName = namedtuple("ClipName",["name","affect","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"])
|
||||
StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
|
||||
StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"])
|
||||
StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
|
||||
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)
|
||||
elif line.startswith("*"):
|
||||
return _parse_remark( line[1:].strip(), line_number)
|
||||
elif line.startswith(">>>"):
|
||||
return _parse_trailer_statement(line, line_number)
|
||||
elif line.startswith(">>> SOURCE"):
|
||||
return _parse_source_umid_statement(line, line_number)
|
||||
elif line.startswith("EFFECTS NAME IS"):
|
||||
return _parse_effects_name(line, line_number)
|
||||
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(),
|
||||
record_in=column_strings[14].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()
|
||||
return StmtTrailer(trimmed, line_number=line_number)
|
||||
return StmtSourceUMID(name=None, umid=None, line_number=line_number)
|
||||
|
||||
|
||||
12
setup.py
12
setup.py
@@ -4,16 +4,22 @@ with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(name='pycmx',
|
||||
version='0.8',
|
||||
version='1.1.1',
|
||||
author='Jamie Hardt',
|
||||
author_email='jamiehardt@me.com',
|
||||
description='CMX 3600 Edit Decision List Parser',
|
||||
long_description_content_type="text/markdown",
|
||||
long_description=long_description,
|
||||
url='https://github.com/iluvcapra/pycmx',
|
||||
classifiers=['Development Status :: 4 - Beta',
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'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',
|
||||
'Programming Language :: Python :: 3.8'
|
||||
],
|
||||
packages=['pycmx'])
|
||||
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_parse
|
||||
@@ -18,14 +18,15 @@ class TestParse(TestCase):
|
||||
counts = [ 287, 466, 250 , 376, 120 , 3 , 466 ]
|
||||
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
self.assertTrue( type(edl.title) is str )
|
||||
self.assertTrue( len(edl.title) > 0 )
|
||||
@@ -33,7 +34,8 @@ class TestParse(TestCase):
|
||||
|
||||
def test_event_sanity(self):
|
||||
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)
|
||||
for index, event in enumerate(edl.events):
|
||||
self.assertTrue( len(event.edits) > 0 )
|
||||
@@ -64,6 +66,7 @@ class TestParse(TestCase):
|
||||
self.assertFalse( events[0].edits[0].channels.a1)
|
||||
self.assertTrue( events[0].edits[0].channels.a2)
|
||||
self.assertTrue( events[2].edits[0].channels.get_audio_channel(7) )
|
||||
self.assertTrue( events[2].edits[0].channels.audio)
|
||||
|
||||
|
||||
def test_multi_edit_events(self):
|
||||
@@ -104,3 +107,7 @@ class TestParse(TestCase):
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" )
|
||||
|
||||
|
||||
# add test for edit_list.channels
|
||||
|
||||
|
||||
Reference in New Issue
Block a user