122 Commits

Author SHA1 Message Date
b85e02585c Nudged version 2025-12-16 09:36:25 -08:00
88f3a7a659 Added unit test for new parsing functionality 2025-12-16 09:23:46 -08:00
959b824dcd flake8 2025-12-15 15:39:25 -08:00
0c6a3896fb Autopep 2025-12-15 15:36:25 -08:00
a128b6ca7d Tests now work for these new files 2025-12-15 15:33:16 -08:00
79778bb847 Adding EDL test cases from issue #14 2025-12-15 14:10:11 -08:00
Jamie Hardt
81997c763e Update pythonpublish.yml
Updated python-publish to latest version
2025-09-08 12:49:46 -07:00
Jamie Hardt
6da04b2c07 Nudged version 2025-01-07 11:52:26 -08:00
Jamie Hardt
89b6cde808 Merge pull request #12 from iluvcapra/feat-adobe
Added support for Adobe Premiere EDLs > 999 events
2025-01-07 11:49:54 -08:00
Jamie Hardt
5e4dde5aa2 Merge branch 'master' of https://github.com/iluvcapra/pycmx into feat-adobe 2025-01-05 22:34:30 -08:00
Jamie Hardt
b6379ec1fe tweaking doc version method 2025-01-05 12:44:03 -08:00
Jamie Hardt
04731d634f Updated version read logic 2025-01-05 12:37:56 -08:00
Jamie Hardt
2dcfdabf01 Merge branch 'master' of https://github.com/iluvcapra/pycmx into feat-adobe 2025-01-05 12:26:03 -08:00
Jamie Hardt
7ed423deaf Dropping max-complexity argument 2025-01-05 12:19:22 -08:00
Jamie Hardt
2be779fe53 Flake8 for docs, test, bin 2025-01-05 12:16:16 -08:00
Jamie Hardt
2adff6dd01 Flake8 lints, flake8 linting fully activated. 2025-01-05 12:03:38 -08:00
Jamie Hardt
7871acdfd0 Merge branch 'master' of https://github.com/iluvcapra/pycmx into feat-adobe 2025-01-05 11:33:57 -08:00
Jamie Hardt
8bb6dad1da Some documentation and autopep 2025-01-05 11:32:23 -08:00
Jamie Hardt
f88b82e8fb README updated 2025-01-05 10:47:57 -08:00
Jamie Hardt
a86ef7ed2e Added support for Adobe Premiere EDLs > 999 events 2025-01-04 13:20:21 -08:00
Jamie Hardt
1e1331eadb Adding a test case for #11 2025-01-02 22:07:20 -08:00
Jamie Hardt
7a0481dbf9 Update bsky_test.yml 2024-11-26 11:50:10 -08:00
Jamie Hardt
20ad04e862 Update bsky_test.yml 2024-11-26 11:43:39 -08:00
Jamie Hardt
9e54aa5fcf Update bsky_test.yml 2024-11-26 11:34:16 -08:00
Jamie Hardt
71aff9baf7 Update bsky_test.yml 2024-11-26 11:33:37 -08:00
Jamie Hardt
ab55cab160 Create bsky_test.yml 2024-11-26 11:29:43 -08:00
Jamie Hardt
a717589683 Bumped version and tweaked Bluesky message 2024-11-26 11:19:01 -08:00
Jamie Hardt
f48c164e1b Merge pull request #10 from iluvcapra/maint-poetry
Sprucing-up Build System and Workflows
2024-11-26 11:17:10 -08:00
Jamie Hardt
be1dc99e94 Update pyproject.toml
Fixed typo
2024-11-26 11:16:33 -08:00
Jamie Hardt
fbdfcddfff Re-activated Bluesky posting
Updated classifiers in pyproject.toml
2024-11-26 11:15:08 -08:00
Jamie Hardt
156828b648 Update pythonpublish.yml 2024-11-26 11:11:25 -08:00
Jamie Hardt
1894a143b1 Removed second dependencies block 2024-11-26 11:02:53 -08:00
Jamie Hardt
c0d278e079 Removing 3.7 support 2024-11-26 11:01:49 -08:00
Jamie Hardt
7d3a58bff8 Small change 2024-11-26 10:57:01 -08:00
Jamie Hardt
f1381f5f46 Merge branch 'maint-poetry' of https://github.com/iluvcapra/pycmx into maint-poetry 2024-11-26 10:54:37 -08:00
Jamie Hardt
dc00b52b61 Merge branch 'master' of https://github.com/iluvcapra/pycmx into maint-poetry 2024-11-26 10:53:39 -08:00
Jamie Hardt
571ffdefd7 Merge branch 'master' into maint-poetry 2024-11-26 10:52:55 -08:00
Jamie Hardt
a79cb02139 Updating workflows
Added 3.13 support, publish to Bluesky, Poetry build system
2024-11-26 10:50:38 -08:00
Jamie Hardt
ce1a0d32bb Update __init__.py
Nudged version to 1.2.2
2023-11-08 10:10:08 -08:00
Jamie Hardt
daa5d58a66 Create .readthedocs.yaml 2023-11-08 10:02:24 -08:00
Jamie Hardt
38ce1445a1 Merge pull request #9 from iluvcapra/maint-py3.12
Added Python 3.12 support
2023-11-08 08:17:04 -08:00
Jamie Hardt
552d007360 Merge branch 'master' into maint-py3.12 2023-11-08 08:16:42 -08:00
Jamie Hardt
d79fdcc6a8 Update pyproject.toml
Added Python 3.12 classifier
2023-11-08 08:15:47 -08:00
Jamie Hardt
3187a50a6b Update pyproject.toml 2023-11-08 08:15:26 -08:00
Jamie Hardt
8e8d4f5753 Update pyproject.toml
Added 3.12 classifier
2023-11-08 08:14:57 -08:00
Jamie Hardt
cc371cd486 Update python-package.yml
Added 3.12 to test matrix
2023-11-08 08:13:42 -08:00
Jamie Hardt
795a666a74 Update conf.py 2023-05-31 19:21:27 -07:00
Jamie Hardt
cd35f4f80d Merge pull request #8 from iluvcapra/master
Merging changes for v1.2.1
2023-05-31 18:57:52 -07:00
Jamie Hardt
6525840151 Merge branch 'release' into master 2023-05-31 18:57:05 -07:00
Jamie Hardt
b78ae05d8c Update pythonpublish.yml
Updated to use pypi Trusted Publisher.
2023-05-31 18:53:46 -07:00
Jamie Hardt
51ed92f5df More typing 2023-05-31 18:10:44 -07:00
Jamie Hardt
a6f042c76f More typing cleanups 2023-05-31 17:50:03 -07:00
Jamie Hardt
179808fbf2 Added type annotations and doc fixes 2023-05-31 11:59:26 -07:00
Jamie Hardt
2b38d8aaf9 Update __init__.py
Bumped copyright year and version number.
2023-05-31 11:34:40 -07:00
Jamie Hardt
e19bb3c5c5 Update __init__.py
Made first line of module documentation shorter, it gets cut off on pypi.
2023-05-31 11:33:15 -07:00
Jamie Hardt
d0e2e5bbe5 Update pythonpublish.yml
Changed action to release-published
2023-05-17 17:18:36 -07:00
Jamie Hardt
e90897e503 Update pythonpublish.yml 2023-05-17 17:06:17 -07:00
Jamie Hardt
4daa008e91 Merge branch 'master' into release 2023-05-17 17:03:47 -07:00
Jamie Hardt
cf1d71800e Update pythonpublish.yml 2023-05-17 17:01:25 -07:00
Jamie Hardt
fcb25ae183 Update pythonpublish.yml
Updated action versions
2023-05-17 16:59:17 -07:00
Jamie Hardt
97ada84bfe Merge pull request #7 from iluvcapra/release
Release
2023-05-17 16:55:00 -07:00
Jamie Hardt
d157512c32 Update pythonpublish.yml
Updating checkoutv2
2023-05-17 16:54:00 -07:00
Jamie Hardt
1e2d31716e Merge remote-tracking branch 'refs/remotes/origin/master' 2023-05-17 16:50:41 -07:00
Jamie Hardt
6be7c54de5 Update pythonpublish.yml 2023-05-17 16:48:25 -07:00
Jamie Hardt
a3109cdb7a Merge branch 'master' into release 2023-05-17 16:30:36 -07:00
Jamie Hardt
3a9d81417e Removing 3.6 from test grid and project classifiers
3.6 isn't supported by the github actions anymore.
2023-05-17 16:24:36 -07:00
Jamie Hardt
9838e6b357 Update python-package.yml
Adding py3.6 to test grid
2023-05-17 16:22:50 -07:00
Jamie Hardt
41fdeeaf56 Update python-package.yml
Adding 3.11 to test grid
2023-05-17 16:21:06 -07:00
Jamie Hardt
b3b6e57f6c Update pyproject.toml
Adding 3.11 to versions
2023-05-17 16:20:32 -07:00
Jamie Hardt
62e7f10cf4 Update pyproject.toml
Downgraded minimum python version
2023-05-17 16:19:02 -07:00
Jamie Hardt
f855d3581d Update python-package.yml
Fixed typo
2023-05-17 16:16:35 -07:00
Jamie Hardt
0b5555d333 Updated workflow for pyproject installation 2023-05-17 16:15:02 -07:00
Jamie Hardt
7123a6f1bb Deleted requirements.txt 2023-05-17 16:13:29 -07:00
Jamie Hardt
173493a610 Updated project to pyproject-style package 2023-05-17 16:12:42 -07:00
Jamie Hardt
d20d9d1bdd Update pythonpublish.yml 2022-11-21 20:29:33 -08:00
Jamie Hardt
d328e12283 Merge pull request #6 from iluvcapra/master
Update release branch
2022-11-18 23:06:52 -08:00
Jamie Hardt
4d83f81fc8 Merge pull request #5 from iluvcapra/release
Release
2022-11-18 23:03:21 -08:00
Jamie Hardt
69b6c7236d Update setup.py
Bump version number
2022-11-18 23:01:57 -08:00
Jamie Hardt
71ffe8cd0d Merge branch 'master' into release 2022-11-18 22:49:45 -08:00
Jamie Hardt
3fff6c8d2a Update gitignore 2022-11-18 22:47:44 -08:00
Jamie Hardt
e350565430 Update channel_map.py 2022-11-18 22:37:29 -08:00
Jamie Hardt
9ec30ede02 Update channel_map.py 2022-11-18 22:34:37 -08:00
Jamie Hardt
e5130b8011 Update edit_list.py 2022-11-18 22:33:33 -08:00
Jamie Hardt
00eaccabac Update index.rst
Twiddle
2022-11-18 22:27:20 -08:00
Jamie Hardt
521d86e444 Update conf.py 2022-11-16 23:24:52 -08:00
Jamie Hardt
0fdba2408b Create CONTRIBUTING.md 2022-11-16 21:26:38 -08:00
Jamie Hardt
199bba2466 Update setup.py 2022-11-16 21:18:27 -08:00
Jamie Hardt
d8f0b5694e Update setup.py 2022-11-16 21:18:14 -08:00
Jamie Hardt
742b0f96c3 Delete .idea directory 2022-11-16 21:15:21 -08:00
Jamie Hardt
d44948bdc1 Update LICENSE 2022-11-16 21:14:36 -08:00
Jamie Hardt
93cff15446 Update README.md 2022-11-16 21:04:33 -08:00
Jamie Hardt
fc914409ce Update README.md 2022-11-16 21:04:13 -08:00
Jamie Hardt
770e7f45a4 Fixed doc generation 2022-11-16 19:00:17 -08:00
Jamie Hardt
d3cf5fa5f2 Doc work 2022-11-16 18:47:16 -08:00
Jamie Hardt
67d2bd7093 Update classes.rst 2022-11-16 18:21:48 -08:00
Jamie Hardt
26a7eae437 Docs 2022-11-16 18:19:55 -08:00
Jamie Hardt
407aa1c1fd Update classes.rst 2022-11-16 18:15:58 -08:00
Jamie Hardt
13d0a80a10 Update classes.rst 2022-11-16 18:13:16 -08:00
Jamie Hardt
dcd2a22a43 Update classes.rst 2022-11-16 18:09:30 -08:00
Jamie Hardt
c586740269 Update classes.rst 2022-11-16 18:07:32 -08:00
Jamie Hardt
e28dbbbe5e Update classes.rst 2022-11-16 18:06:29 -08:00
Jamie Hardt
41df450452 Update classes.rst 2022-11-16 18:05:19 -08:00
Jamie Hardt
c37464036d Delete pycmx.rst 2022-11-16 18:04:25 -08:00
Jamie Hardt
9d89834eb3 Update index.rst 2022-11-16 18:02:15 -08:00
Jamie Hardt
8b53d2249c Docs 2022-11-16 18:00:53 -08:00
Jamie Hardt
0cdbc4e9be Docs 2022-11-16 17:59:08 -08:00
Jamie Hardt
e229e807b1 Update index.rst 2022-11-16 17:55:22 -08:00
Jamie Hardt
a7ee1f6737 Merge branch 'master' of https://github.com/iluvcapra/pycmx 2022-11-16 17:53:35 -08:00
Jamie Hardt
9097de8efa Delete modules.rst 2022-11-16 17:53:33 -08:00
Jamie Hardt
bc6d7f34c0 Update python-package.yml
Removed 3.6 from grid
2022-11-16 17:10:00 -08:00
Jamie Hardt
b642f859f3 Put it back 2022-11-16 17:09:22 -08:00
Jamie Hardt
29a9a5fba7 Removed coverage from reqs 2022-11-16 17:05:09 -08:00
Jamie Hardt
20ff7d7ee8 Added requirements.txt file 2022-11-16 17:03:44 -08:00
Jamie Hardt
183f121cfc Update channel_map.py
Added some typing
2022-11-16 16:57:21 -08:00
Jamie Hardt
e2dffcb745 Update .gitignore 2022-11-16 16:42:12 -08:00
Jamie Hardt
8c2ba3cc09 Update README.md
Added Lint and Test badge
2022-11-16 16:25:08 -08:00
Jamie Hardt
c7569045c1 Update python-package.yml
Named Package workflow
2022-11-16 16:24:16 -08:00
Jamie Hardt
50bcac23bb Update README.md
Removed travis badge.
2022-11-14 11:09:04 -08:00
Jamie Hardt
67b1631ba9 Update README.md
Removed codecov badge
2022-11-14 11:08:31 -08:00
Jamie Hardt
85cbafba8f Deleted .travis file 2022-11-13 17:57:06 -08:00
Jamie Hardt
595cf35e57 Update setup.py
Version 1.1.4
2022-11-13 17:53:51 -08:00
Jamie Hardt
7fa22d4b85 Update setup.py
Version 1.1.2
2022-11-13 17:50:26 -08:00
41 changed files with 14764 additions and 570 deletions

5
.flake8 Normal file
View 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
View 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

View File

@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package name: Lint and Test
on: on:
push: push:
@@ -16,8 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@@ -28,13 +27,12 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install flake8 pytest 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 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 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 --max-line-length=79 --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
pytest pytest

View File

@@ -2,25 +2,41 @@ name: Upload Python Package
on: on:
release: release:
types: [created] types: [published]
permissions:
contents: read
id-token: write
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment:
name: release
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v5.3.0
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install setuptools wheel twine pip install build
- name: Build and publish - name: Build package
env: run: python -m build .
TWINE_USERNAME: __token__ - name: Publish to Pypi
TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }} uses: pypa/gh-action-pypi-publish@v1.13.0
run: | with:
python setup.py sdist bdist_wheel password: ${{ secrets.PYPI_APIKEY }}
twine upload dist/* - 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

9
.gitignore vendored
View File

@@ -11,3 +11,12 @@
# Vim Swapfiles # Vim Swapfiles
*.swp *.swp
.DS_Store .DS_Store
venv/
.coverage
lcov.info
# venv
venv/

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/workspace.xml

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated
View File

@@ -1,4 +0,0 @@
<?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
View File

@@ -1,8 +0,0 @@
<?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
View File

@@ -1,11 +0,0 @@
<?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
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

32
.readthedocs.yaml Normal file
View 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

View File

@@ -1,18 +0,0 @@
language: python
python:
- "3.8"
- "3.7"
- "3.6"
- "3.5"
- "2.7"
script:
- "python setup.py test"
- "py.test tests/ -v --cov pycmx --cov-report term-missing"
install:
- "pip install setuptools"
before_install:
- "pip install coverage"
- "pip install codecov"
- "pip install pytest-cov==2.5.0"
after_success:
- "codecov"

13
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,13 @@
# Contributing
Contributions to this project are welcome!
The best way to contribute code to this project is to find this project on [Github][github] and submit a pull request.
## Call for EDLs
If you have EDLs you are having trouble working with becuase of unusual formatting, please send me a copy! Contact us
through [Github].
[github]: https://github.com/iluvcapra/pycmx

View File

@@ -1,4 +1,4 @@
Copyright (c) 2018 Jamie Hardt. Copyright (c) 2022 Jamie Hardt.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,16 +1,18 @@
[![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx)
[![codecov](https://codecov.io/gh/iluvcapra/pycmx/branch/master/graph/badge.svg)](https://codecov.io/gh/iluvcapra/pycmx)
[![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg) [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg)
![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/pycmx)
[![Lint and Test](https://github.com/iluvcapra/pycmx/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/pycmx/actions/workflows/python-package.yml)
# pycmx # pycmx
The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and its most most common variations. The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and
its most most common variations.
## Features ## Features
* The major variations of the CMX 3600: the standard, "File32" and "File128" * The major variations of the CMX 3600: the standard, "File32", "File128" and
formats are automatically detected and properly read. long Adobe Premiere event numbers are automatically detected and properly
read.
* Preserves relationship between events and individual edits/clips. * Preserves relationship between events and individual edits/clips.
* Remark or comment fields with common recognized forms are read and * Remark or comment fields with common recognized forms are read and
available to the client, including clip name and source file data. available to the client, including clip name and source file data.
@@ -83,5 +85,3 @@ Audio channel 7 is present
>>> events[2].edits[0].channels.video >>> events[2].edits[0].channels.video
False False
``` ```

View File

@@ -12,6 +12,7 @@ logging.basicConfig(format=FORMAT)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def all_video_edits(edl): def all_video_edits(edl):
for event in edl.events: for event in edl.events:
for edit in event.edits: for edit in event.edits:
@@ -30,46 +31,49 @@ def get_scene_name(edit, pattern):
else: else:
return edit.clip_name return edit.clip_name
def output_cmx(outfile, out_list): def output_cmx(outfile, out_list):
outfile.write("TITLE: SCENE LIST\r\n") outfile.write("TITLE: SCENE LIST\r\n")
outfile.write("FCM: NON-DROP FRAME\r\n") outfile.write("FCM: NON-DROP FRAME\r\n")
for o in out_list: for i, o in enumerate(out_list):
line = "%03i AX V C 00:00:00:00 00:00:00:00 %s %s\r\n" % (0, o['start'],o['end']) 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(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): def output_cols(outfile, out_list):
for o in 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): def scene_list(infile, outfile, out_format, pattern):
edl = pycmx.parse_cmx3600(infile) edl = pycmx.parse_cmx3600(infile)
current_scene_name = None current_scene_name = None
grouped_edits = [ ] grouped_edits = []
for edit in all_video_edits(edl): for edit in all_video_edits(edl):
this_scene_name = get_scene_name(edit, pattern) this_scene_name = get_scene_name(edit, pattern)
if this_scene_name is not None: if this_scene_name is not None:
if current_scene_name != this_scene_name: if current_scene_name != this_scene_name:
grouped_edits.append([ ]) grouped_edits.append([])
current_scene_name = this_scene_name current_scene_name = this_scene_name
grouped_edits[-1].append(edit) grouped_edits[-1].append(edit)
out_list = [ ] out_list = []
for group in grouped_edits: for group in grouped_edits:
out_list.append({ out_list.append({
'start': group[0].record_in, 'start': group[0].record_in,
'end': group[-1].record_out, 'end': group[-1].record_out,
'scene': get_scene_name(group[0], pattern ) } 'scene': get_scene_name(group[0], pattern)}
) )
if out_format == 'cmx': if out_format == 'cmx':
output_cmx(outfile, out_list) output_cmx(outfile, out_list)
if out_format == 'cols': if out_format == 'cols':
@@ -80,23 +84,29 @@ def scene_list(infile, outfile, out_format, pattern):
def scene_list_cli(): def scene_list_cli():
parser = argparse.ArgumentParser(description= parser = argparse.ArgumentParser(
'Read video events from an input CMX EDL and output events merged into scenes.') description='Read video events from an input CMX EDL and output '
parser.add_argument('-o','--outfile', default=sys.stdout, type=argparse.FileType('w'), 'events merged into scenes.')
help='Output file. Default is stdout.') parser.add_argument('-o', '--outfile', default=sys.stdout,
parser.add_argument('-f','--format', default='cmx', type=str, type=argparse.FileType('w'),
help='Output format. Options are cols and cmx, cmx is the default.') help='Output file. Default is stdout.')
parser.add_argument('-p','--pattern', default='V?([A-Z]*[0-9]+)', parser.add_argument('-f', '--format', default='cmx', type=str,
help='RE pattern for extracting scene name from clip name. The default is "V?([A-Z]*[0-9]+)". ' + \ help='Output format. Options are cols and cmx, cmx '
'This pattern will be matched case-insensitively.') 'is the default.')
parser.add_argument('input_edl', default=sys.stdin, type=argparse.FileType('r'), nargs='?', parser.add_argument('-p', '--pattern', default='V?([A-Z]*[0-9]+)',
help='Input file. Default is stdin.') 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() args = parser.parse_args()
infile = args.input_edl 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__': if __name__ == '__main__':
scene_list_cli() scene_list_cli()

Binary file not shown.

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

24
docs/source/classes.rst Normal file
View File

@@ -0,0 +1,24 @@
pycmx Classes
=============
.. autoclass:: pycmx.edit_list.EditList
:members:
.. autoclass:: pycmx.event.Event
:members:
.. autoclass:: pycmx.edit.Edit
:members:
.. autoclass:: pycmx.transition.Transition
:members:
.. autoclass:: pycmx.channel_map.ChannelMap
:members:

View File

@@ -12,21 +12,22 @@
# add these directories to sys.path here. If the directory is relative to the # 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. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
import importlib
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../../pycmx')) sys.path.insert(0, os.path.abspath('../..'))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = u'pycmx' project = u'pycmx'
copyright = u'2018, Jamie Hardt' copyright = u'(c) 2025, Jamie Hardt'
author = u'Jamie Hardt' author = u'Jamie Hardt'
release = importlib.metadata.version("pycmx")
version = release
# The short X.Y version # The short X.Y version
version = u''
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = u''
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@@ -63,7 +64,7 @@ master_doc = 'index'
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = 'em'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@@ -144,7 +145,7 @@ latex_documents = [
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
(master_doc, 'pycmx', u'pycmx Documentation', (master_doc, 'pycmx', u'pycmx Documentation',
[author], 1) [author], "3p")
] ]

7
docs/source/function.rst Normal file
View File

@@ -0,0 +1,7 @@
Parse Function
==============
.. autofunction:: pycmx.parse_cmx_events.parse_cmx3600

View File

@@ -8,13 +8,13 @@ Welcome to pycmx's documentation!
.. toctree:: .. toctree::
:maxdepth: 5 :maxdepth: 5
:caption: API Reference: :caption: API Reference
pycmx function
classes
Indices and tables Indices and tables
================== ==================
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex`
* :ref:`search` * :ref:`search`

View File

@@ -1,7 +0,0 @@
pycmx
=====
.. toctree::
:maxdepth: 4
pycmx

View File

@@ -1,46 +0,0 @@
pycmx package
=============
Submodules
----------
pycmx.channel\_map module
-------------------------
.. automodule:: pycmx.channel_map
:members:
:undoc-members:
:show-inheritance:
pycmx.parse\_cmx\_events module
-------------------------------
.. automodule:: pycmx.parse_cmx_events
:members:
:undoc-members:
:show-inheritance:
pycmx.parse\_cmx\_statements module
-----------------------------------
.. automodule:: pycmx.parse_cmx_statements
:members:
:undoc-members:
:show-inheritance:
pycmx.util module
-----------------
.. automodule:: pycmx.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pycmx
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,16 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
pycmx is a module for parsing CMX 3600-style EDLs. For more information and pycmx is a parser for CMX 3600-style EDLs.
examples see README.md
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 copy and reuse this software, refer to the LICENSE file included with the
distribution. distribution.
""" """
__version__ = '1.1.1'
__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

View File

@@ -2,89 +2,91 @@
# (c) 2018 Jamie Hardt # (c) 2018 Jamie Hardt
from re import (compile, match) from re import (compile, match)
from typing import Dict, Tuple, Generator
class ChannelMap: class ChannelMap:
""" """
Represents a set of all the channels to which an event applies. Represents a set of all the channels to which an event applies.
""" """
_chan_map = { _chan_map: Dict[str, Tuple] = {
"V" : (True, False, False), "V": (True, False, False),
"A" : (False, True, False), "A": (False, True, False),
"A2" : (False, False, True), "A2": (False, False, True),
"AA" : (False, True, True), "AA": (False, True, True),
"B" : (True, True, False), "B": (True, True, False),
"AA/V" : (True, True, True), "AA/V": (True, True, True),
"A2/V" : (True, False, True) "A2/V": (True, False, True)
} }
def __init__(self, v=False, audio_channels=set()): def __init__(self, v=False, audio_channels=set()):
self._audio_channel_set = audio_channels self._audio_channel_set = audio_channels
self.v = v self.v = v
@property @property
def video(self): def video(self) -> bool:
'True if video is included' 'True if video is included'
return self.v return self.v
@property @property
def audio(self): def audio(self) -> bool:
'True if an audio channel is included' 'True if an audio channel is included'
return len(self._audio_channel_set) > 0 return len(self._audio_channel_set) > 0
@property @property
def channels(self): def channels(self) -> Generator[int, None, None]:
'A generator for each audio channel' 'A generator for each audio channel'
for c in self._audio_channel_set: for c in self._audio_channel_set:
yield c yield c
@property @property
def a1(self): def a1(self) -> bool:
"""True if A1 is included.""" """True if A1 is included"""
return self.get_audio_channel(1) return self.get_audio_channel(1)
@a1.setter @a1.setter
def a1(self,val): def a1(self, val: bool):
self.set_audio_channel(1,val) self.set_audio_channel(1, val)
@property @property
def a2(self): def a2(self) -> bool:
"""True if A2 is included.""" """True if A2 is included"""
return self.get_audio_channel(2) return self.get_audio_channel(2)
@a2.setter @a2.setter
def a2(self,val): def a2(self, val: bool):
self.set_audio_channel(2,val) self.set_audio_channel(2, val)
@property @property
def a3(self): def a3(self) -> bool:
"""True if A3 is included.""" """True if A3 is included"""
return self.get_audio_channel(3) return self.get_audio_channel(3)
@a3.setter @a3.setter
def a3(self,val): def a3(self, val: bool):
self.set_audio_channel(3,val) self.set_audio_channel(3, val)
@property @property
def a4(self): def a4(self) -> bool:
"""True if A4 is included.""" """True if A4 is included"""
return self.get_audio_channel(4) return self.get_audio_channel(4)
@a4.setter @a4.setter
def a4(self,val): def a4(self, val: bool):
self.set_audio_channel(4,val) 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.""" """True if chan_num is included"""
return (chan_num in self._audio_channel_set) 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 is true, chan_num will be included"""
if enabled: if enabled:
self._audio_channel_set.add(chan_num) self._audio_channel_set.add(chan_num)
elif self.get_audio_channel(chan_num): elif self.get_audio_channel(chan_num):
self._audio_channel_set.remove(chan_num) self._audio_channel_set.remove(chan_num)
def _append_event(self, event_str): def _append_event(self, event_str):
alt_channel_re = compile(r'^A(\d+)') alt_channel_re = compile(r'^A(\d+)')
if event_str in self._chan_map: if event_str in self._chan_map:
@@ -95,7 +97,7 @@ class ChannelMap:
else: else:
matchresult = match(alt_channel_re, event_str) matchresult = match(alt_channel_re, event_str)
if matchresult: 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): def _append_ext(self, audio_ext):
self.a3 = audio_ext.audio3 self.a3 = audio_ext.audio3
@@ -103,9 +105,9 @@ class ChannelMap:
def __or__(self, other): def __or__(self, other):
""" """
Return the logical union of this channel map with another the logical union of this channel map with another
""" """
out_v = self.video | other.video out_v = self.video | other.video
out_a = self._audio_channel_set | other._audio_channel_set 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)

View File

@@ -3,59 +3,68 @@
from .transition import Transition from .transition import Transition
from .channel_map import ChannelMap from .channel_map import ChannelMap
from .parse_cmx_statements import StmtEffectsName # from .parse_cmx_statements import StmtEffectsName
from typing import Optional
class Edit: class Edit:
""" """
An individual source-to-record operation, with a source roll, source and An individual source-to-record operation, with a source roll, source and
recorder timecode in and out, a transition and channels. 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.edit_statement = edit_statement
self.audio_ext = audio_ext_statement self.audio_ext = audio_ext_statement
self.clip_name_statement = clip_name_statement self.clip_name_statement = clip_name_statement
self.source_file_statement = source_file_statement self.source_file_statement = source_file_statement
self.trans_name_statement = trans_name_statement self.trans_name_statement = trans_name_statement
@property @property
def line_number(self): def line_number(self) -> int:
""" """
Get the line number for the "standard form" statement associated with Get the line number for the "standard form" statement associated with
this edit. Line numbers a zero-indexed, such that the this edit. Line numbers a zero-indexed, such that the
"TITLE:" record is line zero. "TITLE:" record is line zero.
""" """
return self.edit_statement.line_number return self.edit_statement.line_number
@property @property
def channels(self): def channels(self) -> ChannelMap:
""" """
Get the :obj:`ChannelMap` object associated with this Edit. Get the :obj:`ChannelMap` object associated with this Edit.
""" """
cm = ChannelMap() cm = ChannelMap()
cm._append_event(self.edit_statement.channels) 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) cm._append_ext(self.audio_ext)
return cm return cm
@property @property
def transition(self): def transition(self) -> Transition:
""" """
Get the :obj:`Transition` object associated with this edit. Get the :obj:`Transition` object associated with this edit.
""" """
if self.trans_name_statement: 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: 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 @property
def source_in(self): def source_in(self) -> str:
""" """
Get the source in timecode. Get the source in timecode.
""" """
return self.edit_statement.source_in return self.edit_statement.source_in
@property @property
def source_out(self): def source_out(self) -> str:
""" """
Get the source out timecode. Get the source out timecode.
""" """
@@ -63,7 +72,7 @@ class Edit:
return self.edit_statement.source_out return self.edit_statement.source_out
@property @property
def record_in(self): def record_in(self) -> str:
""" """
Get the record in timecode. Get the record in timecode.
""" """
@@ -71,7 +80,7 @@ class Edit:
return self.edit_statement.record_in return self.edit_statement.record_in
@property @property
def record_out(self): def record_out(self) -> str:
""" """
Get the record out timecode. Get the record out timecode.
""" """
@@ -79,7 +88,7 @@ class Edit:
return self.edit_statement.record_out return self.edit_statement.record_out
@property @property
def source(self): def source(self) -> str:
""" """
Get the source column. This is the 8, 32 or 128-character string on the 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. event record line, this usually references the tape name of the source.
@@ -87,21 +96,21 @@ class Edit:
return self.edit_statement.source return self.edit_statement.source
@property @property
def black(self): def black(self) -> bool:
""" """
Black video or silence should be used as the source for this event. Black video or silence should be used as the source for this event.
""" """
return self.source == "BL" return self.source == "BL"
@property @property
def aux_source(self): def aux_source(self) -> bool:
""" """
An auxiliary source is the source of this event. An auxiliary source is the source of this event.
""" """
return self.source == "AX" return self.source == "AX"
@property @property
def source_file(self): def source_file(self) -> Optional[str]:
""" """
Get the source file, as attested by a "* SOURCE FILE" remark on the Get the source file, as attested by a "* SOURCE FILE" remark on the
EDL. This will return None if the information is not present. EDL. This will return None if the information is not present.
@@ -112,9 +121,9 @@ class Edit:
return self.source_file_statement.filename return self.source_file_statement.filename
@property @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 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 NAME" remark on the EDL. This will return None if the information is
not present. not present.
""" """
@@ -122,5 +131,3 @@ class Edit:
return None return None
else: else:
return self.clip_name_statement.name return self.clip_name_statement.name

View File

@@ -1,27 +1,34 @@
# pycmx # pycmx
# (c) 2018 Jamie Hardt # (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 .event import Event
from .channel_map import ChannelMap from .channel_map import ChannelMap
from typing import Generator
class EditList: 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): def __init__(self, statements):
self.title_statement = statements[0] self.title_statement = statements[0]
self.event_statements = statements[1:] self.event_statements = statements[1:]
@property @property
def format(self): def format(self) -> str:
""" """
The detected format of the EDL. Possible values are: `3600`,`File32`, The detected format of the EDL. Possible values are: "3600", "File32",
`File128`, and `unknown` "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:
if first_event.format == 8: if first_event.format == 8:
@@ -34,10 +41,9 @@ class EditList:
return 'unknown' return 'unknown'
else: else:
return 'unknown' return 'unknown'
@property @property
def channels(self): def channels(self) -> ChannelMap:
""" """
Return the union of every channel channel. Return the union of every channel channel.
""" """
@@ -48,38 +54,31 @@ class EditList:
retval = retval | edit.channels retval = retval | edit.channels
return retval return retval
@property @property
def title(self): def title(self) -> str:
""" """
The title of this edit list, as attensted by the 'TITLE:' statement on The title of this edit list.
the first line.
""" """
'The title of the edit list'
return self.title_statement.title return self.title_statement.title
@property @property
def unrecognized_statements(self): def unrecognized_statements(self) -> Generator[StmtUnrecognized,
None, None]:
""" """
A generator for all the unrecognized statements in the list. A generator for all the unrecognized statements in the list.
""" """
for s in self.event_statements: for s in self.event_statements:
if type(s) is StmtUnrecognized: if type(s) is StmtUnrecognized:
yield s yield s
@property @property
def events(self): def events(self) -> Generator[Event, None, None]:
'A generator for all the events in the edit list' 'A generator for all the events in the edit list'
is_drop = None
current_event_num = None current_event_num = None
event_statements = [] event_statements = []
for stmt in self.event_statements: for stmt in self.event_statements:
if type(stmt) is StmtFCM: if type(stmt) is StmtEvent:
is_drop = stmt.drop
elif type(stmt) is StmtEvent:
if current_event_num is None: if current_event_num is None:
current_event_num = stmt.event current_event_num = stmt.event
event_statements.append(stmt) event_statements.append(stmt)
@@ -99,13 +98,11 @@ class EditList:
yield Event(statements=event_statements) yield Event(statements=event_statements)
@property @property
def sources(self): def sources(self) -> Generator[StmtSourceUMID, None, None]:
""" """
A generator for all of the sources in the list A generator for all of the sources in the list
""" """
for stmt in self.event_statements: for stmt in self.event_statements:
if type(stmt) is StmtSourceUMID: if type(stmt) is StmtSourceUMID:
yield stmt yield stmt

View File

@@ -1,97 +1,108 @@
# pycmx # 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 .edit import Edit
from typing import List, Generator, Optional, Tuple, Any
class Event: 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): def __init__(self, statements):
self.statements = statements self.statements = statements
@property @property
def number(self): def number(self) -> int:
""" """
Return the event number. Return the event number.
""" """
return int(self._edit_statements()[0].event) return int(self._edit_statements()[0].event)
@property @property
def edits(self): def edits(self) -> List[Edit]:
""" """
Returns the edits. Most events will have a single edit, a single event 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 will have multiple edits when a dissolve, wipe or key transition needs
to be performed. to be performed.
""" """
edits_audio = list( self._statements_with_audio_ext() ) edits_audio = list(self._statements_with_audio_ext())
clip_names = self._clip_name_statements() clip_names = self._clip_name_statements()
source_files= self._source_file_statements() source_files = self._source_file_statements()
the_zip = [edits_audio] the_zip: List[List[Any]] = [edits_audio]
if len(edits_audio) == 2: if len(edits_audio) == 2:
cn = [None, None] start_name: Optional[StmtClipName] = None
end_name: Optional[StmtClipName] = None
for clip_name in clip_names: for clip_name in clip_names:
if clip_name.affect == 'from': if clip_name.affect == 'from':
cn[0] = clip_name start_name = clip_name
elif clip_name.affect == 'to': 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: else:
if len(edits_audio) == len(clip_names): if len(edits_audio) == len(clip_names):
the_zip.append(clip_names) the_zip.append(clip_names)
else: else:
the_zip.append([None] * len(edits_audio) ) the_zip.append([None] * len(edits_audio))
if len(edits_audio) == len(source_files): if len(edits_audio) == len(source_files):
the_zip.append(source_files) the_zip.append(source_files)
elif len(source_files) == 1: elif len(source_files) == 1:
the_zip.append( source_files * len(edits_audio) ) the_zip.append(source_files * len(edits_audio))
else: else:
the_zip.append([None] * len(edits_audio) ) the_zip.append([None] * len(edits_audio))
# attach trans name to last event # attach trans name to last event
try: try:
trans_statement = self._trans_name_statements()[0] 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) trans_names.append(trans_statement)
the_zip.append(trans_names) the_zip.append(trans_names)
except IndexError: except IndexError:
the_zip.append([None] * len(edits_audio) ) the_zip.append([None] * len(edits_audio))
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)]
return [ Edit(e1[0],e1[1],n1,s1,u1) for (e1,n1,s1,u1) in zip(*the_zip) ]
@property @property
def unrecognized_statements(self): def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
None]:
""" """
A generator for all the unrecognized statements in the event. A generator for all the unrecognized statements in the event.
""" """
for s in self.statements: for s in self.statements:
if type(s) is StmtUnrecognized: if type(s) is StmtUnrecognized:
yield s 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] 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] 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] 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] 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:]): for (s1, s2) in zip(self.statements, self.statements[1:]):
if type(s1) is StmtEvent and type(s2) is StmtAudioExt: if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
yield (s1,s2) yield (s1, s2)
elif type(s1) is StmtEvent: elif type(s1) is StmtEvent:
yield (s1, None) yield (s1, None)

View File

@@ -1,21 +1,20 @@
# pycmx # pycmx
# (c) 2018 Jamie Hardt # (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 from .edit_list import EditList
def parse_cmx3600(f): from typing import TextIO
def parse_cmx3600(f: TextIO) -> EditList:
""" """
Parse a CMX 3600 EDL. Parse a CMX 3600 EDL.
Args: :param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
f : a file-like object, anything that's readlines-able. :returns: An :class:`pycmx.edit_list.EditList`.
Returns:
An :class:`EditList`.
""" """
statements = parse_cmx3600_statements(f) statements = parse_cmx3600_statements(f)
return EditList(statements) return EditList(statements)

View File

@@ -2,166 +2,173 @@
# (c) 2018 Jamie Hardt # (c) 2018 Jamie Hardt
import re import re
import sys
from collections import namedtuple from collections import namedtuple
from itertools import count from typing import TextIO, List
from .util import collimate 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","format","line_number"]) "trans_op", "source_in", "source_out",
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"]) "record_in", "record_out", "format",
StmtClipName = namedtuple("ClipName",["name","affect","line_number"]) "line_number"])
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) StmtAudioExt = namedtuple("AudioExt", ["audio3", "audio4", "line_number"])
StmtRemark = namedtuple("Remark",["text","line_number"]) StmtClipName = namedtuple("ClipName", ["name", "affect", "line_number"])
StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) StmtSourceFile = namedtuple("SourceFile", ["filename", "line_number"])
StmtSourceUMID = namedtuple("Source",["name","umid","line_number"]) StmtRemark = namedtuple("Remark", ["text", "line_number"])
StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"]) StmtEffectsName = namedtuple("EffectsName", ["name", "line_number"])
StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields StmtSourceUMID = namedtuple("Source", ["name", "umid", "line_number"])
StmtUnrecognized = namedtuple("Unrecognized",["content","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. Return a list of every statement in the file argument.
""" """
lines = file.readlines() lines = file.readlines()
line_numbers = count() return [_parse_cmx3600_line(line.strip(), line_number)
return [_parse_cmx3600_line(line.strip(), line_number) \ for (line_number, line) in enumerate(lines)]
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]
def _parse_cmx3600_line(line, line_number): def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
long_event_num_p = re.compile("^[0-9]{6} ") return [event_field_length, 2, source_field_length, 1,
short_event_num_p = re.compile("^[0-9]{3} ") 4, 2, # chans
4, 1, # trans
if isinstance(line,str): 3, 1, # trans op
if line.startswith("TITLE:"): 11, 1,
return _parse_title(line,line_number) 11, 1,
elif line.startswith("FCM:"): 11, 1,
return _parse_fcm(line, line_number) 11]
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():
def _parse_title(line, line_num): # return [2, # "M2"
# 3,3, #
# 8,8,1,4,2,1,4,13,3,1,1]
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() 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() val = line[4:].strip()
if val == "DROP FRAME": if val == "DROP FRAME":
return StmtFCM(drop= True, line_number=line_num) return StmtFCM(drop=True, line_number=line_num)
else: 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): def _parse_extended_audio_channels(line, line_number):
content = line.strip() content = line.strip()
if content == "AUD 3": audio3 = True if "3" in content else False
return StmtAudioExt(audio3=True, audio4=False, line_number=line_number) audio4 = True if "4" in content else False
elif content == "AUD 4":
return StmtAudioExt(audio3=False, audio4=True, line_number=line_number) if audio3 or audio4:
elif content == "AUD 3 4": return StmtAudioExt(audio3, audio4, line_number)
return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
else: else:
return StmtUnrecognized(content=line, line_number=line_number) return StmtUnrecognized(line, line_number)
def _parse_remark(line, line_number):
def _parse_remark(line, line_number) -> object:
if line.startswith("FROM CLIP NAME:"): 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:"): 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:"): 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: else:
return StmtRemark(text=line, line_number=line_number) 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() name = line[16:].strip()
return StmtEffectsName(name=name, line_number=line_number) return StmtEffectsName(name=name, line_number=line_number)
def _parse_split(line, line_number): def _parse_split(line, line_number):
split_type = line[10:21] split_type = line[10:21]
is_video = False is_video = False
if split_type.startswith("VIDEO"): if split_type.startswith("VIDEO"):
is_video = True is_video = True
split_mag = line[24:35] split_mag = line[24:35]
return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number) return StmtSplitEdit(video=is_video, magnitude=split_mag,
line_number=line_number)
def _parse_motion_memory(line, line_number): def _parse_motion_memory(line, line_number):
return StmtMotionMemory(source = "", fps="") return StmtMotionMemory(source="", fps="")
def _parse_unrecognized(line, line_number): def _parse_unrecognized(line, line_number):
return StmtUnrecognized(content=line, line_number=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) col_widths = _edl_column_widths(event_field_length, source_field_length)
if sum(col_widths) > len(line): if sum(col_widths) > len(line):
return StmtUnrecognized(content=line, line_number=line_number) 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], return StmtEvent(event=column_strings[0],
source=column_strings[2].strip(), source=column_strings[2].strip(),
channels=column_strings[4].strip(), channels=column_strings[4].strip(),
trans=column_strings[6].strip(), trans=column_strings[6].strip(),
trans_op=column_strings[8].strip(), trans_op=column_strings[8].strip(),
source_in=column_strings[10].strip(), source_in=column_strings[10].strip(),
source_out=column_strings[12].strip(), source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(), record_in=column_strings[14].strip(),
record_out=column_strings[16].strip(), record_out=column_strings[16].strip(),
line_number=line_number, line_number=line_number, format=source_field_length)
format=source_field_length)
def _parse_source_umid_statement(line, line_number): 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) return StmtSourceUMID(name=None, umid=None, line_number=line_number)

View File

@@ -1,11 +1,14 @@
# pycmx # pycmx
# (c) 2018 Jamie Hardt # (c) 2023 Jamie Hardt
from typing import Optional
class Transition: class Transition:
""" """
A CMX transition: a wipe, dissolve or cut. A CMX transition: a wipe, dissolve or cut.
""" """
Cut = "C" Cut = "C"
Dissolve = "D" Dissolve = "D"
Wipe = "W" Wipe = "W"
@@ -19,7 +22,7 @@ class Transition:
self.name = name self.name = name
@property @property
def kind(self): def kind(self) -> Optional[str]:
""" """
Return the kind of transition: Cut, Wipe, etc Return the kind of transition: Cut, Wipe, etc
""" """
@@ -37,30 +40,30 @@ class Transition:
return Transition.KeyOut return Transition.KeyOut
@property @property
def cut(self): def cut(self) -> bool:
"`True` if this transition is a cut." "`True` if this transition is a cut."
return self.transition == 'C' return self.transition == 'C'
@property @property
def dissolve(self): def dissolve(self) -> bool:
"`True` if this traansition is a dissolve." "`True` if this traansition is a dissolve."
return self.transition == 'D' return self.transition == 'D'
@property @property
def wipe(self): def wipe(self) -> bool:
"`True` if this transition is a wipe." "`True` if this transition is a wipe."
return self.transition.startswith('W') return self.transition.startswith('W')
@property @property
def effect_duration(self): def effect_duration(self) -> int:
"""The duration of this transition, in frames of the record target. """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. In the event of a key event, this is the duration of the fade in.
""" """
return int(self.operand) return int(self.operand)
@property @property
def wipe_number(self): def wipe_number(self) -> Optional[int]:
"Wipes are identified by a particular number." "Wipes are identified by a particular number."
if self.wipe: if self.wipe:
return int(self.transition[1:]) return int(self.transition[1:])
@@ -68,17 +71,17 @@ class Transition:
return None return None
@property @property
def key_background(self): def key_background(self) -> bool:
"`True` if this edit is a key background." "`True` if this edit is a key background."
return self.transition == Transition.KeyBackground return self.transition == Transition.KeyBackground
@property @property
def key_foreground(self): def key_foreground(self) -> bool:
"`True` if this edit is a key foreground." "`True` if this edit is a key foreground."
return self.transition == Transition.Key return self.transition == Transition.Key
@property @property
def key_out(self): def key_out(self) -> bool:
""" """
`True` if this edit is a key out. This material will removed from `True` if this edit is a key out. This material will removed from
the key foreground and replaced with the key background. the key foreground and replaced with the key background.

View File

@@ -3,29 +3,28 @@
# Utility functions # Utility functions
def collimate(a_string, column_widths): def collimate(a_string, column_widths):
""" """
Split a list-type thing, like a string, into slices that are column_widths Split a list-type thing, like a string, into slices that are column_widths
length. length.
>>> collimate("a b1 c2345",[2,3,3,2]) >>> collimate("a b1 c2345",[2,3,3,2])
['a ','b1 ','c23','45'] ['a ','b1 ','c23','45']
Args: Args:
a_string: The string to split. This parameter can actually be anything a_string: The string to split. This parameter can actually be anything
sliceable. sliceable.
column_widths: A list of integers, each one is the length of a column. column_widths: A list of integers, each one is the length of a column.
Returns: Returns:
A list of slices. The len() of the returned list will *always* equal A list of slices. The len() of the returned list will *always* equal
len(:column_widths:). len(:column_widths:).
""" """
if len(column_widths) == 0: if len(column_widths) == 0:
return [] return []
width = column_widths[0] width = column_widths[0]
element = a_string[:width] element = a_string[:width]
rest = a_string[width:] rest = a_string[width:]
return [element] + collimate(rest, column_widths[1:]) return [element] + collimate(rest, column_widths[1:])

56
pyproject.toml Normal file
View 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"

View File

@@ -1,25 +0,0 @@
from setuptools import setup
with open("README.md", "r") as fh:
long_description = fh.read()
setup(name='pycmx',
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 :: 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'])

View 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

View 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

View 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*

File diff suppressed because it is too large Load Diff

View File

@@ -2,112 +2,138 @@ from unittest import TestCase
import pycmx import pycmx
class TestParse(TestCase): class TestParse(TestCase):
files = ["INS4_R1_010417.edl" , files = ["INS4_R1_010417.edl",
"INS4_R1_DX_092117.edl", "INS4_R1_DX_092117.edl",
"STP R1 v082517.edl", "STP R1 v082517.edl",
"ToD_R4_LOCK3.1_030618_Video.edl", "ToD_R4_LOCK3.1_030618_Video.edl",
"TEST.edl", "TEST.edl",
"test_edl_cdl.edl", "test_edl_cdl.edl",
"INS4_R1_DX_092117.edl" "INS4_R1_DX_092117.edl"
] ]
def test_event_counts(self): 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): 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) edl = pycmx.parse_cmx3600(f)
actual = len( list( edl.events )) actual = len(list(edl.events))
self.assertTrue( actual == count , self.assertTrue(actual == count,
"expected %i in file %s but found %i" % (count, fn, actual)) "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("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:
path = "tests/edls/" + fn path = "tests/edls/" + fn
with open(path ,'r') as f: 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,
self.assertTrue( event.number == index + 1 ) f"Failed for {path}")
self.assertEqual(event.number, index + 1,
f"Failed for {path}")
def test_events(self): 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) 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")
self.assertEqual( events[0].edits[0].source_file , "OY_HEAD_LEADER.MOV") self.assertEqual(
self.assertEqual( events[0].edits[0].source_in , "00:00:00:00") events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
self.assertEqual( events[0].edits[0].source_out , "00:00:00:00") self.assertEqual(events[0].edits[0].source_in, "00:00:00:00")
self.assertEqual( events[0].edits[0].record_in , "01:00:00:00") self.assertEqual(events[0].edits[0].source_out, "00:00:00:00")
self.assertEqual( events[0].edits[0].record_out , "01:00:08:00") self.assertEqual(events[0].edits[0].record_in, "01:00:00:00")
self.assertTrue( events[0].edits[0].transition.kind == pycmx.Transition.Cut) 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): 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) edl = pycmx.parse_cmx3600(f)
events = list( edl.events ) events = list(edl.events)
self.assertFalse( events[0].edits[0].channels.video) self.assertFalse(events[0].edits[0].channels.video)
self.assertFalse( events[0].edits[0].channels.a1) self.assertFalse(events[0].edits[0].channels.a1)
self.assertTrue( events[0].edits[0].channels.a2) 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.get_audio_channel(7))
self.assertTrue( events[2].edits[0].channels.audio) self.assertTrue(events[2].edits[0].channels.audio)
def test_multi_edit_events(self): 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) edl = pycmx.parse_cmx3600(f)
events = list( edl.events ) events = list(edl.events)
self.assertEqual( events[42].number , 43) self.assertEqual(events[42].number, 43)
self.assertEqual( len(events[42].edits), 2) self.assertEqual(len(events[42].edits), 2)
self.assertEqual( events[42].edits[0].source , "TC_R1_V1") 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].clip_name,
self.assertEqual( events[42].edits[0].source_in , "00:00:00:00") "TC R1 V1.2 TEMP1 FX ST.WAV")
self.assertEqual( events[42].edits[0].source_out , "00:00:00:00") self.assertEqual(events[42].edits[0].source_in, "00:00:00:00")
self.assertEqual( events[42].edits[0].record_in , "01:08:56:09") self.assertEqual(events[42].edits[0].source_out, "00:00:00:00")
self.assertEqual( events[42].edits[0].record_out , "01:08:56:09") self.assertEqual(events[42].edits[0].record_in, "01:08:56:09")
self.assertTrue( events[42].edits[0].transition.kind == pycmx.Transition.Cut) 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].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].clip_name,
self.assertEqual( events[42].edits[1].source_in , "00:00:00:00") "TC R1 V6 TEMP2 ST FX.WAV")
self.assertEqual( events[42].edits[1].source_out , "00:00:00:00") self.assertEqual(events[42].edits[1].source_in, "00:00:00:00")
self.assertEqual( events[42].edits[1].record_in , "01:08:56:09") self.assertEqual(events[42].edits[1].source_out, "00:00:00:00")
self.assertEqual( events[42].edits[1].record_out , "01:08:56:11") self.assertEqual(events[42].edits[1].record_in, "01:08:56:09")
self.assertTrue( events[42].edits[1].transition.kind == pycmx.Transition.Dissolve) 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): def test_line_numbers(self):
with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f: with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
events = list( edl.events ) events = list(edl.events)
self.assertEqual( events[0].edits[0].line_number, 2) self.assertEqual(events[0].edits[0].line_number, 2)
self.assertEqual( events[14].edits[0].line_number, 45) self.assertEqual(events[14].edits[0].line_number, 45)
self.assertEqual( events[180].edits[0].line_number, 544) self.assertEqual(events[180].edits[0].line_number, 544)
def test_transition_name(self): 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) 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")
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")