mirror of
https://github.com/iluvcapra/pycmx.git
synced 2025-12-31 08:50:54 +00:00
Compare commits
76 Commits
v1.1.5
...
14-issue-p
| Author | SHA1 | Date | |
|---|---|---|---|
| b85e02585c | |||
| 88f3a7a659 | |||
| 959b824dcd | |||
| 0c6a3896fb | |||
| a128b6ca7d | |||
| 79778bb847 | |||
|
|
81997c763e | ||
|
|
6da04b2c07 | ||
|
|
89b6cde808 | ||
|
|
5e4dde5aa2 | ||
|
|
b6379ec1fe | ||
|
|
04731d634f | ||
|
|
2dcfdabf01 | ||
|
|
7ed423deaf | ||
|
|
2be779fe53 | ||
|
|
2adff6dd01 | ||
|
|
7871acdfd0 | ||
|
|
8bb6dad1da | ||
|
|
f88b82e8fb | ||
|
|
a86ef7ed2e | ||
|
|
1e1331eadb | ||
|
|
7a0481dbf9 | ||
|
|
20ad04e862 | ||
|
|
9e54aa5fcf | ||
|
|
71aff9baf7 | ||
|
|
ab55cab160 | ||
|
|
a717589683 | ||
|
|
f48c164e1b | ||
|
|
be1dc99e94 | ||
|
|
fbdfcddfff | ||
|
|
156828b648 | ||
|
|
1894a143b1 | ||
|
|
c0d278e079 | ||
|
|
7d3a58bff8 | ||
|
|
f1381f5f46 | ||
|
|
dc00b52b61 | ||
|
|
571ffdefd7 | ||
|
|
a79cb02139 | ||
|
|
ce1a0d32bb | ||
|
|
daa5d58a66 | ||
|
|
38ce1445a1 | ||
|
|
552d007360 | ||
|
|
d79fdcc6a8 | ||
|
|
3187a50a6b | ||
|
|
8e8d4f5753 | ||
|
|
cc371cd486 | ||
|
|
795a666a74 | ||
|
|
cd35f4f80d | ||
|
|
6525840151 | ||
|
|
b78ae05d8c | ||
|
|
51ed92f5df | ||
|
|
a6f042c76f | ||
|
|
179808fbf2 | ||
|
|
2b38d8aaf9 | ||
|
|
e19bb3c5c5 | ||
|
|
d0e2e5bbe5 | ||
|
|
e90897e503 | ||
|
|
4daa008e91 | ||
|
|
cf1d71800e | ||
|
|
fcb25ae183 | ||
|
|
97ada84bfe | ||
|
|
d157512c32 | ||
|
|
1e2d31716e | ||
|
|
6be7c54de5 | ||
|
|
a3109cdb7a | ||
|
|
3a9d81417e | ||
|
|
9838e6b357 | ||
|
|
41fdeeaf56 | ||
|
|
b3b6e57f6c | ||
|
|
62e7f10cf4 | ||
|
|
f855d3581d | ||
|
|
0b5555d333 | ||
|
|
7123a6f1bb | ||
|
|
173493a610 | ||
|
|
d20d9d1bdd | ||
|
|
d328e12283 |
5
.flake8
Normal file
5
.flake8
Normal file
@@ -0,0 +1,5 @@
|
||||
[flake8]
|
||||
per-file-ignores =
|
||||
pycmx/__init__.py: F401
|
||||
tests/__init__.py: F401
|
||||
|
||||
25
.github/workflows/bsky_test.yml
vendored
Normal file
25
.github/workflows/bsky_test.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Test Post to Bluesky
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
# id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- name: Send Bluesky Post
|
||||
uses: myConsciousness/bluesky-post@v5
|
||||
with:
|
||||
text: |
|
||||
This is a test post!
|
||||
link-preview-url: ${{ github.server_url }}/${{ github.repository }}
|
||||
identifier: ${{ secrets.BLUESKY_APP_USER }}
|
||||
password: ${{ secrets.BLUESKY_APP_PASSWORD }}
|
||||
service: bsky.social
|
||||
retry-count: 1
|
||||
8
.github/workflows/python-package.yml
vendored
8
.github/workflows/python-package.yml
vendored
@@ -16,8 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -28,13 +27,12 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
python -m pip install -e .
|
||||
- name: Lint with flake8
|
||||
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
|
||||
|
||||
38
.github/workflows/pythonpublish.yml
vendored
38
.github/workflows/pythonpublish.yml
vendored
@@ -2,25 +2,41 @@ name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5.3.0
|
||||
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: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
pip install build
|
||||
- name: Build package
|
||||
run: python -m build .
|
||||
- name: Publish to Pypi
|
||||
uses: pypa/gh-action-pypi-publish@v1.13.0
|
||||
with:
|
||||
password: ${{ secrets.PYPI_APIKEY }}
|
||||
- name: Send Bluesky Post
|
||||
uses: myConsciousness/bluesky-post@v5
|
||||
with:
|
||||
text: |
|
||||
I've released a new version of pycmx, my python module for
|
||||
reading CMX EDLs.
|
||||
link-preview-url: ${{ github.server_url }}/${{ github.repository }}
|
||||
identifier: ${{ secrets.BLUESKY_APP_USER }}
|
||||
password: ${{ secrets.BLUESKY_APP_PASSWORD }}
|
||||
service: bsky.social
|
||||
retry-count: 1
|
||||
|
||||
32
.readthedocs.yaml
Normal file
32
.readthedocs.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "16"
|
||||
# rust: "1.55"
|
||||
# golang: "1.17"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
#If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
#Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- doc
|
||||
10
README.md
10
README.md
@@ -5,12 +5,14 @@
|
||||
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
|
||||
* The major variations of the CMX 3600: the standard, "File32" and "File128"
|
||||
formats are automatically detected and properly read.
|
||||
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
||||
long Adobe Premiere event numbers are automatically detected and properly
|
||||
read.
|
||||
* Preserves relationship between events and individual edits/clips.
|
||||
* Remark or comment fields with common recognized forms are read and
|
||||
available to the client, including clip name and source file data.
|
||||
@@ -83,5 +85,3 @@ Audio channel 7 is present
|
||||
>>> events[2].edits[0].channels.video
|
||||
False
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ logging.basicConfig(format=FORMAT)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def all_video_edits(edl):
|
||||
for event in edl.events:
|
||||
for edit in event.edits:
|
||||
@@ -30,19 +31,22 @@ 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):
|
||||
@@ -51,24 +55,24 @@ def scene_list(infile, outfile, out_format, pattern):
|
||||
|
||||
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,
|
||||
'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)
|
||||
@@ -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()
|
||||
|
||||
@@ -12,21 +12,22 @@
|
||||
# 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 importlib
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
print(sys.path)
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'pycmx'
|
||||
copyright = u'2022, Jamie Hardt'
|
||||
copyright = u'(c) 2025, Jamie Hardt'
|
||||
author = u'Jamie Hardt'
|
||||
|
||||
release = importlib.metadata.version("pycmx")
|
||||
version = release
|
||||
# The short X.Y version
|
||||
version = u''
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = u''
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
@@ -144,7 +145,7 @@ latex_documents = [
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'pycmx', u'pycmx Documentation',
|
||||
[author], 1)
|
||||
[author], "3p")
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pycmx is a module for parsing CMX 3600-style EDLs. For more information and
|
||||
examples see README.md
|
||||
pycmx is a parser for CMX 3600-style EDLs.
|
||||
|
||||
This module (c) 2018 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.1.1'
|
||||
__author__ = 'Jamie Hardt'
|
||||
|
||||
from .parse_cmx_events import parse_cmx3600
|
||||
from .transition import Transition
|
||||
from .event import Event
|
||||
|
||||
@@ -2,84 +2,85 @@
|
||||
# (c) 2018 Jamie Hardt
|
||||
|
||||
from re import (compile, match)
|
||||
from typing import Dict, Tuple
|
||||
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.v = v
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
def video(self) -> bool:
|
||||
'True if video is included'
|
||||
return self.v
|
||||
|
||||
@property
|
||||
def audio(self):
|
||||
def audio(self) -> bool:
|
||||
'True if an audio channel is included'
|
||||
return len(self._audio_channel_set) > 0
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
def channels(self) -> Generator[int, None, None]:
|
||||
'A generator for each audio channel'
|
||||
for c in self._audio_channel_set:
|
||||
yield c
|
||||
|
||||
@property
|
||||
def a1(self):
|
||||
def a1(self) -> bool:
|
||||
"""True if A1 is included"""
|
||||
return self.get_audio_channel(1)
|
||||
|
||||
@a1.setter
|
||||
def a1(self,val):
|
||||
self.set_audio_channel(1,val)
|
||||
def a1(self, val: bool):
|
||||
self.set_audio_channel(1, val)
|
||||
|
||||
@property
|
||||
def a2(self):
|
||||
def a2(self) -> bool:
|
||||
"""True if A2 is included"""
|
||||
return self.get_audio_channel(2)
|
||||
|
||||
@a2.setter
|
||||
def a2(self,val):
|
||||
self.set_audio_channel(2,val)
|
||||
def a2(self, val: bool):
|
||||
self.set_audio_channel(2, val)
|
||||
|
||||
@property
|
||||
def a3(self):
|
||||
def a3(self) -> bool:
|
||||
"""True if A3 is included"""
|
||||
return self.get_audio_channel(3)
|
||||
|
||||
@a3.setter
|
||||
def a3(self,val):
|
||||
self.set_audio_channel(3,val)
|
||||
def a3(self, val: bool):
|
||||
self.set_audio_channel(3, val)
|
||||
|
||||
@property
|
||||
def a4(self):
|
||||
def a4(self) -> bool:
|
||||
"""True if A4 is included"""
|
||||
return self.get_audio_channel(4)
|
||||
|
||||
@a4.setter
|
||||
def a4(self,val):
|
||||
self.set_audio_channel(4,val)
|
||||
def a4(self, val: bool):
|
||||
self.set_audio_channel(4, val)
|
||||
|
||||
def get_audio_channel(self,chan_num):
|
||||
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):
|
||||
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)
|
||||
@@ -96,7 +97,7 @@ class ChannelMap:
|
||||
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
|
||||
@@ -109,5 +110,4 @@ class ChannelMap:
|
||||
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)
|
||||
|
||||
@@ -3,14 +3,20 @@
|
||||
|
||||
from .transition import Transition
|
||||
from .channel_map import ChannelMap
|
||||
from .parse_cmx_statements import StmtEffectsName
|
||||
# from .parse_cmx_statements import StmtEffectsName
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Edit:
|
||||
"""
|
||||
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
|
||||
@@ -18,7 +24,7 @@ class Edit:
|
||||
self.trans_name_statement = trans_name_statement
|
||||
|
||||
@property
|
||||
def line_number(self):
|
||||
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
|
||||
@@ -27,35 +33,38 @@ class Edit:
|
||||
return self.edit_statement.line_number
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
def channels(self) -> ChannelMap:
|
||||
"""
|
||||
Get the :obj:`ChannelMap` object associated with this Edit.
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
def transition(self):
|
||||
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):
|
||||
def source_in(self) -> str:
|
||||
"""
|
||||
Get the source in timecode.
|
||||
"""
|
||||
return self.edit_statement.source_in
|
||||
|
||||
@property
|
||||
def source_out(self):
|
||||
def source_out(self) -> str:
|
||||
"""
|
||||
Get the source out timecode.
|
||||
"""
|
||||
@@ -63,7 +72,7 @@ class Edit:
|
||||
return self.edit_statement.source_out
|
||||
|
||||
@property
|
||||
def record_in(self):
|
||||
def record_in(self) -> str:
|
||||
"""
|
||||
Get the record in timecode.
|
||||
"""
|
||||
@@ -71,7 +80,7 @@ class Edit:
|
||||
return self.edit_statement.record_in
|
||||
|
||||
@property
|
||||
def record_out(self):
|
||||
def record_out(self) -> str:
|
||||
"""
|
||||
Get the record out timecode.
|
||||
"""
|
||||
@@ -79,7 +88,7 @@ class Edit:
|
||||
return self.edit_statement.record_out
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
def source(self) -> str:
|
||||
"""
|
||||
Get the source column. This is the 8, 32 or 128-character string on the
|
||||
event record line, this usually references the tape name of the source.
|
||||
@@ -87,21 +96,21 @@ class Edit:
|
||||
return self.edit_statement.source
|
||||
|
||||
@property
|
||||
def black(self):
|
||||
def black(self) -> bool:
|
||||
"""
|
||||
Black video or silence should be used as the source for this event.
|
||||
"""
|
||||
return self.source == "BL"
|
||||
|
||||
@property
|
||||
def aux_source(self):
|
||||
def aux_source(self) -> bool:
|
||||
"""
|
||||
An auxiliary source is the source of this event.
|
||||
"""
|
||||
return self.source == "AX"
|
||||
|
||||
@property
|
||||
def source_file(self):
|
||||
def source_file(self) -> Optional[str]:
|
||||
"""
|
||||
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
||||
EDL. This will return None if the information is not present.
|
||||
@@ -112,7 +121,7 @@ class Edit:
|
||||
return self.source_file_statement.filename
|
||||
|
||||
@property
|
||||
def clip_name(self):
|
||||
def clip_name(self) -> Optional[str]:
|
||||
"""
|
||||
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
|
||||
@@ -122,5 +131,3 @@ class Edit:
|
||||
return None
|
||||
else:
|
||||
return self.clip_name_statement.name
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
# pycmx
|
||||
# (c) 2018 Jamie Hardt
|
||||
|
||||
from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
|
||||
from .parse_cmx_statements import (
|
||||
StmtUnrecognized, StmtEvent, StmtSourceUMID)
|
||||
from .event import Event
|
||||
from .channel_map import ChannelMap
|
||||
|
||||
from typing import Generator
|
||||
|
||||
|
||||
class EditList:
|
||||
"""
|
||||
Represents an entire edit decision list as returned by `parse_cmx3600()`.
|
||||
|
||||
Represents an entire edit decision list as returned by
|
||||
:func:`~pycmx.parse_cmx3600()`.
|
||||
"""
|
||||
|
||||
def __init__(self, statements):
|
||||
self.title_statement = statements[0]
|
||||
self.event_statements = statements[1:]
|
||||
|
||||
|
||||
@property
|
||||
def format(self):
|
||||
def format(self) -> str:
|
||||
"""
|
||||
The detected format of the EDL. Possible values are: `3600`,`File32`,
|
||||
`File128`, and `unknown`
|
||||
The detected format of the EDL. Possible values are: "3600", "File32",
|
||||
"File128", and "unknown".
|
||||
|
||||
Adobe EDLs with more than 999 events will be reported as "3600".
|
||||
"""
|
||||
first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
|
||||
first_event = next(
|
||||
(s for s in self.event_statements if type(s) is StmtEvent), None)
|
||||
|
||||
if first_event:
|
||||
if first_event.format == 8:
|
||||
@@ -35,9 +42,8 @@ class EditList:
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
def channels(self) -> ChannelMap:
|
||||
"""
|
||||
Return the union of every channel channel.
|
||||
"""
|
||||
@@ -49,17 +55,16 @@ class EditList:
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
"""
|
||||
The title of this edit list.
|
||||
"""
|
||||
return self.title_statement.title
|
||||
|
||||
|
||||
@property
|
||||
def unrecognized_statements(self):
|
||||
def unrecognized_statements(self) -> Generator[StmtUnrecognized,
|
||||
None, None]:
|
||||
"""
|
||||
A generator for all the unrecognized statements in the list.
|
||||
"""
|
||||
@@ -67,17 +72,13 @@ class EditList:
|
||||
if type(s) is StmtUnrecognized:
|
||||
yield s
|
||||
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
def events(self) -> Generator[Event, None, None]:
|
||||
'A generator for all the events in the edit list'
|
||||
is_drop = None
|
||||
current_event_num = None
|
||||
event_statements = []
|
||||
for stmt in self.event_statements:
|
||||
if type(stmt) is StmtFCM:
|
||||
is_drop = stmt.drop
|
||||
elif type(stmt) is StmtEvent:
|
||||
if type(stmt) is StmtEvent:
|
||||
if current_event_num is None:
|
||||
current_event_num = stmt.event
|
||||
event_statements.append(stmt)
|
||||
@@ -97,7 +98,7 @@ class EditList:
|
||||
yield Event(statements=event_statements)
|
||||
|
||||
@property
|
||||
def sources(self):
|
||||
def sources(self) -> Generator[StmtSourceUMID, None, None]:
|
||||
"""
|
||||
A generator for all of the sources in the list
|
||||
"""
|
||||
@@ -105,5 +106,3 @@ class EditList:
|
||||
for stmt in self.event_statements:
|
||||
if type(stmt) is StmtSourceUMID:
|
||||
yield stmt
|
||||
|
||||
|
||||
|
||||
@@ -1,73 +1,85 @@
|
||||
# pycmx
|
||||
# (c) 2018 Jamie Hardt
|
||||
# (c) 2023 Jamie Hardt
|
||||
|
||||
from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName)
|
||||
from .parse_cmx_statements import (
|
||||
StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized,
|
||||
StmtEffectsName)
|
||||
from .edit import Edit
|
||||
|
||||
from typing import List, Generator, Optional, Tuple, Any
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
Represents a collection of :class:`Edit`s, all with the same event number.
|
||||
"""
|
||||
Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
|
||||
event number. """
|
||||
|
||||
def __init__(self, statements):
|
||||
self.statements = statements
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
def number(self) -> int:
|
||||
"""
|
||||
Return the event number.
|
||||
"""
|
||||
return int(self._edit_statements()[0].event)
|
||||
|
||||
@property
|
||||
def edits(self):
|
||||
def edits(self) -> List[Edit]:
|
||||
"""
|
||||
Returns the edits. Most events will have a single edit, a single event
|
||||
will have multiple edits when a dissolve, wipe or key transition needs
|
||||
to be performed.
|
||||
"""
|
||||
edits_audio = list( self._statements_with_audio_ext() )
|
||||
clip_names = self._clip_name_statements()
|
||||
source_files= self._source_file_statements()
|
||||
edits_audio = list(self._statements_with_audio_ext())
|
||||
clip_names = self._clip_name_statements()
|
||||
source_files = self._source_file_statements()
|
||||
|
||||
the_zip = [edits_audio]
|
||||
the_zip: List[List[Any]] = [edits_audio]
|
||||
|
||||
if len(edits_audio) == 2:
|
||||
cn = [None, None]
|
||||
start_name: Optional[StmtClipName] = None
|
||||
end_name: Optional[StmtClipName] = None
|
||||
|
||||
for clip_name in clip_names:
|
||||
if clip_name.affect == 'from':
|
||||
cn[0] = clip_name
|
||||
start_name = clip_name
|
||||
elif clip_name.affect == 'to':
|
||||
cn[1] = clip_name
|
||||
end_name = clip_name
|
||||
|
||||
the_zip.append(cn)
|
||||
the_zip.append([start_name, end_name])
|
||||
else:
|
||||
if len(edits_audio) == len(clip_names):
|
||||
the_zip.append(clip_names)
|
||||
else:
|
||||
the_zip.append([None] * len(edits_audio) )
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
|
||||
if len(edits_audio) == len(source_files):
|
||||
the_zip.append(source_files)
|
||||
elif len(source_files) == 1:
|
||||
the_zip.append( source_files * len(edits_audio) )
|
||||
the_zip.append(source_files * len(edits_audio))
|
||||
else:
|
||||
the_zip.append([None] * len(edits_audio) )
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
|
||||
# attach trans name to last event
|
||||
try:
|
||||
trans_statement = self._trans_name_statements()[0]
|
||||
trans_names = [None] * (len(edits_audio) - 1)
|
||||
trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
|
||||
trans_names.append(trans_statement)
|
||||
the_zip.append(trans_names)
|
||||
except IndexError:
|
||||
the_zip.append([None] * len(edits_audio) )
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
|
||||
|
||||
return [ Edit(e1[0],e1[1],n1,s1,u1) for (e1,n1,s1,u1) in zip(*the_zip) ]
|
||||
return [Edit(edit_statement=e1[0],
|
||||
audio_ext_statement=e1[1],
|
||||
clip_name_statement=n1,
|
||||
source_file_statement=s1,
|
||||
trans_name_statement=u1)
|
||||
for (e1, n1, s1, u1) in zip(*the_zip)]
|
||||
|
||||
@property
|
||||
def unrecognized_statements(self):
|
||||
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
|
||||
None]:
|
||||
"""
|
||||
A generator for all the unrecognized statements in the event.
|
||||
"""
|
||||
@@ -75,23 +87,22 @@ class Event:
|
||||
if type(s) is StmtUnrecognized:
|
||||
yield s
|
||||
|
||||
def _trans_name_statements(self):
|
||||
def _trans_name_statements(self) -> List[StmtEffectsName]:
|
||||
return [s for s in self.statements if type(s) is StmtEffectsName]
|
||||
|
||||
def _edit_statements(self):
|
||||
def _edit_statements(self) -> List[StmtEvent]:
|
||||
return [s for s in self.statements if type(s) is StmtEvent]
|
||||
|
||||
def _clip_name_statements(self):
|
||||
def _clip_name_statements(self) -> List[StmtClipName]:
|
||||
return [s for s in self.statements if type(s) is StmtClipName]
|
||||
|
||||
def _source_file_statements(self):
|
||||
def _source_file_statements(self) -> List[StmtSourceFile]:
|
||||
return [s for s in self.statements if type(s) is StmtSourceFile]
|
||||
|
||||
def _statements_with_audio_ext(self):
|
||||
def _statements_with_audio_ext(self) -> Generator[
|
||||
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
|
||||
for (s1, s2) in zip(self.statements, self.statements[1:]):
|
||||
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
|
||||
yield (s1,s2)
|
||||
yield (s1, s2)
|
||||
elif type(s1) is StmtEvent:
|
||||
yield (s1, None)
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# pycmx
|
||||
# (c) 2018 Jamie Hardt
|
||||
|
||||
from collections import namedtuple
|
||||
# from collections import namedtuple
|
||||
|
||||
from .parse_cmx_statements import (parse_cmx3600_statements, StmtEvent,StmtFCM )
|
||||
from .parse_cmx_statements import (parse_cmx3600_statements)
|
||||
from .edit_list import EditList
|
||||
|
||||
def parse_cmx3600(f):
|
||||
from typing import TextIO
|
||||
|
||||
|
||||
def parse_cmx3600(f: TextIO) -> EditList:
|
||||
"""
|
||||
Parse a CMX 3600 EDL.
|
||||
|
||||
Args:
|
||||
f : a file-like object, anything that's readlines-able.
|
||||
|
||||
Returns:
|
||||
An :class:`pycmx.edit_list.EditList`.
|
||||
:param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
|
||||
:returns: An :class:`pycmx.edit_list.EditList`.
|
||||
"""
|
||||
statements = parse_cmx3600_statements(f)
|
||||
return EditList(statements)
|
||||
|
||||
|
||||
@@ -2,166 +2,173 @@
|
||||
# (c) 2018 Jamie Hardt
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from itertools import count
|
||||
from typing import TextIO, List
|
||||
|
||||
|
||||
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","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"])
|
||||
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"])
|
||||
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", "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"])
|
||||
StmtSourceUMID = namedtuple("Source", ["name", "umid", "line_number"])
|
||||
StmtSplitEdit = namedtuple("SplitEdit", ["video", "magnitude", "line_number"])
|
||||
StmtMotionMemory = namedtuple(
|
||||
"MotionMemory", ["source", "fps"]) # FIXME needs more fields
|
||||
StmtUnrecognized = namedtuple("Unrecognized", ["content", "line_number"])
|
||||
|
||||
|
||||
def parse_cmx3600_statements(file):
|
||||
def parse_cmx3600_statements(file: TextIO) -> List[object]:
|
||||
"""
|
||||
Return a list of every statement in the file argument.
|
||||
"""
|
||||
lines = file.readlines()
|
||||
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,
|
||||
4,2, # chans
|
||||
4,1, # trans
|
||||
3,1, # trans op
|
||||
11,1,
|
||||
11,1,
|
||||
11,1,
|
||||
11]
|
||||
|
||||
def _edl_m2_column_widths():
|
||||
return [2, # "M2"
|
||||
3,3, #
|
||||
8,8,1,4,2,1,4,13,3,1,1]
|
||||
return [_parse_cmx3600_line(line.strip(), line_number)
|
||||
for (line_number, line) in enumerate(lines)]
|
||||
|
||||
|
||||
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} ")
|
||||
def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
|
||||
return [event_field_length, 2, source_field_length, 1,
|
||||
4, 2, # chans
|
||||
4, 1, # trans
|
||||
3, 1, # trans op
|
||||
11, 1,
|
||||
11, 1,
|
||||
11, 1,
|
||||
11]
|
||||
|
||||
if isinstance(line,str):
|
||||
if line.startswith("TITLE:"):
|
||||
return _parse_title(line,line_number)
|
||||
elif line.startswith("FCM:"):
|
||||
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, line_number)
|
||||
else:
|
||||
return _parse_long_standard_form(line, 128, line_number)
|
||||
elif short_event_num_p.match(line) != None:
|
||||
return _parse_standard_form(line, line_number)
|
||||
elif line.startswith("AUD"):
|
||||
return _parse_extended_audio_channels(line,line_number)
|
||||
elif line.startswith("*"):
|
||||
return _parse_remark( line[1:].strip(), 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:"):
|
||||
return _parse_split(line, line_number)
|
||||
elif line.startswith("M2"):
|
||||
return _parse_motion_memory(line, line_number)
|
||||
else:
|
||||
return _parse_unrecognized(line, line_number)
|
||||
# def _edl_m2_column_widths():
|
||||
# return [2, # "M2"
|
||||
# 3,3, #
|
||||
# 8,8,1,4,2,1,4,13,3,1,1]
|
||||
|
||||
|
||||
def _parse_title(line, line_num):
|
||||
def _parse_cmx3600_line(line: str, line_number: int) -> object:
|
||||
"""
|
||||
Parses a single CMX EDL line.
|
||||
|
||||
:param line: A single EDL line.
|
||||
:param line_number: The index of this line in the file.
|
||||
"""
|
||||
event_num_p = re.compile(r"^(\d+) ")
|
||||
line_matcher = event_num_p.match(line)
|
||||
|
||||
if line.startswith("TITLE:"):
|
||||
return _parse_title(line, line_number)
|
||||
elif line.startswith("FCM:"):
|
||||
return _parse_fcm(line, line_number)
|
||||
elif line_matcher is not None:
|
||||
event_field_len = len(line_matcher.group(1))
|
||||
source_field_len = len(line) - (event_field_len + 65)
|
||||
return _parse_columns_for_standard_form(line, event_field_len,
|
||||
source_field_len, line_number)
|
||||
elif line.startswith("AUD"):
|
||||
return _parse_extended_audio_channels(line, line_number)
|
||||
elif line.startswith("*"):
|
||||
return _parse_remark(line[1:].strip(), 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:"):
|
||||
return _parse_split(line, line_number)
|
||||
elif line.startswith("M2"):
|
||||
return _parse_motion_memory(line, line_number)
|
||||
else:
|
||||
return _parse_unrecognized(line, line_number)
|
||||
|
||||
|
||||
def _parse_title(line, line_num) -> StmtTitle:
|
||||
title = line[6:].strip()
|
||||
return StmtTitle(title=title,line_number=line_num)
|
||||
return StmtTitle(title=title, line_number=line_num)
|
||||
|
||||
def _parse_fcm(line, line_num):
|
||||
|
||||
def _parse_fcm(line, line_num) -> StmtFCM:
|
||||
val = line[4:].strip()
|
||||
if val == "DROP FRAME":
|
||||
return StmtFCM(drop= True, line_number=line_num)
|
||||
return StmtFCM(drop=True, line_number=line_num)
|
||||
else:
|
||||
return StmtFCM(drop= False, line_number=line_num)
|
||||
return StmtFCM(drop=False, line_number=line_num)
|
||||
|
||||
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, line_number):
|
||||
return _parse_columns_for_standard_form(line, 3, 8, line_number)
|
||||
|
||||
def _parse_extended_audio_channels(line, line_number):
|
||||
content = line.strip()
|
||||
if content == "AUD 3":
|
||||
return StmtAudioExt(audio3=True, audio4=False, line_number=line_number)
|
||||
elif content == "AUD 4":
|
||||
return StmtAudioExt(audio3=False, audio4=True, line_number=line_number)
|
||||
elif content == "AUD 3 4":
|
||||
return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
|
||||
else:
|
||||
return StmtUnrecognized(content=line, line_number=line_number)
|
||||
audio3 = True if "3" in content else False
|
||||
audio4 = True if "4" in content else False
|
||||
|
||||
def _parse_remark(line, line_number):
|
||||
if audio3 or audio4:
|
||||
return StmtAudioExt(audio3, audio4, line_number)
|
||||
else:
|
||||
return StmtUnrecognized(line, line_number)
|
||||
|
||||
|
||||
def _parse_remark(line, line_number) -> object:
|
||||
if line.startswith("FROM CLIP NAME:"):
|
||||
return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
|
||||
return StmtClipName(name=line[15:].strip(), affect="from",
|
||||
line_number=line_number)
|
||||
elif line.startswith("TO CLIP NAME:"):
|
||||
return StmtClipName(name=line[13:].strip(), affect="to", line_number=line_number)
|
||||
return StmtClipName(name=line[13:].strip(), affect="to",
|
||||
line_number=line_number)
|
||||
elif line.startswith("SOURCE FILE:"):
|
||||
return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
|
||||
return StmtSourceFile(filename=line[12:].strip(),
|
||||
line_number=line_number)
|
||||
else:
|
||||
return StmtRemark(text=line, line_number=line_number)
|
||||
|
||||
def _parse_effects_name(line, line_number):
|
||||
|
||||
def _parse_effects_name(line, line_number) -> StmtEffectsName:
|
||||
name = line[16:].strip()
|
||||
return StmtEffectsName(name=name, line_number=line_number)
|
||||
|
||||
|
||||
def _parse_split(line, line_number):
|
||||
split_type = line[10:21]
|
||||
is_video = False
|
||||
if split_type.startswith("VIDEO"):
|
||||
is_video = True
|
||||
|
||||
split_mag = line[24:35]
|
||||
return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number)
|
||||
split_mag = line[24:35]
|
||||
return StmtSplitEdit(video=is_video, magnitude=split_mag,
|
||||
line_number=line_number)
|
||||
|
||||
|
||||
def _parse_motion_memory(line, line_number):
|
||||
return StmtMotionMemory(source = "", fps="")
|
||||
return StmtMotionMemory(source="", fps="")
|
||||
|
||||
|
||||
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, line_number):
|
||||
|
||||
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, line_number=line_number)
|
||||
|
||||
column_strings = collimate(line,col_widths)
|
||||
column_strings = collimate(line, col_widths)
|
||||
|
||||
return StmtEvent(event=column_strings[0],
|
||||
source=column_strings[2].strip(),
|
||||
channels=column_strings[4].strip(),
|
||||
trans=column_strings[6].strip(),
|
||||
source=column_strings[2].strip(),
|
||||
channels=column_strings[4].strip(),
|
||||
trans=column_strings[6].strip(),
|
||||
trans_op=column_strings[8].strip(),
|
||||
source_in=column_strings[10].strip(),
|
||||
source_out=column_strings[12].strip(),
|
||||
record_in=column_strings[14].strip(),
|
||||
record_out=column_strings[16].strip(),
|
||||
line_number=line_number,
|
||||
format=source_field_length)
|
||||
line_number=line_number, format=source_field_length)
|
||||
|
||||
|
||||
def _parse_source_umid_statement(line, line_number):
|
||||
trimmed = line[3:].strip()
|
||||
# trimmed = line[3:].strip()
|
||||
return StmtSourceUMID(name=None, umid=None, line_number=line_number)
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# pycmx
|
||||
# (c) 2018 Jamie Hardt
|
||||
# (c) 2023 Jamie Hardt
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Transition:
|
||||
"""
|
||||
@@ -19,7 +22,7 @@ class Transition:
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
def kind(self) -> Optional[str]:
|
||||
"""
|
||||
Return the kind of transition: Cut, Wipe, etc
|
||||
"""
|
||||
@@ -37,22 +40,22 @@ class Transition:
|
||||
return Transition.KeyOut
|
||||
|
||||
@property
|
||||
def cut(self):
|
||||
def cut(self) -> bool:
|
||||
"`True` if this transition is a cut."
|
||||
return self.transition == 'C'
|
||||
|
||||
@property
|
||||
def dissolve(self):
|
||||
def dissolve(self) -> bool:
|
||||
"`True` if this traansition is a dissolve."
|
||||
return self.transition == 'D'
|
||||
|
||||
@property
|
||||
def wipe(self):
|
||||
def wipe(self) -> bool:
|
||||
"`True` if this transition is a wipe."
|
||||
return self.transition.startswith('W')
|
||||
|
||||
@property
|
||||
def effect_duration(self):
|
||||
def effect_duration(self) -> int:
|
||||
"""The duration of this transition, in frames of the record target.
|
||||
|
||||
In the event of a key event, this is the duration of the fade in.
|
||||
@@ -60,7 +63,7 @@ class Transition:
|
||||
return int(self.operand)
|
||||
|
||||
@property
|
||||
def wipe_number(self):
|
||||
def wipe_number(self) -> Optional[int]:
|
||||
"Wipes are identified by a particular number."
|
||||
if self.wipe:
|
||||
return int(self.transition[1:])
|
||||
@@ -68,17 +71,17 @@ class Transition:
|
||||
return None
|
||||
|
||||
@property
|
||||
def key_background(self):
|
||||
def key_background(self) -> bool:
|
||||
"`True` if this edit is a key background."
|
||||
return self.transition == Transition.KeyBackground
|
||||
|
||||
@property
|
||||
def key_foreground(self):
|
||||
def key_foreground(self) -> bool:
|
||||
"`True` if this edit is a key foreground."
|
||||
return self.transition == Transition.Key
|
||||
|
||||
@property
|
||||
def key_out(self):
|
||||
def key_out(self) -> bool:
|
||||
"""
|
||||
`True` if this edit is a key out. This material will removed from
|
||||
the key foreground and replaced with the key background.
|
||||
|
||||
@@ -28,4 +28,3 @@ def collimate(a_string, column_widths):
|
||||
element = a_string[:width]
|
||||
rest = a_string[width:]
|
||||
return [element] + collimate(rest, column_widths[1:])
|
||||
|
||||
|
||||
56
pyproject.toml
Normal file
56
pyproject.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[tool.poetry]
|
||||
name = "pycmx"
|
||||
version = "1.4.0"
|
||||
description = "Python CMX 3600 Edit Decision List Parser"
|
||||
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
'parser',
|
||||
'film',
|
||||
'broadcast'
|
||||
]
|
||||
classifiers = [
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'Topic :: Multimedia :: Video',
|
||||
'Topic :: Text Processing',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
'Programming Language :: Python :: 3.13'
|
||||
]
|
||||
homepage = "https://github.com/iluvcapra/pycmx"
|
||||
documentation = "https://pycmx.readthedocs.io/"
|
||||
repository = "https://github.com/iluvcapra/pycmx.git"
|
||||
urls.Tracker = "https://github.com/iluvcapra/pycmx/issues"
|
||||
|
||||
[tool.poetry.extras]
|
||||
doc = ['sphinx', 'sphinx_rtd_theme']
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
sphinx = { version='>= 5.3.0', optional=true}
|
||||
sphinx_rtd_theme = {version ='>= 1.1.1', optional=true}
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
[tool.pylint]
|
||||
max-line-length = 88
|
||||
disable = [
|
||||
"C0103", # (invalid-name)
|
||||
"C0114", # (missing-module-docstring)
|
||||
"C0115", # (missing-class-docstring)
|
||||
"C0116", # (missing-function-docstring)
|
||||
"R0903", # (too-few-public-methods)
|
||||
"R0913", # (too-many-arguments)
|
||||
"W0105", # (pointless-string-statement)
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -1,9 +0,0 @@
|
||||
attrs==22.1.0
|
||||
coverage==6.5.0
|
||||
exceptiongroup==1.0.4
|
||||
iniconfig==1.1.1
|
||||
packaging==21.3
|
||||
pluggy==1.0.0
|
||||
pyparsing==3.0.9
|
||||
pytest==7.2.0
|
||||
tomli==2.0.1
|
||||
33
setup.py
33
setup.py
@@ -1,33 +0,0 @@
|
||||
from setuptools import setup
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(name='pycmx',
|
||||
version='1.1.5',
|
||||
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,
|
||||
project_urls={
|
||||
'Source':
|
||||
'https://github.com/iluvcapra/pycmx',
|
||||
'Documentation':
|
||||
'https://pycmx.readthedocs.io/',
|
||||
'Issues':
|
||||
'https://github.com/iluvcapra/pycmx/issues',
|
||||
},
|
||||
url='https://github.com/iluvcapra/pycmx',
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'Topic :: Multimedia :: Video',
|
||||
'Topic :: Text Processing',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10'
|
||||
],
|
||||
packages=['pycmx'])
|
||||
13
tests/edls/ISSUE_14_conform_edl_issue_01.edl
Normal file
13
tests/edls/ISSUE_14_conform_edl_issue_01.edl
Normal file
@@ -0,0 +1,13 @@
|
||||
TITLE: conform_edl_issue_01
|
||||
FCM: NON-DROP FRAME
|
||||
|
||||
001 C_0022C003_241016_092821_h1F4X V C 09:33:31:12 09:33:33:14 01:00:06:20 01:00:08:22
|
||||
* FROM CLIP NAME: 13A-1-C
|
||||
|
||||
002 B_0020C009_241003_214837_h1C2T V C 21:48:54:22 21:48:55:15 01:01:34:06 01:01:34:23
|
||||
* FROM CLIP NAME: 111B-1-B
|
||||
|
||||
003 B_0088C002_241125_144410_h1C2T V C 13:48:57:10 13:48:58:11 01:01:41:13 01:01:42:14
|
||||
M2 B_0088C002_241125_144410_h1C2T 031.7 13:48:57:10
|
||||
* FROM CLIP NAME: 102C-2-B
|
||||
|
||||
33
tests/edls/ISSUE_14_conform_edl_issue_02.edl
Normal file
33
tests/edls/ISSUE_14_conform_edl_issue_02.edl
Normal file
@@ -0,0 +1,33 @@
|
||||
TITLE: conform_edl_issue_02
|
||||
FCM: NON-DROP FRAME
|
||||
|
||||
001 C_0019C005_241014_204338_h1F4X V C 11:19:41:07 11:19:49:03 02:04:48:10 02:04:56:06
|
||||
* FROM CLIP NAME: 40E-4 MOS*
|
||||
|
||||
002 B_0075C004_241114_164247_h1C2T V C 15:43:03:12 15:43:05:17 02:06:03:03 02:06:05:08
|
||||
* FROM CLIP NAME: 39B-4-B
|
||||
|
||||
003 A_0079C015_241112_160227_h1EHP V C 16:06:21:10 16:06:23:03 02:08:14:07 02:08:16:00
|
||||
* FROM CLIP NAME: 46L-2
|
||||
|
||||
004 C_0047C005_241121_123629_h1F4X V C 11:41:15:17 11:41:21:01 02:09:08:01 02:09:13:09
|
||||
* FROM CLIP NAME: 49-5-C
|
||||
|
||||
005 A_0003C002_240923_130856_h1EHP V C 14:11:06:01 14:11:12:03 02:11:26:11 02:11:32:13
|
||||
* FROM CLIP NAME: 54-2-A
|
||||
|
||||
006 A_0090C003_241119_095341_h1EHP V C 09:57:25:14 09:57:32:20 02:13:40:13 02:13:47:19
|
||||
* FROM CLIP NAME: 57B-3-A*
|
||||
|
||||
007 A_0090C008_241119_102624_h1EHP V C 10:30:43:07 10:30:46:23 02:14:16:01 02:14:19:17
|
||||
* FROM CLIP NAME: 57D-2*
|
||||
|
||||
008 B_0079C003_241119_105658_h1C2T V C 09:58:57:15 09:59:00:14 02:15:01:08 02:15:04:07
|
||||
* FROM CLIP NAME: 57B-3-B*
|
||||
|
||||
009 A_0005C011_240924_113730_h1EHP V C 12:40:01:07 12:40:04:06 02:16:13:11 02:16:16:10
|
||||
* FROM CLIP NAME: 58C-3-A
|
||||
|
||||
010 A_0060C001_241030_133415_h1EHP V C 13:35:38:11 13:35:41:17 02:18:29:17 02:18:32:23
|
||||
* FROM CLIP NAME: 61A-1
|
||||
|
||||
129
tests/edls/ISSUE_14_conform_edl_issue_03.edl
Normal file
129
tests/edls/ISSUE_14_conform_edl_issue_03.edl
Normal file
@@ -0,0 +1,129 @@
|
||||
TITLE: conform_edl_issue_03
|
||||
FCM: NON-DROP FRAME
|
||||
|
||||
002 A_0113C007_250602_103141_h1D4P V C 10:32:32:22 10:32:48:16 01:00:20:23 01:00:36:17
|
||||
* FROM CLIP NAME: AP002A-3*
|
||||
|
||||
003 A_0113C004_250602_101043_h1D4P V C 10:11:34:01 10:11:48:01 01:00:36:17 01:00:50:17
|
||||
* FROM CLIP NAME: AP002-4*
|
||||
|
||||
004 A_0113C018_250602_122238_h1D4P V C 12:24:03:12 12:24:04:17 01:01:15:17 01:01:16:22
|
||||
* FROM CLIP NAME: AP002E-3-A*
|
||||
|
||||
005 A_0113C018_250602_122238_h1D4P V C 12:24:05:22 12:24:18:02 01:01:20:21 01:01:33:01
|
||||
* FROM CLIP NAME: AP002E-3-A*
|
||||
|
||||
006 A_0113C022_250602_125451_h1D4P V C 12:55:43:09 12:55:55:03 01:01:33:01 01:01:44:19
|
||||
* FROM CLIP NAME: AP002F-4-A*
|
||||
|
||||
007 A_0113C024_250602_132048_h1D4P V C 13:21:54:17 13:22:00:07 01:01:44:19 01:01:50:09
|
||||
* FROM CLIP NAME: AP002G-2-A
|
||||
|
||||
008 B_0098C009_250602_115121_h1EZ3 V C 12:56:02:03 12:56:05:11 01:01:50:09 01:01:53:17
|
||||
* FROM CLIP NAME: AP002F-4-B*
|
||||
|
||||
009 A_0113C024_250602_132048_h1D4P V C 13:21:41:20 13:21:46:01 01:01:53:17 01:01:57:22
|
||||
* FROM CLIP NAME: AP002G-2-A
|
||||
|
||||
010 A_0113C022_250602_125451_h1D4P V C 12:56:08:09 12:56:12:23 01:01:57:22 01:02:02:12
|
||||
* FROM CLIP NAME: AP002F-4-A*
|
||||
|
||||
011 B_0099C004_250602_133201_h1EZ3 V C 14:36:34:09 14:36:38:03 01:02:06:07 01:02:10:01
|
||||
* FROM CLIP NAME: AP002H-4-B*
|
||||
|
||||
012 A_0114C009_250602_161406_h1D4P V C 16:15:15:09 16:15:17:16 01:02:10:01 01:02:12:08
|
||||
* FROM CLIP NAME: AP002M-1-A
|
||||
|
||||
013 A_0115C001_250602_172408_h1D4P V C 17:25:55:01 17:25:59:17 01:02:20:15 01:02:25:07
|
||||
* FROM CLIP NAME: AP002Q-1-A
|
||||
|
||||
014 B_0099C019_250602_153856_h1EZ3 V C 16:43:35:06 16:43:38:13 01:02:25:07 01:02:28:14
|
||||
* FROM CLIP NAME: AP002N-2
|
||||
|
||||
015 A_0115C002_250602_172803_h1D4P V C 17:29:28:01 17:29:32:02 01:02:28:14 01:02:32:15
|
||||
* FROM CLIP NAME: AP002Q-2-A*
|
||||
|
||||
016 B_0099C019_250602_153856_h1EZ3 V C 16:43:41:10 16:43:45:12 01:02:32:15 01:02:36:17
|
||||
* FROM CLIP NAME: AP002N-2
|
||||
|
||||
017 A_0115C001_250602_172408_h1D4P V C 17:26:08:05 17:26:10:20 01:02:36:17 01:02:39:08
|
||||
* FROM CLIP NAME: AP002Q-1-A
|
||||
|
||||
018 B_0099C018_250602_153023_h1EZ3 V C 16:35:28:00 16:35:30:10 01:02:39:08 01:02:41:18
|
||||
* FROM CLIP NAME: AP002N-1*
|
||||
|
||||
019 B_0100C001_250602_162041_h1EZ3 V C 17:26:17:05 17:26:24:23 01:02:41:18 01:02:49:12
|
||||
* FROM CLIP NAME: AP002Q-1-B
|
||||
|
||||
020 A_0114C010_250602_162156_h1D4P V C 16:24:28:03 16:24:33:20 01:02:49:12 01:02:55:05
|
||||
* FROM CLIP NAME: AP002M-2-A*
|
||||
|
||||
021 B_0099C007_250602_140229_h1EZ3 V C 15:07:55:18 15:08:00:01 01:02:55:05 01:02:59:12
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
022 B_0099C018_250602_153023_h1EZ3 V C 16:35:47:23 16:35:49:05 01:02:59:12 01:03:00:18
|
||||
* FROM CLIP NAME: AP002N-1*
|
||||
|
||||
023 B_0099C007_250602_140229_h1EZ3 V C 15:08:01:17 15:08:12:19 01:03:00:18 01:03:11:20
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
024 B_0099C018_250602_153023_h1EZ3 V C 16:36:10:00 16:36:11:10 01:03:11:20 01:03:13:06
|
||||
* FROM CLIP NAME: AP002N-1*
|
||||
|
||||
025 B_0099C007_250602_140229_h1EZ3 V C 15:08:15:08 15:08:22:13 01:03:13:06 01:03:20:11
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
026 B_0099C018_250602_153023_h1EZ3 V C 16:36:18:21 16:36:23:01 01:03:20:11 01:03:24:15
|
||||
* FROM CLIP NAME: AP002N-1*
|
||||
|
||||
027 B_0099C007_250602_140229_h1EZ3 V C 15:08:33:21 15:08:37:23 01:03:30:14 01:03:34:16
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
028 B_0099C017_250602_151824_h1EZ3 V C 16:25:21:01 16:25:29:04 01:03:34:16 01:03:42:19
|
||||
* FROM CLIP NAME: AP002M-2-B*
|
||||
|
||||
029 B_0099C007_250602_140229_h1EZ3 V C 15:08:47:10 15:08:55:02 01:03:42:19 01:03:50:11
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
030 B_0099C017_250602_151824_h1EZ3 V C 16:25:39:00 16:25:42:03 01:03:50:11 01:03:53:14
|
||||
* FROM CLIP NAME: AP002M-2-B*
|
||||
|
||||
031 B_0099C007_250602_140229_h1EZ3 V C 15:08:58:10 15:09:03:04 01:03:53:14 01:03:58:08
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
032 B_0099C016_250602_151038_h1EZ3 V C 16:17:20:21 16:17:23:10 01:03:58:08 01:04:00:21
|
||||
* FROM CLIP NAME: AP002M-1-B
|
||||
|
||||
033 B_0099C007_250602_140229_h1EZ3 V C 15:09:05:09 15:09:16:04 01:04:00:21 01:04:11:16
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
034 B_0099C019_250602_153856_h1EZ3 V C 16:45:46:08 16:45:52:18 01:04:11:16 01:04:18:02
|
||||
* FROM CLIP NAME: AP002N-2
|
||||
|
||||
035 B_0099C007_250602_140229_h1EZ3 V C 15:09:24:21 15:09:27:22 01:04:18:02 01:04:21:03
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
036 B_0099C019_250602_153856_h1EZ3 V C 16:45:55:19 16:45:58:02 01:04:21:03 01:04:23:10
|
||||
* FROM CLIP NAME: AP002N-2
|
||||
|
||||
037 B_0099C004_250602_133201_h1EZ3 V C 14:38:53:22 14:38:58:19 01:04:23:10 01:04:28:07
|
||||
* FROM CLIP NAME: AP002H-4-B*
|
||||
|
||||
038 B_0099C015_250602_150046_h1EZ3 V C 16:07:52:23 16:07:55:04 01:04:28:07 01:04:30:12
|
||||
* FROM CLIP NAME: AP002L-1-B
|
||||
|
||||
039 B_0099C007_250602_140229_h1EZ3 V C 15:09:35:22 15:09:40:19 01:04:30:12 01:04:35:09
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
040 B_0099C015_250602_150046_h1EZ3 V C 16:08:01:21 16:08:05:01 01:04:35:09 01:04:38:13
|
||||
* FROM CLIP NAME: AP002L-1-B
|
||||
|
||||
041 B_0099C007_250602_140229_h1EZ3 V C 15:09:45:16 15:09:55:00 01:04:38:13 01:04:47:21
|
||||
* FROM CLIP NAME: AP002K-2-B
|
||||
|
||||
042 M018C0005_240925_1F4L13 V C 18:44:20:12 18:44:23:18 01:04:50:16 01:04:53:22
|
||||
* FROM CLIP NAME: 13-1-SER-1-M MOS
|
||||
|
||||
043 A_0022C009_241003_141208_h1EHP V C 15:13:54:11 15:13:56:04 01:12:22:02 01:12:23:19
|
||||
* FROM CLIP NAME: 26H-3-A*
|
||||
|
||||
13945
tests/edls/adobe_dai109_test.txt
Normal file
13945
tests/edls/adobe_dai109_test.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,112 +2,138 @@ from unittest import TestCase
|
||||
|
||||
import pycmx
|
||||
|
||||
|
||||
class TestParse(TestCase):
|
||||
|
||||
files = ["INS4_R1_010417.edl" ,
|
||||
"INS4_R1_DX_092117.edl",
|
||||
"STP R1 v082517.edl",
|
||||
"ToD_R4_LOCK3.1_030618_Video.edl",
|
||||
"TEST.edl",
|
||||
"test_edl_cdl.edl",
|
||||
"INS4_R1_DX_092117.edl"
|
||||
]
|
||||
files = ["INS4_R1_010417.edl",
|
||||
"INS4_R1_DX_092117.edl",
|
||||
"STP R1 v082517.edl",
|
||||
"ToD_R4_LOCK3.1_030618_Video.edl",
|
||||
"TEST.edl",
|
||||
"test_edl_cdl.edl",
|
||||
"INS4_R1_DX_092117.edl"
|
||||
]
|
||||
|
||||
def test_event_counts(self):
|
||||
|
||||
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):
|
||||
with open("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 ,
|
||||
"expected %i in file %s but found %i" % (count, fn, actual))
|
||||
actual = len(list(edl.events))
|
||||
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("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 )
|
||||
|
||||
self.assertTrue(type(edl.title) is str)
|
||||
self.assertTrue(len(edl.title) > 0)
|
||||
|
||||
def test_event_sanity(self):
|
||||
for fn in type(self).files:
|
||||
path = "tests/edls/" + fn
|
||||
with open(path ,'r') as f:
|
||||
with open(path, 'r') as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
for index, event in enumerate(edl.events):
|
||||
self.assertTrue( len(event.edits) > 0 )
|
||||
self.assertTrue( event.number == index + 1 )
|
||||
|
||||
|
||||
self.assertTrue(len(event.edits) > 0,
|
||||
f"Failed for {path}")
|
||||
self.assertEqual(event.number, index + 1,
|
||||
f"Failed for {path}")
|
||||
|
||||
def test_events(self):
|
||||
with open("tests/edls/TEST.edl",'r') as f:
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list( edl.events )
|
||||
events = list(edl.events)
|
||||
|
||||
self.assertEqual( events[0].number , 1)
|
||||
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].source_file , "OY_HEAD_LEADER.MOV")
|
||||
self.assertEqual( events[0].edits[0].source_in , "00:00:00:00")
|
||||
self.assertEqual( events[0].edits[0].source_out , "00:00:00:00")
|
||||
self.assertEqual( events[0].edits[0].record_in , "01:00:00:00")
|
||||
self.assertEqual( events[0].edits[0].record_out , "01:00:08:00")
|
||||
self.assertTrue( events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
self.assertEqual(events[0].number, 1)
|
||||
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].source_file, "OY_HEAD_LEADER.MOV")
|
||||
self.assertEqual(events[0].edits[0].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].record_in, "01:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].record_out, "01:00:08:00")
|
||||
self.assertTrue(
|
||||
events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
|
||||
def test_channel_map(self):
|
||||
with open("tests/edls/TEST.edl",'r') as f:
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list( edl.events )
|
||||
self.assertFalse( events[0].edits[0].channels.video)
|
||||
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)
|
||||
|
||||
events = list(edl.events)
|
||||
self.assertFalse(events[0].edits[0].channels.video)
|
||||
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):
|
||||
with open("tests/edls/TEST.edl",'r') as f:
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list( edl.events )
|
||||
events = list(edl.events)
|
||||
|
||||
self.assertEqual( events[42].number , 43)
|
||||
self.assertEqual( len(events[42].edits), 2)
|
||||
self.assertEqual(events[42].number, 43)
|
||||
self.assertEqual(len(events[42].edits), 2)
|
||||
|
||||
self.assertEqual( events[42].edits[0].source , "TC_R1_V1")
|
||||
self.assertEqual( events[42].edits[0].clip_name , "TC R1 V1.2 TEMP1 FX ST.WAV")
|
||||
self.assertEqual( events[42].edits[0].source_in , "00:00:00:00")
|
||||
self.assertEqual( events[42].edits[0].source_out , "00:00:00:00")
|
||||
self.assertEqual( events[42].edits[0].record_in , "01:08:56:09")
|
||||
self.assertEqual( events[42].edits[0].record_out , "01:08:56:09")
|
||||
self.assertTrue( events[42].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
self.assertEqual(events[42].edits[0].source, "TC_R1_V1")
|
||||
self.assertEqual(events[42].edits[0].clip_name,
|
||||
"TC R1 V1.2 TEMP1 FX ST.WAV")
|
||||
self.assertEqual(events[42].edits[0].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[0].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[0].record_in, "01:08:56:09")
|
||||
self.assertEqual(events[42].edits[0].record_out, "01:08:56:09")
|
||||
self.assertTrue(
|
||||
events[42].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
|
||||
self.assertEqual( events[42].edits[1].source , "TC_R1_V6")
|
||||
self.assertEqual( events[42].edits[1].clip_name , "TC R1 V6 TEMP2 ST FX.WAV")
|
||||
self.assertEqual( events[42].edits[1].source_in , "00:00:00:00")
|
||||
self.assertEqual( events[42].edits[1].source_out , "00:00:00:00")
|
||||
self.assertEqual( events[42].edits[1].record_in , "01:08:56:09")
|
||||
self.assertEqual( events[42].edits[1].record_out , "01:08:56:11")
|
||||
self.assertTrue( events[42].edits[1].transition.kind == pycmx.Transition.Dissolve)
|
||||
self.assertEqual(events[42].edits[1].source, "TC_R1_V6")
|
||||
self.assertEqual(events[42].edits[1].clip_name,
|
||||
"TC R1 V6 TEMP2 ST FX.WAV")
|
||||
self.assertEqual(events[42].edits[1].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[1].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[1].record_in, "01:08:56:09")
|
||||
self.assertEqual(events[42].edits[1].record_out, "01:08:56:11")
|
||||
self.assertTrue(
|
||||
events[42].edits[1].transition.kind ==
|
||||
pycmx.Transition.Dissolve)
|
||||
|
||||
def test_line_numbers(self):
|
||||
with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
|
||||
events = list( edl.events )
|
||||
self.assertEqual( events[0].edits[0].line_number, 2)
|
||||
self.assertEqual( events[14].edits[0].line_number, 45)
|
||||
self.assertEqual( events[180].edits[0].line_number, 544)
|
||||
events = list(edl.events)
|
||||
self.assertEqual(events[0].edits[0].line_number, 2)
|
||||
self.assertEqual(events[14].edits[0].line_number, 45)
|
||||
self.assertEqual(events[180].edits[0].line_number, 544)
|
||||
|
||||
def test_transition_name(self):
|
||||
with open("tests/edls/test_25.edl","r") as f:
|
||||
with open("tests/edls/test_25.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" )
|
||||
self.assertEqual(
|
||||
events[4].edits[1].transition.name, "CROSS DISSOLVE")
|
||||
|
||||
def test_adobe_wide(self):
|
||||
with open("tests/edls/adobe_dai109_test.txt", 'r',
|
||||
encoding='ISO-8859-1') as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
|
||||
# add test for edit_list.channels
|
||||
self.assertEqual(len(events), 2839)
|
||||
|
||||
def test_issue14(self):
|
||||
with open("tests/edls/ISSUE_14_conform_edl_issue_03.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
|
||||
for event in edl.events:
|
||||
if event.number == 42:
|
||||
self.assertEqual(len(event.edits), 1)
|
||||
self.assertEqual(event.edits[0].source,
|
||||
"M018C0005_240925_1F4L13")
|
||||
self.assertEqual(event.edits[0].transition.kind,
|
||||
pycmx.Transition.Cut)
|
||||
self.assertEqual(event.edits[0].source_in,
|
||||
"18:44:20:12")
|
||||
|
||||
Reference in New Issue
Block a user