diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..cfcee6d --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# at some point there will be support for pdm +# see https://github.com/direnv/direnv/pull/1019 +layout python diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..1a015da --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,41 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install -e .[docs] + sphinx-build -b html docs/ docs/_build + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: docs/_build/ + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install enzyme + run: | + python -m pip install -U pip + python -m pip install -e ".[test]" + - name: Test + run: pytest diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..f7ae753 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,28 @@ +name: Publish + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v3 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 2ca3a38..0e7e0ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,71 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ *.py[cod] +*$py.class -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ .installed.cfg -lib -lib64 -__pycache__ +*.egg +MANIFEST -# Tests +# Unit test / coverage reports +htmlcov/ .tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ -# Installer logs -pip-log.txt +# Sphinx documentation +docs/_build/ -# PyDev -.project -.pydevproject -.settings +# pdm +.pdm.toml +.pdm-python +.pdm-build/ -# Visual Studio Code -.vscode/ +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ -# Sphinx -docs/_build +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.direnv/ -# Enzyme test folders -enzyme/tests/test_*/ +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json +# Enzyme +tests/data/ diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index 949c0d2..0000000 --- a/HISTORY.rst +++ /dev/null @@ -1,34 +0,0 @@ -Changelog -========= - -0.4.1 ------ -**release date:** 2013-11-28 - -* Fix parsing nested SeekHead elements -* Make parsing nested SeekHead elements optional -* Fix fromelement in Chapter when there is no ChapterDisplay - - -0.4.0 ------ -**release date:** 2013-10-30 - -* Import exceptions under enzyme namespace -* Change repr format -* Rename base exception -* Remove test file - - -0.3.1 ------ -**release date:** 2013-10-20 - -* Fix package distribution - - -0.3 ---- -**release date:** 2013-05-18 - -* Complete refactoring, for the old enzyme see https://github.com/Diaoul/enzyme-old diff --git a/LICENSE b/LICENSE index 32c6448..da9dca7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,22 @@ -Copyright 2013 Antoine Bertin +MIT License - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Copyright (c) 2021 Antoine Bertin - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 6ca7fbe..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include LICENSE -include HISTORY.rst -include tox.ini -include enzyme/parsers/ebml/specs/matroska.xml -include enzyme/tests/parsers/ebml/test1.mkv.yml - -graft docs - diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffe305a --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Enzyme + +Enzyme is a Python module to parse video metadata. + +## Usage + +Parse a MKV file metadata: + +```python +>>> import enzyme +>>> with open('example.mkv', 'rb') as f: +... mkv = enzyme.MKV(f) +... +>>> mkv.info + +>>> mkv.video_tracks +[] +>>> mkv.audio_tracks +[] +``` + +## License + +Enzyme is licensed under the MIT license. diff --git a/README.rst b/README.rst deleted file mode 100644 index 37277a4..0000000 --- a/README.rst +++ /dev/null @@ -1,41 +0,0 @@ -Enzyme -====== - -Enzyme is a Python module to parse video metadata. - -.. image:: https://travis-ci.org/Diaoul/enzyme.png?branch=master - :target: https://travis-ci.org/Diaoul/enzyme - - -Usage ------ - -Parse a MKV file metadata: - - >>> import enzyme - >>> with open('example.mkv', 'rb') as f: - ... mkv = enzyme.MKV(f) - ... - >>> mkv.info - - >>> mkv.video_tracks - [] - >>> mkv.audio_tracks - [] - -License -------- - -Copyright 2013-2015 Antoine Bertin - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/docs/conf.py b/docs/conf.py index 97f0360..890b817 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # enzyme documentation build configuration file, created by # sphinx-quickstart on Fri May 10 01:11:03 2013. @@ -12,38 +11,39 @@ # serve to show the default. import sys, os +import sphinx_rtd_theme # If extensions (or modules to document with autodoc) are in another directory, # 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. -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('_themes')) +sys.path.insert(0, os.path.abspath("..")) +sys.path.append(os.path.abspath("_themes")) import enzyme # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. project = enzyme.__title__ -copyright = ' '.join(enzyme.__copyright__.split()[1:]) +copyright = " ".join(enzyme.__copyright__.split()[1:]) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -56,188 +56,198 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'diaoul' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {'github_user': 'Diaoul', - 'github_repo': 'enzyme', - 'github_branch': 'master', - 'fork_me': 1, - 'flattr': 0, - 'gittip': 'Diaoul', - 'pypi_downloads': 1, - 'pypi_version': 0, - 'travis': 0, - 'coveralls': 0} +# html_theme_options = { +# "github_user": "Diaoul", +# "github_repo": "enzyme", +# "github_branch": "master", +# "fork_me": 1, +# "flattr": 0, +# "gittip": "Diaoul", +# "pypi_downloads": 1, +# "pypi_version": 0, +# "travis": 0, +# "coveralls": 0, +# } # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - 'index': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html', - 'sourcelink.html', 'searchbox.html'], - '**': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html', - 'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] + "index": [ + "sidebar-intro.html", + "sidebar-star.html", + "sidebar-pypi.html", + "sidebar-donate.html", + "sourcelink.html", + "searchbox.html", + ], + "**": [ + "sidebar-intro.html", + "sidebar-star.html", + "sidebar-pypi.html", + "sidebar-donate.html", + "localtoc.html", + "relations.html", + "sourcelink.html", + "searchbox.html", + ], } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'enzymedoc' +htmlhelp_basename = "enzymedoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'enzyme.tex', u'enzyme Documentation', - u'Antoine Bertin', 'manual'), + ("index", "enzyme.tex", "enzyme Documentation", "Antoine Bertin", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'enzyme', u'enzyme Documentation', - [u'Antoine Bertin'], 1) -] +man_pages = [("index", "enzyme", "enzyme Documentation", ["Antoine Bertin"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -246,23 +256,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'enzyme', u'enzyme Documentation', - u'Antoine Bertin', 'enzyme', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "enzyme", + "enzyme Documentation", + "Antoine Bertin", + "enzyme", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Options for autodoc ------------------------------------------------------- -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" diff --git a/enzyme/__init__.py b/enzyme/__init__.py index 134efbe..726ba71 100644 --- a/enzyme/__init__.py +++ b/enzyme/__init__.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- -__title__ = 'enzyme' -__version__ = '0.4.2-dev' -__author__ = 'Antoine Bertin' -__license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2013 Antoine Bertin' +__title__ = "enzyme" +__version__ = "0.5.0" +__author__ = "Antoine Bertin" +__license__ = "MIT" +__copyright__ = "Copyright 2013 Antoine Bertin" import logging from .exceptions import * diff --git a/enzyme/exceptions.py b/enzyme/exceptions.py index b2252aa..e7f62a0 100644 --- a/enzyme/exceptions.py +++ b/enzyme/exceptions.py @@ -1,27 +1,31 @@ -# -*- coding: utf-8 -*- -__all__ = ['Error', 'MalformedMKVError', 'ParserError', 'ReadError', 'SizeError'] +__all__ = ["Error", "MalformedMKVError", "ParserError", "ReadError", "SizeError"] class Error(Exception): """Base class for enzyme exceptions""" + pass class MalformedMKVError(Error): """Wrong or malformed element found""" + pass class ParserError(Error): """Base class for exceptions in parsers""" + pass class ReadError(ParserError): """Unable to correctly read""" + pass class SizeError(ParserError): """Mismatch between the type of the element and the size of its data""" + pass diff --git a/enzyme/mkv.py b/enzyme/mkv.py index 9dbce9b..88c1a37 100644 --- a/enzyme/mkv.py +++ b/enzyme/mkv.py @@ -1,12 +1,23 @@ -# -*- coding: utf-8 -*- from .exceptions import ParserError, MalformedMKVError from .parsers import ebml from datetime import timedelta import logging -__all__ = ['VIDEO_TRACK', 'AUDIO_TRACK', 'SUBTITLE_TRACK', 'MKV', 'Info', 'Track', 'VideoTrack', - 'AudioTrack', 'SubtitleTrack', 'Tag', 'SimpleTag', 'Chapter'] +__all__ = [ + "VIDEO_TRACK", + "AUDIO_TRACK", + "SUBTITLE_TRACK", + "MKV", + "Info", + "Track", + "VideoTrack", + "AudioTrack", + "SubtitleTrack", + "Tag", + "SimpleTag", + "Chapter", +] logger = logging.getLogger(__name__) @@ -14,12 +25,13 @@ VIDEO_TRACK, AUDIO_TRACK, SUBTITLE_TRACK = 0x01, 0x02, 0x11 -class MKV(object): +class MKV: """Matroska Video file :param stream: seekable file-like object """ + def __init__(self, stream, recurse_seek_head=False): # default attributes self.info = None @@ -35,73 +47,111 @@ def __init__(self, stream, recurse_seek_head=False): try: # get the Segment element - logger.info('Reading Segment element') + logger.info("Reading Segment element") specs = ebml.get_matroska_specs() - segments = ebml.parse(stream, specs, ignore_element_names=['EBML'], max_level=0) + segments = ebml.parse(stream, specs, ignore_element_names=["EBML"], max_level=0) if not segments: - raise MalformedMKVError('No Segment found') + raise MalformedMKVError("No Segment found") if len(segments) > 1: - logger.warning('%d segments found, using the first one', len(segments)) + logger.warning("%d segments found, using the first one", len(segments)) segment = segments[0] # get and recursively parse the SeekHead element - logger.info('Reading SeekHead element') + logger.info("Reading SeekHead element") stream.seek(segment.position) seek_head = ebml.parse_element(stream, specs) - if seek_head.name != 'SeekHead': - raise MalformedMKVError('No SeekHead found') - seek_head.load(stream, specs, ignore_element_names=['Void', 'CRC-32']) + if seek_head.name != "SeekHead": + raise MalformedMKVError("No SeekHead found") + seek_head.load(stream, specs, ignore_element_names=["Void", "CRC-32"]) self._parse_seekhead(seek_head, segment, stream, specs) except ParserError as e: - raise MalformedMKVError('Parsing error: %s' % e) + raise MalformedMKVError("Parsing error: %s" % e) def _parse_seekhead(self, seek_head, segment, stream, specs): for seek in seek_head: - element_id = ebml.read_element_id(seek['SeekID'].data) + element_id = ebml.read_element_id(seek["SeekID"].data) element_name = specs[element_id][1] - element_position = seek['SeekPosition'].data + segment.position + element_position = seek["SeekPosition"].data + segment.position if element_position in self._parsed_positions: - logger.warning('Skipping already parsed %s element at position %d', element_name, element_position) + logger.warning("Skipping already parsed %s element at position %d", element_name, element_position) continue - if element_name == 'Info': - logger.info('Processing element %s from SeekHead at position %d', element_name, element_position) + if element_name == "Info": + logger.info("Processing element %s from SeekHead at position %d", element_name, element_position) stream.seek(element_position) - self.info = Info.fromelement(ebml.parse_element(stream, specs, True, ignore_element_names=['Void', 'CRC-32'])) - elif element_name == 'Tracks': - logger.info('Processing element %s from SeekHead at position %d', element_name, element_position) + self.info = Info.fromelement( + ebml.parse_element(stream, specs, True, ignore_element_names=["Void", "CRC-32"]) + ) + elif element_name == "Tracks": + logger.info("Processing element %s from SeekHead at position %d", element_name, element_position) stream.seek(element_position) - tracks = ebml.parse_element(stream, specs, True, ignore_element_names=['Void', 'CRC-32']) - self.video_tracks.extend([VideoTrack.fromelement(t) for t in tracks if t['TrackType'].data == VIDEO_TRACK]) - self.audio_tracks.extend([AudioTrack.fromelement(t) for t in tracks if t['TrackType'].data == AUDIO_TRACK]) - self.subtitle_tracks.extend([SubtitleTrack.fromelement(t) for t in tracks if t['TrackType'].data == SUBTITLE_TRACK]) - elif element_name == 'Chapters': - logger.info('Processing element %s from SeekHead at position %d', element_name, element_position) + tracks = ebml.parse_element(stream, specs, True, ignore_element_names=["Void", "CRC-32"]) + self.video_tracks.extend( + [VideoTrack.fromelement(t) for t in tracks if t["TrackType"].data == VIDEO_TRACK] + ) + self.audio_tracks.extend( + [AudioTrack.fromelement(t) for t in tracks if t["TrackType"].data == AUDIO_TRACK] + ) + self.subtitle_tracks.extend( + [SubtitleTrack.fromelement(t) for t in tracks if t["TrackType"].data == SUBTITLE_TRACK] + ) + elif element_name == "Chapters": + logger.info("Processing element %s from SeekHead at position %d", element_name, element_position) stream.seek(element_position) - self.chapters.extend([Chapter.fromelement(c) for c in ebml.parse_element(stream, specs, True, ignore_element_names=['Void', 'CRC-32'])[0] if c.name == 'ChapterAtom']) - elif element_name == 'Tags': - logger.info('Processing element %s from SeekHead at position %d', element_name, element_position) + self.chapters.extend( + [ + Chapter.fromelement(c) + for c in ebml.parse_element(stream, specs, True, ignore_element_names=["Void", "CRC-32"])[0] + if c.name == "ChapterAtom" + ] + ) + elif element_name == "Tags": + logger.info("Processing element %s from SeekHead at position %d", element_name, element_position) stream.seek(element_position) - self.tags.extend([Tag.fromelement(t) for t in ebml.parse_element(stream, specs, True, ignore_element_names=['Void', 'CRC-32'])]) - elif element_name == 'SeekHead' and self.recurse_seek_head: - logger.info('Processing element %s from SeekHead at position %d', element_name, element_position) + self.tags.extend( + [ + Tag.fromelement(t) + for t in ebml.parse_element(stream, specs, True, ignore_element_names=["Void", "CRC-32"]) + ] + ) + elif element_name == "SeekHead" and self.recurse_seek_head: + logger.info("Processing element %s from SeekHead at position %d", element_name, element_position) stream.seek(element_position) - self._parse_seekhead(ebml.parse_element(stream, specs, True, ignore_element_names=['Void', 'CRC-32']), segment, stream, specs) + self._parse_seekhead( + ebml.parse_element(stream, specs, True, ignore_element_names=["Void", "CRC-32"]), + segment, + stream, + specs, + ) else: - logger.debug('Element %s ignored', element_name) + logger.debug("Element %s ignored", element_name) self._parsed_positions.add(element_position) def to_dict(self): - return {'info': self.info.__dict__, 'video_tracks': [t.__dict__ for t in self.video_tracks], - 'audio_tracks': [t.__dict__ for t in self.audio_tracks], 'subtitle_tracks': [t.__dict__ for t in self.subtitle_tracks], - 'chapters': [c.__dict__ for c in self.chapters], 'tags': [t.__dict__ for t in self.tags]} + return { + "info": self.info.__dict__, + "video_tracks": [t.__dict__ for t in self.video_tracks], + "audio_tracks": [t.__dict__ for t in self.audio_tracks], + "subtitle_tracks": [t.__dict__ for t in self.subtitle_tracks], + "chapters": [c.__dict__ for c in self.chapters], + "tags": [t.__dict__ for t in self.tags], + } def __repr__(self): - return '<%s [%r, %r, %r, %r]>' % (self.__class__.__name__, self.info, self.video_tracks, self.audio_tracks, self.subtitle_tracks) + return "<%s [%r, %r, %r, %r]>" % ( + self.__class__.__name__, + self.info, + self.video_tracks, + self.audio_tracks, + self.subtitle_tracks, + ) -class Info(object): +class Info: """Object for the Info EBML element""" - def __init__(self, title=None, duration=None, date_utc=None, timecode_scale=None, muxing_app=None, writing_app=None): + + def __init__( + self, title=None, duration=None, date_utc=None, timecode_scale=None, muxing_app=None, writing_app=None + ): self.title = title self.duration = timedelta(microseconds=duration * (timecode_scale or 1000000) // 1000) if duration else None self.date_utc = date_utc @@ -116,25 +166,42 @@ def fromelement(cls, element): :type element: :class:`~enzyme.parsers.ebml.Element` """ - title = element.get('Title') - duration = element.get('Duration') - date_utc = element.get('DateUTC') - timecode_scale = element.get('TimecodeScale') - muxing_app = element.get('MuxingApp') - writing_app = element.get('WritingApp') + title = element.get("Title") + duration = element.get("Duration") + date_utc = element.get("DateUTC") + timecode_scale = element.get("TimecodeScale") + muxing_app = element.get("MuxingApp") + writing_app = element.get("WritingApp") return cls(title, duration, date_utc, timecode_scale, muxing_app, writing_app) def __repr__(self): - return '<%s [title=%r, duration=%s, date=%s]>' % (self.__class__.__name__, self.title, self.duration, self.date_utc) + return "<%s [title=%r, duration=%s, date=%s]>" % ( + self.__class__.__name__, + self.title, + self.duration, + self.date_utc, + ) def __str__(self): return repr(self.__dict__) -class Track(object): +class Track: """Base object for the Tracks EBML element""" - def __init__(self, type=None, number=None, name=None, language=None, enabled=None, default=None, forced=None, lacing=None, # @ReservedAssignment - codec_id=None, codec_name=None): + + def __init__( + self, + type=None, + number=None, + name=None, + language=None, + enabled=None, + default=None, + forced=None, + lacing=None, # @ReservedAssignment + codec_id=None, + codec_name=None, + ): self.type = type self.number = number self.name = name @@ -154,21 +221,31 @@ def fromelement(cls, element): :type element: :class:`~enzyme.parsers.ebml.Element` """ - type = element.get('TrackType') # @ReservedAssignment - number = element.get('TrackNumber', 0) - name = element.get('Name') - language = element.get('Language') - enabled = bool(element.get('FlagEnabled', 1)) - default = bool(element.get('FlagDefault', 1)) - forced = bool(element.get('FlagForced', 0)) - lacing = bool(element.get('FlagLacing', 1)) - codec_id = element.get('CodecID') - codec_name = element.get('CodecName') - return cls(type=type, number=number, name=name, language=language, enabled=enabled, default=default, - forced=forced, lacing=lacing, codec_id=codec_id, codec_name=codec_name) + type = element.get("TrackType") # @ReservedAssignment + number = element.get("TrackNumber", 0) + name = element.get("Name") + language = element.get("Language") + enabled = bool(element.get("FlagEnabled", 1)) + default = bool(element.get("FlagDefault", 1)) + forced = bool(element.get("FlagForced", 0)) + lacing = bool(element.get("FlagLacing", 1)) + codec_id = element.get("CodecID") + codec_name = element.get("CodecName") + return cls( + type=type, + number=number, + name=name, + language=language, + enabled=enabled, + default=default, + forced=forced, + lacing=lacing, + codec_id=codec_id, + codec_name=codec_name, + ) def __repr__(self): - return '<%s [%d, name=%r, language=%s]>' % (self.__class__.__name__, self.number, self.name, self.language) + return "<%s [%d, name=%r, language=%s]>" % (self.__class__.__name__, self.number, self.name, self.language) def __str__(self): return str(self.__dict__) @@ -176,8 +253,20 @@ def __str__(self): class VideoTrack(Track): """Object for the Tracks EBML element with :data:`VIDEO_TRACK` TrackType""" - def __init__(self, width=0, height=0, interlaced=False, stereo_mode=None, crop=None, - display_width=None, display_height=None, display_unit=None, aspect_ratio_type=None, **kwargs): + + def __init__( + self, + width=0, + height=0, + interlaced=False, + stereo_mode=None, + crop=None, + display_width=None, + display_height=None, + display_unit=None, + aspect_ratio_type=None, + **kwargs, + ): super(VideoTrack, self).__init__(**kwargs) self.width = width self.height = height @@ -198,28 +287,35 @@ def fromelement(cls, element): """ videotrack = super(VideoTrack, cls).fromelement(element) - videotrack.width = element['Video'].get('PixelWidth', 0) - videotrack.height = element['Video'].get('PixelHeight', 0) - videotrack.interlaced = bool(element['Video'].get('FlagInterlaced', False)) - videotrack.stereo_mode = element['Video'].get('StereoMode') + videotrack.width = element["Video"].get("PixelWidth", 0) + videotrack.height = element["Video"].get("PixelHeight", 0) + videotrack.interlaced = bool(element["Video"].get("FlagInterlaced", False)) + videotrack.stereo_mode = element["Video"].get("StereoMode") videotrack.crop = {} - if 'PixelCropTop' in element['Video']: - videotrack.crop['top'] = element['Video']['PixelCropTop'] - if 'PixelCropBottom' in element['Video']: - videotrack.crop['bottom'] = element['Video']['PixelCropBottom'] - if 'PixelCropLeft' in element['Video']: - videotrack.crop['left'] = element['Video']['PixelCropLeft'] - if 'PixelCropRight' in element['Video']: - videotrack.crop['right'] = element['Video']['PixelCropRight'] - videotrack.display_width = element['Video'].get('DisplayWidth') - videotrack.display_height = element['Video'].get('DisplayHeight') - videotrack.display_unit = element['Video'].get('DisplayUnit') - videotrack.aspect_ratio_type = element['Video'].get('AspectRatioType') + if "PixelCropTop" in element["Video"]: + videotrack.crop["top"] = element["Video"]["PixelCropTop"] + if "PixelCropBottom" in element["Video"]: + videotrack.crop["bottom"] = element["Video"]["PixelCropBottom"] + if "PixelCropLeft" in element["Video"]: + videotrack.crop["left"] = element["Video"]["PixelCropLeft"] + if "PixelCropRight" in element["Video"]: + videotrack.crop["right"] = element["Video"]["PixelCropRight"] + videotrack.display_width = element["Video"].get("DisplayWidth") + videotrack.display_height = element["Video"].get("DisplayHeight") + videotrack.display_unit = element["Video"].get("DisplayUnit") + videotrack.aspect_ratio_type = element["Video"].get("AspectRatioType") return videotrack def __repr__(self): - return '<%s [%d, %dx%d, %s, name=%r, language=%s]>' % (self.__class__.__name__, self.number, self.width, self.height, - self.codec_id, self.name, self.language) + return "<%s [%d, %dx%d, %s, name=%r, language=%s]>" % ( + self.__class__.__name__, + self.number, + self.width, + self.height, + self.codec_id, + self.name, + self.language, + ) def __str__(self): return str(self.__dict__) @@ -227,7 +323,10 @@ def __str__(self): class AudioTrack(Track): """Object for the Tracks EBML element with :data:`AUDIO_TRACK` TrackType""" - def __init__(self, sampling_frequency=None, channels=None, output_sampling_frequency=None, bit_depth=None, **kwargs): + + def __init__( + self, sampling_frequency=None, channels=None, output_sampling_frequency=None, bit_depth=None, **kwargs + ): super(AudioTrack, self).__init__(**kwargs) self.sampling_frequency = sampling_frequency self.channels = channels @@ -243,24 +342,33 @@ def fromelement(cls, element): """ audiotrack = super(AudioTrack, cls).fromelement(element) - audiotrack.sampling_frequency = element['Audio'].get('SamplingFrequency', 8000.0) - audiotrack.channels = element['Audio'].get('Channels', 1) - audiotrack.output_sampling_frequency = element['Audio'].get('OutputSamplingFrequency') - audiotrack.bit_depth = element['Audio'].get('BitDepth') + audiotrack.sampling_frequency = element["Audio"].get("SamplingFrequency", 8000.0) + audiotrack.channels = element["Audio"].get("Channels", 1) + audiotrack.output_sampling_frequency = element["Audio"].get("OutputSamplingFrequency") + audiotrack.bit_depth = element["Audio"].get("BitDepth") return audiotrack def __repr__(self): - return '<%s [%d, %d channel(s), %.0fHz, %s, name=%r, language=%s]>' % (self.__class__.__name__, self.number, self.channels, - self.sampling_frequency, self.codec_id, self.name, self.language) + return "<%s [%d, %d channel(s), %.0fHz, %s, name=%r, language=%s]>" % ( + self.__class__.__name__, + self.number, + self.channels, + self.sampling_frequency, + self.codec_id, + self.name, + self.language, + ) class SubtitleTrack(Track): """Object for the Tracks EBML element with :data:`SUBTITLE_TRACK` TrackType""" + pass -class Tag(object): +class Tag: """Object for the Tag EBML element""" + def __init__(self, targets=None, simpletags=None): self.targets = targets if targets is not None else [] self.simpletags = simpletags if simpletags is not None else [] @@ -273,17 +381,18 @@ def fromelement(cls, element): :type element: :class:`~enzyme.parsers.ebml.Element` """ - targets = element['Targets'] if 'Targets' in element else [] - simpletags = [SimpleTag.fromelement(s) for s in element if s.name == 'SimpleTag'] + targets = element["Targets"] if "Targets" in element else [] + simpletags = [SimpleTag.fromelement(s) for s in element if s.name == "SimpleTag"] return cls(targets, simpletags) def __repr__(self): - return '<%s [targets=%r, simpletags=%r]>' % (self.__class__.__name__, self.targets, self.simpletags) + return "<%s [targets=%r, simpletags=%r]>" % (self.__class__.__name__, self.targets, self.simpletags) -class SimpleTag(object): +class SimpleTag: """Object for the SimpleTag EBML element""" - def __init__(self, name, language='und', default=True, string=None, binary=None): + + def __init__(self, name, language="und", default=True, string=None, binary=None): self.name = name self.language = language self.default = default @@ -298,18 +407,24 @@ def fromelement(cls, element): :type element: :class:`~enzyme.parsers.ebml.Element` """ - name = element.get('TagName') - language = element.get('TagLanguage', 'und') - default = element.get('TagDefault', True) - string = element.get('TagString') - binary = element.get('TagBinary') + name = element.get("TagName") + language = element.get("TagLanguage", "und") + default = element.get("TagDefault", True) + string = element.get("TagString") + binary = element.get("TagBinary") return cls(name, language, default, string, binary) def __repr__(self): - return '<%s [%s, language=%s, default=%s, string=%s]>' % (self.__class__.__name__, self.name, self.language, self.default, self.string) + return "<%s [%s, language=%s, default=%s, string=%s]>" % ( + self.__class__.__name__, + self.name, + self.language, + self.default, + self.string, + ) -class Chapter(object): +class Chapter: """Object for the ChapterAtom and ChapterDisplay EBML element .. note:: @@ -318,6 +433,7 @@ class Chapter(object): are merged into the :class:`Chapter` """ + def __init__(self, start, hidden=False, enabled=False, end=None, string=None, language=None): self.start = start self.hidden = hidden @@ -334,18 +450,20 @@ def fromelement(cls, element): :type element: :class:`~enzyme.parsers.ebml.Element` """ - start = timedelta(microseconds=element.get('ChapterTimeStart') // 1000) - hidden = element.get('ChapterFlagHidden', False) - enabled = element.get('ChapterFlagEnabled', True) - end = element.get('ChapterTimeEnd') - chapterdisplays = [c for c in element if c.name == 'ChapterDisplay'] + start = timedelta(microseconds=element.get("ChapterTimeStart") // 1000) + hidden = element.get("ChapterFlagHidden", False) + enabled = element.get("ChapterFlagEnabled", True) + end = element.get("ChapterTimeEnd") + chapterdisplays = [c for c in element if c.name == "ChapterDisplay"] if len(chapterdisplays) > 1: - logger.warning('More than 1 (%d) ChapterDisplay element in the ChapterAtom, using the first one', len(chapterdisplays)) + logger.warning( + "More than 1 (%d) ChapterDisplay element in the ChapterAtom, using the first one", len(chapterdisplays) + ) if chapterdisplays: - string = chapterdisplays[0].get('ChapString') - language = chapterdisplays[0].get('ChapLanguage') + string = chapterdisplays[0].get("ChapString") + language = chapterdisplays[0].get("ChapLanguage") return cls(start, hidden, enabled, end, string, language) return cls(start, hidden, enabled, end) def __repr__(self): - return '<%s [%s, enabled=%s]>' % (self.__class__.__name__, self.start, self.enabled) + return "<%s [%s, enabled=%s]>" % (self.__class__.__name__, self.start, self.enabled) diff --git a/enzyme/parsers/__init__.py b/enzyme/parsers/__init__.py index 40a96af..e69de29 100644 --- a/enzyme/parsers/__init__.py +++ b/enzyme/parsers/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/enzyme/parsers/ebml/__init__.py b/enzyme/parsers/ebml/__init__.py index 04219f8..1097fab 100644 --- a/enzyme/parsers/ebml/__init__.py +++ b/enzyme/parsers/ebml/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- from .core import * from .readers import * diff --git a/enzyme/parsers/ebml/core.py b/enzyme/parsers/ebml/core.py index 43300e7..9c25abe 100644 --- a/enzyme/parsers/ebml/core.py +++ b/enzyme/parsers/ebml/core.py @@ -8,9 +8,23 @@ except ImportError: from importlib_resources import files # type: ignore[assignment,no-redef,import-not-found] -__all__ = ['INTEGER', 'UINTEGER', 'FLOAT', 'STRING', 'UNICODE', 'DATE', 'MASTER', 'BINARY', - 'SPEC_TYPES', 'READERS', 'Element', 'MasterElement', 'parse', 'parse_element', - 'get_matroska_specs'] +__all__ = [ + "INTEGER", + "UINTEGER", + "FLOAT", + "STRING", + "UNICODE", + "DATE", + "MASTER", + "BINARY", + "SPEC_TYPES", + "READERS", + "Element", + "MasterElement", + "parse", + "parse_element", + "get_matroska_specs", +] logger = logging.getLogger(__name__) @@ -19,14 +33,14 @@ # Spec types to EBML types mapping SPEC_TYPES = { - 'integer': INTEGER, - 'uinteger': UINTEGER, - 'float': FLOAT, - 'string': STRING, - 'utf-8': UNICODE, - 'date': DATE, - 'master': MASTER, - 'binary': BINARY + "integer": INTEGER, + "uinteger": UINTEGER, + "float": FLOAT, + "string": STRING, + "utf-8": UNICODE, + "date": DATE, + "master": MASTER, + "binary": BINARY, } # Readers to use per EBML type @@ -37,11 +51,11 @@ STRING: read_element_string, UNICODE: read_element_unicode, DATE: read_element_date, - BINARY: read_element_binary + BINARY: read_element_binary, } -class Element(object): +class Element: """Base object of EBML :param int id: id of the element, best represented as hexadecimal (0x18538067 for Matroska Segment element) @@ -54,7 +68,10 @@ class Element(object): :param data: data as read by the corresponding :data:`READERS` """ - def __init__(self, id=None, type=None, name=None, level=None, position=None, size=None, data=None): # @ReservedAssignment + + def __init__( + self, id=None, type=None, name=None, level=None, position=None, size=None, data=None + ): # @ReservedAssignment self.id = id self.type = type self.name = name @@ -64,7 +81,7 @@ def __init__(self, id=None, type=None, name=None, level=None, position=None, siz self.data = data def __repr__(self): - return '<%s [%s, %r]>' % (self.__class__.__name__, self.name, self.data) + return "<%s [%s, %r]>" % (self.__class__.__name__, self.name, self.data) class MasterElement(Element): @@ -91,6 +108,7 @@ class MasterElement(Element): Element(DocType, u'matroska') """ + def __init__(self, id=None, name=None, level=None, position=None, size=None, data=None): # @ReservedAssignment super(MasterElement, self).__init__(id, MASTER, name, level, position, size, data) @@ -120,7 +138,7 @@ def get(self, name, default=None): return default element = self[name] if element.type == MASTER: - raise ValueError('%s is a MasterElement' % name) + raise ValueError("%s is a MasterElement" % name) return element.data def __getitem__(self, key): @@ -130,7 +148,7 @@ def __getitem__(self, key): if not children: raise KeyError(key) if len(children) > 1: - raise KeyError('More than 1 child with key %s (%d)' % (key, len(children))) + raise KeyError("More than 1 child with key %s (%d)" % (key, len(children))) return children[0] def __contains__(self, item): @@ -167,19 +185,31 @@ def parse(stream, specs, size=None, ignore_element_types=None, ignore_element_na element = parse_element(stream, specs) if element is None: continue - logger.debug('%s %s parsed', element.__class__.__name__, element.name) + logger.debug("%s %s parsed", element.__class__.__name__, element.name) if element.type in ignore_element_types or element.name in ignore_element_names: - logger.info('%s %s ignored', element.__class__.__name__, element.name) + logger.info("%s %s ignored", element.__class__.__name__, element.name) if element.type == MASTER: stream.seek(element.size, 1) continue if element.type == MASTER: if max_level is not None and element.level >= max_level: - logger.info('Maximum level %d reached for children of %s %s', max_level, element.__class__.__name__, element.name) + logger.info( + "Maximum level %d reached for children of %s %s", + max_level, + element.__class__.__name__, + element.name, + ) stream.seek(element.size, 1) else: - logger.debug('Loading child elements for %s %s with size %d', element.__class__.__name__, element.name, element.size) - element.data = parse(stream, specs, element.size, ignore_element_types, ignore_element_names, max_level) + logger.debug( + "Loading child elements for %s %s with size %d", + element.__class__.__name__, + element.name, + element.size, + ) + element.data = parse( + stream, specs, element.size, ignore_element_types, ignore_element_names, max_level + ) elements.append(element) except ReadError: if size is not None: @@ -188,7 +218,9 @@ def parse(stream, specs, size=None, ignore_element_types=None, ignore_element_na return elements -def parse_element(stream, specs, load_children=False, ignore_element_types=None, ignore_element_names=None, max_level=None): +def parse_element( + stream, specs, load_children=False, ignore_element_types=None, ignore_element_names=None, max_level=None +): """Extract a single :class:`Element` from the `stream` according to the `specs` :param stream: file-like object from which to read @@ -205,12 +237,12 @@ def parse_element(stream, specs, load_children=False, ignore_element_types=None, ignore_element_names = ignore_element_names if ignore_element_names is not None else [] element_id = read_element_id(stream) if element_id is None: - raise ReadError('Cannot read element id') + raise ReadError("Cannot read element id") element_size = read_element_size(stream) if element_size is None: - raise ReadError('Cannot read element size') + raise ReadError("Cannot read element size") if element_id not in specs: - logger.error('Element with id 0x%x is not in the specs' % element_id) + logger.error("Element with id 0x%x is not in the specs" % element_id) stream.seek(element_size, 1) return None element_type, element_name, element_level = specs[element_id] @@ -233,10 +265,14 @@ def get_matroska_specs(webm_only=False): """ specs = {} - spec_file = files(__package__).joinpath('specs', 'matroska.xml') - with spec_file.open('rb') as resource: + spec_file = files(__package__).joinpath("specs", "matroska.xml") + with spec_file.open("rb") as resource: xmldoc = minidom.parse(resource) - for element in xmldoc.getElementsByTagName('element'): - if not webm_only or element.hasAttribute('webm') and element.getAttribute('webm') == '1': - specs[int(element.getAttribute('id'), 16)] = (SPEC_TYPES[element.getAttribute('type')], element.getAttribute('name'), int(element.getAttribute('level'))) + for element in xmldoc.getElementsByTagName("element"): + if not webm_only or element.hasAttribute("webm") and element.getAttribute("webm") == "1": + specs[int(element.getAttribute("id"), 16)] = ( + SPEC_TYPES[element.getAttribute("type")], + element.getAttribute("name"), + int(element.getAttribute("level")), + ) return specs diff --git a/enzyme/parsers/ebml/readers.py b/enzyme/parsers/ebml/readers.py index 7429c2e..c415380 100644 --- a/enzyme/parsers/ebml/readers.py +++ b/enzyme/parsers/ebml/readers.py @@ -1,13 +1,20 @@ -# -*- coding: utf-8 -*- from ...exceptions import ReadError, SizeError from datetime import datetime, timedelta from io import BytesIO from struct import unpack -__all__ = ['read_element_id', 'read_element_size', 'read_element_integer', 'read_element_uinteger', - 'read_element_float', 'read_element_string', 'read_element_unicode', 'read_element_date', - 'read_element_binary'] +__all__ = [ + "read_element_id", + "read_element_size", + "read_element_integer", + "read_element_uinteger", + "read_element_float", + "read_element_string", + "read_element_unicode", + "read_element_date", + "read_element_binary", +] def _read(stream, size): @@ -23,7 +30,7 @@ def _read(stream, size): """ data = stream.read(size) if len(data) < size: - raise ReadError('Less than %d bytes read (%d)' % (size, len(data))) + raise ReadError("Less than %d bytes read (%d)" % (size, len(data))) return data @@ -41,14 +48,14 @@ def read_element_id(stream): if byte & 0x80: return byte elif byte & 0x40: - return unpack('>H', char + _read(stream, 1))[0] + return unpack(">H", char + _read(stream, 1))[0] elif byte & 0x20: - b, h = unpack('>BH', char + _read(stream, 2)) - return b * 2 ** 16 + h + b, h = unpack(">BH", char + _read(stream, 2)) + return b * 2**16 + h elif byte & 0x10: - return unpack('>L', char + _read(stream, 3))[0] + return unpack(">L", char + _read(stream, 3))[0] else: - ValueError('Not an Element ID') + ValueError("Not an Element ID") def read_element_size(stream): @@ -63,27 +70,27 @@ def read_element_size(stream): char = _read(stream, 1) byte = ord(char) if byte & 0x80: - return unpack('>B', bytes((byte ^ 0x80,)))[0] + return unpack(">B", bytes((byte ^ 0x80,)))[0] elif byte & 0x40: - return unpack('>H', bytes((byte ^ 0x40,)) + _read(stream, 1))[0] + return unpack(">H", bytes((byte ^ 0x40,)) + _read(stream, 1))[0] elif byte & 0x20: - b, h = unpack('>BH', bytes((byte ^ 0x20,)) + _read(stream, 2)) - return b * 2 ** 16 + h + b, h = unpack(">BH", bytes((byte ^ 0x20,)) + _read(stream, 2)) + return b * 2**16 + h elif byte & 0x10: - return unpack('>L', bytes((byte ^ 0x10,)) + _read(stream, 3))[0] + return unpack(">L", bytes((byte ^ 0x10,)) + _read(stream, 3))[0] elif byte & 0x08: - b, l = unpack('>BL', bytes((byte ^ 0x08,)) + _read(stream, 4)) - return b * 2 ** 32 + l + b, l = unpack(">BL", bytes((byte ^ 0x08,)) + _read(stream, 4)) + return b * 2**32 + l elif byte & 0x04: - h, l = unpack('>HL', bytes((byte ^ 0x04,)) + _read(stream, 5)) - return h * 2 ** 32 + l + h, l = unpack(">HL", bytes((byte ^ 0x04,)) + _read(stream, 5)) + return h * 2**32 + l elif byte & 0x02: - b, h, l = unpack('>BHL', bytes((byte ^ 0x02,)) + _read(stream, 6)) - return b * 2 ** 48 + h * 2 ** 32 + l + b, h, l = unpack(">BHL", bytes((byte ^ 0x02,)) + _read(stream, 6)) + return b * 2**48 + h * 2**32 + l elif byte & 0x01: - return unpack('>Q', bytes((byte ^ 0x01,)) + _read(stream, 7))[0] + return unpack(">Q", bytes((byte ^ 0x01,)) + _read(stream, 7))[0] else: - ValueError('Not an Element Size') + ValueError("Not an Element Size") def read_element_integer(stream, size): @@ -98,25 +105,25 @@ def read_element_integer(stream, size): """ if size == 1: - return unpack('>b', _read(stream, 1))[0] + return unpack(">b", _read(stream, 1))[0] elif size == 2: - return unpack('>h', _read(stream, 2))[0] + return unpack(">h", _read(stream, 2))[0] elif size == 3: - b, h = unpack('>bH', _read(stream, 3)) - return b * 2 ** 16 + h + b, h = unpack(">bH", _read(stream, 3)) + return b * 2**16 + h elif size == 4: - return unpack('>l', _read(stream, 4))[0] + return unpack(">l", _read(stream, 4))[0] elif size == 5: - b, l = unpack('>bL', _read(stream, 5)) - return b * 2 ** 32 + l + b, l = unpack(">bL", _read(stream, 5)) + return b * 2**32 + l elif size == 6: - h, l = unpack('>hL', _read(stream, 6)) - return h * 2 ** 32 + l + h, l = unpack(">hL", _read(stream, 6)) + return h * 2**32 + l elif size == 7: - b, h, l = unpack('>bHL', _read(stream, 7)) - return b * 2 ** 48 + h * 2 ** 32 + l + b, h, l = unpack(">bHL", _read(stream, 7)) + return b * 2**48 + h * 2**32 + l elif size == 8: - return unpack('>q', _read(stream, 8))[0] + return unpack(">q", _read(stream, 8))[0] else: raise SizeError(size) @@ -133,25 +140,25 @@ def read_element_uinteger(stream, size): """ if size == 1: - return unpack('>B', _read(stream, 1))[0] + return unpack(">B", _read(stream, 1))[0] elif size == 2: - return unpack('>H', _read(stream, 2))[0] + return unpack(">H", _read(stream, 2))[0] elif size == 3: - b, h = unpack('>BH', _read(stream, 3)) - return b * 2 ** 16 + h + b, h = unpack(">BH", _read(stream, 3)) + return b * 2**16 + h elif size == 4: - return unpack('>L', _read(stream, 4))[0] + return unpack(">L", _read(stream, 4))[0] elif size == 5: - b, l = unpack('>BL', _read(stream, 5)) - return b * 2 ** 32 + l + b, l = unpack(">BL", _read(stream, 5)) + return b * 2**32 + l elif size == 6: - h, l = unpack('>HL', _read(stream, 6)) - return h * 2 ** 32 + l + h, l = unpack(">HL", _read(stream, 6)) + return h * 2**32 + l elif size == 7: - b, h, l = unpack('>BHL', _read(stream, 7)) - return b * 2 ** 48 + h * 2 ** 32 + l + b, h, l = unpack(">BHL", _read(stream, 7)) + return b * 2**48 + h * 2**32 + l elif size == 8: - return unpack('>Q', _read(stream, 8))[0] + return unpack(">Q", _read(stream, 8))[0] else: raise SizeError(size) @@ -168,9 +175,9 @@ def read_element_float(stream, size): """ if size == 4: - return unpack('>f', _read(stream, 4))[0] + return unpack(">f", _read(stream, 4))[0] elif size == 8: - return unpack('>d', _read(stream, 8))[0] + return unpack(">d", _read(stream, 8))[0] else: raise SizeError(size) @@ -186,7 +193,7 @@ def read_element_string(stream, size): :rtype: unicode """ - return _read(stream, size).decode('ascii') + return _read(stream, size).decode("ascii") def read_element_unicode(stream, size): @@ -200,7 +207,7 @@ def read_element_unicode(stream, size): :rtype: unicode """ - return _read(stream, size).decode('utf-8') + return _read(stream, size).decode("utf-8") def read_element_date(stream, size): @@ -216,7 +223,7 @@ def read_element_date(stream, size): """ if size != 8: raise SizeError(size) - nanoseconds = unpack('>q', _read(stream, 8))[0] + nanoseconds = unpack(">q", _read(stream, 8))[0] return datetime(2001, 1, 1, 0, 0, 0, 0, None) + timedelta(microseconds=nanoseconds // 1000) diff --git a/enzyme/tests/__init__.py b/enzyme/tests/__init__.py deleted file mode 100644 index 426d359..0000000 --- a/enzyme/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from . import test_mkv, test_parsers -import unittest - - -suite = unittest.TestSuite([test_mkv.suite(), test_parsers.suite()]) - - -if __name__ == '__main__': - unittest.TextTestRunner().run(suite) diff --git a/enzyme/tests/test_mkv.py b/enzyme/tests/test_mkv.py deleted file mode 100644 index 2403661..0000000 --- a/enzyme/tests/test_mkv.py +++ /dev/null @@ -1,607 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import timedelta, datetime -from enzyme.mkv import MKV, VIDEO_TRACK, AUDIO_TRACK, SUBTITLE_TRACK -import io -import os.path -import requests -import unittest -import zipfile - - -# Test directory -TEST_DIR = os.path.join(os.path.dirname(__file__), os.path.splitext(__file__)[0]) - - -def setUpModule(): - if not os.path.exists(TEST_DIR): - r = requests.get('http://downloads.sourceforge.net/project/matroska/test_files/matroska_test_w1_1.zip') - with zipfile.ZipFile(io.BytesIO(r.content), 'r') as f: - f.extractall(TEST_DIR) - - -class MKVTestCase(unittest.TestCase): - def test_test1(self): - with io.open(os.path.join(TEST_DIR, 'test1.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(minutes=1, seconds=27, milliseconds=336)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 7, 23, 3)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.10.0 + libmatroska2 v0.10.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.5.5 ru from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 (\'Bouncin\' Back\') built on Jul 3 2010 22:54:08') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == True) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MS/VFW/FOURCC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 854) - self.assertTrue(mkv.video_tracks[0].height == 480) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width is None) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == True) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_MPEG/L3') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Big Buck Bunny - test 1') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test2(self): - with io.open(os.path.join(TEST_DIR, 'test2.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=47, milliseconds=509)) - self.assertTrue(mkv.info.date_utc == datetime(2011, 6, 2, 12, 45, 20)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.21.0 + libmatroska2 v0.22.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.8.3 ru from libebml2 v0.10.0 + libmatroska2 v0.10.1 + mkclean 0.5.5 ru from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 (\'Bouncin\' Back\') built on Jul 3 2010 22:54:08') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == True) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MPEG4/ISO/AVC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 1024) - self.assertTrue(mkv.video_tracks[0].height == 576) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width == 1354) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == True) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_AAC') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Elephant Dream - test 2') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test3(self): - with io.open(os.path.join(TEST_DIR, 'test3.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=49, milliseconds=64)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 21, 43, 25)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.11.0 + libmatroska2 v0.10.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.5.5 ro from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 (\'Bouncin\' Back\') built on Jul 3 2010 22:54:08') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == True) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MPEG4/ISO/AVC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 1024) - self.assertTrue(mkv.video_tracks[0].height == 576) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width is None) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language is None) - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == True) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_MPEG/L3') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Elephant Dream - test 3') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 3, header stripping on the video track and no SimpleBlock') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test5(self): - with io.open(os.path.join(TEST_DIR, 'test5.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=46, milliseconds=665)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 18, 6, 43)) - self.assertTrue(mkv.info.muxing_app == 'libebml v1.0.0 + libmatroska v1.0.0') - self.assertTrue(mkv.info.writing_app == 'mkvmerge v4.0.0 (\'The Stars were mine\') built on Jun 6 2010 16:18:42') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == True) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MPEG4/ISO/AVC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 1024) - self.assertTrue(mkv.video_tracks[0].height == 576) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width == 1024) - self.assertTrue(mkv.video_tracks[0].display_height == 576) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio tracks - self.assertTrue(len(mkv.audio_tracks) == 2) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == True) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_AAC') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - self.assertTrue(mkv.audio_tracks[1].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[1].number == 10) - self.assertTrue(mkv.audio_tracks[1].name == 'Commentary') - self.assertTrue(mkv.audio_tracks[1].language is None) - self.assertTrue(mkv.audio_tracks[1].enabled == True) - self.assertTrue(mkv.audio_tracks[1].default == False) - self.assertTrue(mkv.audio_tracks[1].forced == False) - self.assertTrue(mkv.audio_tracks[1].lacing == True) - self.assertTrue(mkv.audio_tracks[1].codec_id == 'A_AAC') - self.assertTrue(mkv.audio_tracks[1].codec_name is None) - self.assertTrue(mkv.audio_tracks[1].sampling_frequency == 22050.0) - self.assertTrue(mkv.audio_tracks[1].channels == 1) - self.assertTrue(mkv.audio_tracks[1].output_sampling_frequency == 44100.0) - self.assertTrue(mkv.audio_tracks[1].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 8) - self.assertTrue(mkv.subtitle_tracks[0].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[0].number == 3) - self.assertTrue(mkv.subtitle_tracks[0].name is None) - self.assertTrue(mkv.subtitle_tracks[0].language is None) - self.assertTrue(mkv.subtitle_tracks[0].enabled == True) - self.assertTrue(mkv.subtitle_tracks[0].default == True) - self.assertTrue(mkv.subtitle_tracks[0].forced == False) - self.assertTrue(mkv.subtitle_tracks[0].lacing == False) - self.assertTrue(mkv.subtitle_tracks[0].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[0].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[1].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[1].number == 4) - self.assertTrue(mkv.subtitle_tracks[1].name is None) - self.assertTrue(mkv.subtitle_tracks[1].language == 'hun') - self.assertTrue(mkv.subtitle_tracks[1].enabled == True) - self.assertTrue(mkv.subtitle_tracks[1].default == False) - self.assertTrue(mkv.subtitle_tracks[1].forced == False) - self.assertTrue(mkv.subtitle_tracks[1].lacing == False) - self.assertTrue(mkv.subtitle_tracks[1].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[1].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[2].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[2].number == 5) - self.assertTrue(mkv.subtitle_tracks[2].name is None) - self.assertTrue(mkv.subtitle_tracks[2].language == 'ger') - self.assertTrue(mkv.subtitle_tracks[2].enabled == True) - self.assertTrue(mkv.subtitle_tracks[2].default == False) - self.assertTrue(mkv.subtitle_tracks[2].forced == False) - self.assertTrue(mkv.subtitle_tracks[2].lacing == False) - self.assertTrue(mkv.subtitle_tracks[2].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[2].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[3].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[3].number == 6) - self.assertTrue(mkv.subtitle_tracks[3].name is None) - self.assertTrue(mkv.subtitle_tracks[3].language == 'fre') - self.assertTrue(mkv.subtitle_tracks[3].enabled == True) - self.assertTrue(mkv.subtitle_tracks[3].default == False) - self.assertTrue(mkv.subtitle_tracks[3].forced == False) - self.assertTrue(mkv.subtitle_tracks[3].lacing == False) - self.assertTrue(mkv.subtitle_tracks[3].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[3].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[4].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[4].number == 8) - self.assertTrue(mkv.subtitle_tracks[4].name is None) - self.assertTrue(mkv.subtitle_tracks[4].language == 'spa') - self.assertTrue(mkv.subtitle_tracks[4].enabled == True) - self.assertTrue(mkv.subtitle_tracks[4].default == False) - self.assertTrue(mkv.subtitle_tracks[4].forced == False) - self.assertTrue(mkv.subtitle_tracks[4].lacing == False) - self.assertTrue(mkv.subtitle_tracks[4].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[4].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[5].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[5].number == 9) - self.assertTrue(mkv.subtitle_tracks[5].name is None) - self.assertTrue(mkv.subtitle_tracks[5].language == 'ita') - self.assertTrue(mkv.subtitle_tracks[5].enabled == True) - self.assertTrue(mkv.subtitle_tracks[5].default == False) - self.assertTrue(mkv.subtitle_tracks[5].forced == False) - self.assertTrue(mkv.subtitle_tracks[5].lacing == False) - self.assertTrue(mkv.subtitle_tracks[5].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[5].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[6].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[6].number == 11) - self.assertTrue(mkv.subtitle_tracks[6].name is None) - self.assertTrue(mkv.subtitle_tracks[6].language == 'jpn') - self.assertTrue(mkv.subtitle_tracks[6].enabled == True) - self.assertTrue(mkv.subtitle_tracks[6].default == False) - self.assertTrue(mkv.subtitle_tracks[6].forced == False) - self.assertTrue(mkv.subtitle_tracks[6].lacing == False) - self.assertTrue(mkv.subtitle_tracks[6].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[6].codec_name is None) - self.assertTrue(mkv.subtitle_tracks[7].type == SUBTITLE_TRACK) - self.assertTrue(mkv.subtitle_tracks[7].number == 7) - self.assertTrue(mkv.subtitle_tracks[7].name is None) - self.assertTrue(mkv.subtitle_tracks[7].language == 'und') - self.assertTrue(mkv.subtitle_tracks[7].enabled == True) - self.assertTrue(mkv.subtitle_tracks[7].default == False) - self.assertTrue(mkv.subtitle_tracks[7].forced == False) - self.assertTrue(mkv.subtitle_tracks[7].lacing == False) - self.assertTrue(mkv.subtitle_tracks[7].codec_id == 'S_TEXT/UTF8') - self.assertTrue(mkv.subtitle_tracks[7].codec_name is None) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Big Buck Bunny - test 8') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 8, secondary audio commentary track, misc subtitle tracks') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test6(self): - with io.open(os.path.join(TEST_DIR, 'test6.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=87, milliseconds=336)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 16, 31, 55)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.10.1 + libmatroska2 v0.10.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 (\'The Stars were mine\') built on Jun 6 2010 16:18:42') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == False) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MS/VFW/FOURCC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 854) - self.assertTrue(mkv.video_tracks[0].height == 480) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width is None) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == False) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_MPEG/L3') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Big Buck Bunny - test 6') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 6, random length to code the size of Clusters and Blocks, no Cues for seeking') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test7(self): - with io.open(os.path.join(TEST_DIR, 'test7.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=37, milliseconds=43)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 17, 0, 23)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.10.1 + libmatroska2 v0.10.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 (\'The Stars were mine\') built on Jun 6 2010 16:18:42') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == False) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MPEG4/ISO/AVC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 1024) - self.assertTrue(mkv.video_tracks[0].height == 576) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width is None) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == False) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_AAC') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Big Buck Bunny - test 7') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 7, junk elements are present at the beggining or end of clusters, the parser should skip it. There is also a damaged element at 451418') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - def test_test8(self): - with io.open(os.path.join(TEST_DIR, 'test8.mkv'), 'rb') as stream: - mkv = MKV(stream) - # info - self.assertTrue(mkv.info.title is None) - self.assertTrue(mkv.info.duration == timedelta(seconds=47, milliseconds=341)) - self.assertTrue(mkv.info.date_utc == datetime(2010, 8, 21, 17, 22, 14)) - self.assertTrue(mkv.info.muxing_app == 'libebml2 v0.10.1 + libmatroska2 v0.10.1') - self.assertTrue(mkv.info.writing_app == 'mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 (\'The Stars were mine\') built on Jun 6 2010 16:18:42') - # video track - self.assertTrue(len(mkv.video_tracks) == 1) - self.assertTrue(mkv.video_tracks[0].type == VIDEO_TRACK) - self.assertTrue(mkv.video_tracks[0].number == 1) - self.assertTrue(mkv.video_tracks[0].name is None) - self.assertTrue(mkv.video_tracks[0].language == 'und') - self.assertTrue(mkv.video_tracks[0].enabled == True) - self.assertTrue(mkv.video_tracks[0].default == False) - self.assertTrue(mkv.video_tracks[0].forced == False) - self.assertTrue(mkv.video_tracks[0].lacing == False) - self.assertTrue(mkv.video_tracks[0].codec_id == 'V_MPEG4/ISO/AVC') - self.assertTrue(mkv.video_tracks[0].codec_name is None) - self.assertTrue(mkv.video_tracks[0].width == 1024) - self.assertTrue(mkv.video_tracks[0].height == 576) - self.assertTrue(mkv.video_tracks[0].interlaced == False) - self.assertTrue(mkv.video_tracks[0].stereo_mode is None) - self.assertTrue(mkv.video_tracks[0].crop == {}) - self.assertTrue(mkv.video_tracks[0].display_width is None) - self.assertTrue(mkv.video_tracks[0].display_height is None) - self.assertTrue(mkv.video_tracks[0].display_unit is None) - self.assertTrue(mkv.video_tracks[0].aspect_ratio_type is None) - # audio track - self.assertTrue(len(mkv.audio_tracks) == 1) - self.assertTrue(mkv.audio_tracks[0].type == AUDIO_TRACK) - self.assertTrue(mkv.audio_tracks[0].number == 2) - self.assertTrue(mkv.audio_tracks[0].name is None) - self.assertTrue(mkv.audio_tracks[0].language == 'und') - self.assertTrue(mkv.audio_tracks[0].enabled == True) - self.assertTrue(mkv.audio_tracks[0].default == False) - self.assertTrue(mkv.audio_tracks[0].forced == False) - self.assertTrue(mkv.audio_tracks[0].lacing == True) - self.assertTrue(mkv.audio_tracks[0].codec_id == 'A_AAC') - self.assertTrue(mkv.audio_tracks[0].codec_name is None) - self.assertTrue(mkv.audio_tracks[0].sampling_frequency == 48000.0) - self.assertTrue(mkv.audio_tracks[0].channels == 2) - self.assertTrue(mkv.audio_tracks[0].output_sampling_frequency is None) - self.assertTrue(mkv.audio_tracks[0].bit_depth is None) - # subtitle track - self.assertTrue(len(mkv.subtitle_tracks) == 0) - # chapters - self.assertTrue(len(mkv.chapters) == 0) - # tags - self.assertTrue(len(mkv.tags) == 1) - self.assertTrue(len(mkv.tags[0].simpletags) == 3) - self.assertTrue(mkv.tags[0].simpletags[0].name == 'TITLE') - self.assertTrue(mkv.tags[0].simpletags[0].default == True) - self.assertTrue(mkv.tags[0].simpletags[0].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[0].string == 'Big Buck Bunny - test 8') - self.assertTrue(mkv.tags[0].simpletags[0].binary is None) - self.assertTrue(mkv.tags[0].simpletags[1].name == 'DATE_RELEASED') - self.assertTrue(mkv.tags[0].simpletags[1].default == True) - self.assertTrue(mkv.tags[0].simpletags[1].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[1].string == '2010') - self.assertTrue(mkv.tags[0].simpletags[1].binary is None) - self.assertTrue(mkv.tags[0].simpletags[2].name == 'COMMENT') - self.assertTrue(mkv.tags[0].simpletags[2].default == True) - self.assertTrue(mkv.tags[0].simpletags[2].language == 'und') - self.assertTrue(mkv.tags[0].simpletags[2].string == 'Matroska Validation File 8, audio missing between timecodes 6.019s and 6.360s') - self.assertTrue(mkv.tags[0].simpletags[2].binary is None) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.TestLoader().loadTestsFromTestCase(MKVTestCase)) - return suite - -if __name__ == '__main__': - unittest.TextTestRunner().run(suite()) diff --git a/pyproject.toml b/pyproject.toml index c4d8479..0ddd14a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,189 @@ +# https://peps.python.org/pep-0517/ [build-system] -requires = ["setuptools"] +requires = ["setuptools>=64"] build-backend = "setuptools.build_meta" +# https://peps.python.org/pep-0621/ [project] name = "enzyme" -description = "Python video metadata parser" +description = "Video metadata parser" requires-python = ">=3.8" -license = { file = "LICENSE" } -keywords = ["parser", "video", "metadata", "mkv"] -authors = [{ name = "Antoine Bertin", email = "diaoulael@gmail.com" }] +license = { text = "MIT" } +authors = [{ name = "Antoine Bertin", email = "ant.bertin@gmail.com" }] +maintainers = [{ name = "Antoine Bertin", email = "ant.bertin@gmail.com" }] +keywords = ["video", "metadata", "parser", "mkv", "matroska", "ebml"] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "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", - "Topic :: Multimedia :: Video", - "Topic :: Software Development :: Libraries :: Python Modules", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Multimedia :: Video", ] dynamic = ["version", "readme"] -dependencies = [ - "importlib_resources>=4.6; python_version=='3.8'" -] +dependencies = ["importlib_resources>=4.6; python_version=='3.8'"] +# extras +# https://peps.python.org/pep-0621/#dependencies-optional-dependencies [project.optional-dependencies] -docs = ["sphinx"] -test = ["PyYAML", "requests"] -dev = ["tox"] +docs = ["sphinx", "sphinx-rtd-theme"] +test = [ + "PyYAML", + "requests", + "mypy", + "pytest>=6.0", + "importlib_metadata>=4.6; python_version<'3.10'", +] +dev = ["doc8", "mypy", "ruff", "typos", "validate-pyproject", "tox"] [project.urls] homepage = "https://github.com/Diaoul/enzyme" repository = "https://github.com/Diaoul/enzyme" -documentation = "https://enzyme.readthedocs.io" +documentation = "https://enzyme.readthedocs.org" + +[tool.setuptools] +py-modules = ["enzyme"] +include-package-data = true + +[tool.setuptools.package-data] +enzyme = ["py.typed", "parsers/ebml/specs/matroska.xml"] + +[tool.setuptools.packages.find] +namespaces = false +where = ["."] [tool.setuptools.dynamic] version = { attr = "enzyme.__version__" } -readme = { file = ["README.rst", "HISTORY.rst"] } +readme = { file = ["README.md"] } + + +# https://docs.astral.sh/ruff/ +[tool.ruff] +line-length = 120 +src = ["subliminal", "tests"] +exclude = ["_version.py"] + +[tool.ruff.lint] +pydocstyle = { convention = "pep257" } +select = [ + "E", # style errors + "F", # flakes + "W", # warnings + "D", # pydocstyle + "D417", # Missing argument descriptions in Docstrings + "I", # isort + "UP", # pyupgrade + "S", # bandit + "C4", # flake8-comprehensions + "B", # flake8-bugbear + "TCH", # flake8-typecheck + "TID", # flake8-tidy-imports + "RUF", # ruff-specific rules + "ISC", # flake8-implicit-str-concat + "PT", # flake8-pytest-style + "FA", # flake8-future-annotations + "BLE", # flake8-blind-except + "RET", # flake8-return + "SIM", # flake8-simplify + "DTZ", # flake8-datetimez + "A", # flake8-builtins + "FBT", # flake8-boolean-trap + "ANN0", # flake8-annotations + "ANN2", + "ASYNC", # flake8-async + "TRY", # tryceratops +] +ignore = [ + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "D401", # First line should be in imperative mood +] + +[tool.ruff.lint.per-file-ignores] +"docs/conf*.py" = ["ALL"] +"subliminal/__init__.py" = ["E402"] +"tests/*.py" = ["D", "S", "RUF012", "ANN", "FBT"] + +# https://docs.astral.sh/ruff/formatter/ +[tool.ruff.format] +docstring-code-format = true + +# https://docs.pytest.org/en/6.2.x/customize.html +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = ["tests"] +addopts = "--doctest-glob='*.rst'" +# markers = ["integration", "converter"] +doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] + +# https://coverage.readthedocs.io/en/latest/config.html +[tool.coverage.report] +exclude_also = [ + "pragma: no.cover", + "if TYPE_CHECKING:", + "@overload", + "except ImportError", + "\\.\\.\\.", + "raise NotImplementedError()", + "if __name__ == .__main__.:", +] +show_missing = true +skip_covered = true +fail_under = 80 + +[tool.coverage.run] +source = ["enzyme"] +relative_files = true + + +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +files = "enzyme/**/*.py" +exclude = ['build', 'dist', 'docs'] +# global-only flags +pretty = true +show_error_codes = true +namespace_packages = false +warn_redundant_casts = true +# global per-module flags +check_untyped_defs = true +strict_equality = true +disallow_any_generics = false +disallow_subclassing_any = false + +[[tool.mypy.overrides]] +module = ["enzyme.*"] +warn_return_any = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_untyped_decorators = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +disallow_untyped_defs = false +disallow_untyped_calls = false +warn_return_any = false +disable_error_code = ["var-annotated"] + + +# https://github.com/PyCQA/doc8 +[tool.doc8] +allow-long-titles = true +max-line-length = 120 + +# https://github.com/crate-ci/typos/blob/master/docs/reference.md +[tool.typos.files] +extend-exclude = ["tests/"] + +[tool.typos.default] +extend-ignore-re = [ + "(?Rm)^.*#\\s*spellchecker:\\s*disable-line$", + "#\\s*spellchecker:off\\s*\\n.*\\n\\s*#\\s*spellchecker:on", +] diff --git a/enzyme/tests/parsers/ebml/test1.mkv.yml b/tests/parsers/ebml/test1.mkv.yml similarity index 100% rename from enzyme/tests/parsers/ebml/test1.mkv.yml rename to tests/parsers/ebml/test1.mkv.yml diff --git a/tests/test_mkv.py b/tests/test_mkv.py new file mode 100644 index 0000000..c07420e --- /dev/null +++ b/tests/test_mkv.py @@ -0,0 +1,646 @@ +from datetime import timedelta, datetime +from enzyme.mkv import MKV, VIDEO_TRACK, AUDIO_TRACK, SUBTITLE_TRACK +import io +import os.path +import requests +import unittest +import zipfile +import pytest + + +DATA_DIR = os.path.join(os.path.dirname(__file__), "data") + + +@pytest.fixture(scope="session") +def data_files(): + files = [] + for i in range(1, 9): + files.append(f"test{i}.mkv") + files.append(f"test{i}-tag.xml") + missing_files = [file for file in files if not os.path.exists(os.path.join(DATA_DIR, file))] + if not missing_files: + return + r = requests.get("http://downloads.sourceforge.net/project/matroska/test_files/matroska_test_w1_1.zip") + with zipfile.ZipFile(io.BytesIO(r.content), "r") as f: + for missing_file in missing_files: + f.extract(missing_file, DATA_DIR) + + +def test_test1(data_files): + with io.open(os.path.join(DATA_DIR, "test1.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(minutes=1, seconds=27, milliseconds=336) + assert mkv.info.date_utc == datetime(2010, 8, 21, 7, 23, 3) + assert mkv.info.muxing_app == "libebml2 v0.10.0 + libmatroska2 v0.10.1" + assert ( + mkv.info.writing_app + == "mkclean 0.5.5 ru from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 ('Bouncin' Back') built on Jul 3 2010 22:54:08" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == True + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MS/VFW/FOURCC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 854 + assert mkv.video_tracks[0].height == 480 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width is None + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == True + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_MPEG/L3" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Big Buck Bunny - test 1" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert mkv.tags[0].simpletags[2].string == "Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock" + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test2(data_files): + with io.open(os.path.join(DATA_DIR, "test2.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=47, milliseconds=509) + assert mkv.info.date_utc == datetime(2011, 6, 2, 12, 45, 20) + assert mkv.info.muxing_app == "libebml2 v0.21.0 + libmatroska2 v0.22.1" + assert ( + mkv.info.writing_app + == "mkclean 0.8.3 ru from libebml2 v0.10.0 + libmatroska2 v0.10.1 + mkclean 0.5.5 ru from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 ('Bouncin' Back') built on Jul 3 2010 22:54:08" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == True + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MPEG4/ISO/AVC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 1024 + assert mkv.video_tracks[0].height == 576 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width == 1354 + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == True + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_AAC" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Elephant Dream - test 2" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC" + ) + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test3(data_files): + with io.open(os.path.join(DATA_DIR, "test3.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=49, milliseconds=64) + assert mkv.info.date_utc == datetime(2010, 8, 21, 21, 43, 25) + assert mkv.info.muxing_app == "libebml2 v0.11.0 + libmatroska2 v0.10.1" + assert ( + mkv.info.writing_app + == "mkclean 0.5.5 ro from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.1.1 ('Bouncin' Back') built on Jul 3 2010 22:54:08" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == True + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MPEG4/ISO/AVC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 1024 + assert mkv.video_tracks[0].height == 576 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width is None + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language is None + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == True + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_MPEG/L3" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Elephant Dream - test 3" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 3, header stripping on the video track and no SimpleBlock" + ) + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test5(data_files): + with io.open(os.path.join(DATA_DIR, "test5.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=46, milliseconds=665) + assert mkv.info.date_utc == datetime(2010, 8, 21, 18, 6, 43) + assert mkv.info.muxing_app == "libebml v1.0.0 + libmatroska v1.0.0" + assert mkv.info.writing_app == "mkvmerge v4.0.0 ('The Stars were mine') built on Jun 6 2010 16:18:42" + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == True + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MPEG4/ISO/AVC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 1024 + assert mkv.video_tracks[0].height == 576 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width == 1024 + assert mkv.video_tracks[0].display_height == 576 + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio tracks + assert len(mkv.audio_tracks) == 2 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == True + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_AAC" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + assert mkv.audio_tracks[1].type == AUDIO_TRACK + assert mkv.audio_tracks[1].number == 10 + assert mkv.audio_tracks[1].name == "Commentary" + assert mkv.audio_tracks[1].language is None + assert mkv.audio_tracks[1].enabled == True + assert mkv.audio_tracks[1].default == False + assert mkv.audio_tracks[1].forced == False + assert mkv.audio_tracks[1].lacing == True + assert mkv.audio_tracks[1].codec_id == "A_AAC" + assert mkv.audio_tracks[1].codec_name is None + assert mkv.audio_tracks[1].sampling_frequency == 22050.0 + assert mkv.audio_tracks[1].channels == 1 + assert mkv.audio_tracks[1].output_sampling_frequency == 44100.0 + assert mkv.audio_tracks[1].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 8 + assert mkv.subtitle_tracks[0].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[0].number == 3 + assert mkv.subtitle_tracks[0].name is None + assert mkv.subtitle_tracks[0].language is None + assert mkv.subtitle_tracks[0].enabled == True + assert mkv.subtitle_tracks[0].default == True + assert mkv.subtitle_tracks[0].forced == False + assert mkv.subtitle_tracks[0].lacing == False + assert mkv.subtitle_tracks[0].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[0].codec_name is None + assert mkv.subtitle_tracks[1].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[1].number == 4 + assert mkv.subtitle_tracks[1].name is None + assert mkv.subtitle_tracks[1].language == "hun" + assert mkv.subtitle_tracks[1].enabled == True + assert mkv.subtitle_tracks[1].default == False + assert mkv.subtitle_tracks[1].forced == False + assert mkv.subtitle_tracks[1].lacing == False + assert mkv.subtitle_tracks[1].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[1].codec_name is None + assert mkv.subtitle_tracks[2].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[2].number == 5 + assert mkv.subtitle_tracks[2].name is None + assert mkv.subtitle_tracks[2].language == "ger" + assert mkv.subtitle_tracks[2].enabled == True + assert mkv.subtitle_tracks[2].default == False + assert mkv.subtitle_tracks[2].forced == False + assert mkv.subtitle_tracks[2].lacing == False + assert mkv.subtitle_tracks[2].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[2].codec_name is None + assert mkv.subtitle_tracks[3].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[3].number == 6 + assert mkv.subtitle_tracks[3].name is None + assert mkv.subtitle_tracks[3].language == "fre" + assert mkv.subtitle_tracks[3].enabled == True + assert mkv.subtitle_tracks[3].default == False + assert mkv.subtitle_tracks[3].forced == False + assert mkv.subtitle_tracks[3].lacing == False + assert mkv.subtitle_tracks[3].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[3].codec_name is None + assert mkv.subtitle_tracks[4].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[4].number == 8 + assert mkv.subtitle_tracks[4].name is None + assert mkv.subtitle_tracks[4].language == "spa" + assert mkv.subtitle_tracks[4].enabled == True + assert mkv.subtitle_tracks[4].default == False + assert mkv.subtitle_tracks[4].forced == False + assert mkv.subtitle_tracks[4].lacing == False + assert mkv.subtitle_tracks[4].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[4].codec_name is None + assert mkv.subtitle_tracks[5].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[5].number == 9 + assert mkv.subtitle_tracks[5].name is None + assert mkv.subtitle_tracks[5].language == "ita" + assert mkv.subtitle_tracks[5].enabled == True + assert mkv.subtitle_tracks[5].default == False + assert mkv.subtitle_tracks[5].forced == False + assert mkv.subtitle_tracks[5].lacing == False + assert mkv.subtitle_tracks[5].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[5].codec_name is None + assert mkv.subtitle_tracks[6].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[6].number == 11 + assert mkv.subtitle_tracks[6].name is None + assert mkv.subtitle_tracks[6].language == "jpn" + assert mkv.subtitle_tracks[6].enabled == True + assert mkv.subtitle_tracks[6].default == False + assert mkv.subtitle_tracks[6].forced == False + assert mkv.subtitle_tracks[6].lacing == False + assert mkv.subtitle_tracks[6].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[6].codec_name is None + assert mkv.subtitle_tracks[7].type == SUBTITLE_TRACK + assert mkv.subtitle_tracks[7].number == 7 + assert mkv.subtitle_tracks[7].name is None + assert mkv.subtitle_tracks[7].language == "und" + assert mkv.subtitle_tracks[7].enabled == True + assert mkv.subtitle_tracks[7].default == False + assert mkv.subtitle_tracks[7].forced == False + assert mkv.subtitle_tracks[7].lacing == False + assert mkv.subtitle_tracks[7].codec_id == "S_TEXT/UTF8" + assert mkv.subtitle_tracks[7].codec_name is None + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Big Buck Bunny - test 8" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 8, secondary audio commentary track, misc subtitle tracks" + ) + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test6(data_files): + with io.open(os.path.join(DATA_DIR, "test6.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=87, milliseconds=336) + assert mkv.info.date_utc == datetime(2010, 8, 21, 16, 31, 55) + assert mkv.info.muxing_app == "libebml2 v0.10.1 + libmatroska2 v0.10.1" + assert ( + mkv.info.writing_app + == "mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 ('The Stars were mine') built on Jun 6 2010 16:18:42" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == False + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MS/VFW/FOURCC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 854 + assert mkv.video_tracks[0].height == 480 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width is None + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == False + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_MPEG/L3" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Big Buck Bunny - test 6" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 6, random length to code the size of Clusters and Blocks, no Cues for seeking" + ) + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test7(data_files): + with io.open(os.path.join(DATA_DIR, "test7.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=37, milliseconds=43) + assert mkv.info.date_utc == datetime(2010, 8, 21, 17, 0, 23) + assert mkv.info.muxing_app == "libebml2 v0.10.1 + libmatroska2 v0.10.1" + assert ( + mkv.info.writing_app + == "mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 ('The Stars were mine') built on Jun 6 2010 16:18:42" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == False + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MPEG4/ISO/AVC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 1024 + assert mkv.video_tracks[0].height == 576 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width is None + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == False + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_AAC" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Big Buck Bunny - test 7" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 7, junk elements are present at the beggining or end of clusters, the parser should skip it. There is also a damaged element at 451418" + ) + assert mkv.tags[0].simpletags[2].binary is None + + +def test_test8(data_files): + with io.open(os.path.join(DATA_DIR, "test8.mkv"), "rb") as stream: + mkv = MKV(stream) + # info + assert mkv.info.title is None + assert mkv.info.duration == timedelta(seconds=47, milliseconds=341) + assert mkv.info.date_utc == datetime(2010, 8, 21, 17, 22, 14) + assert mkv.info.muxing_app == "libebml2 v0.10.1 + libmatroska2 v0.10.1" + assert ( + mkv.info.writing_app + == "mkclean 0.5.5 r from libebml v1.0.0 + libmatroska v1.0.0 + mkvmerge v4.0.0 ('The Stars were mine') built on Jun 6 2010 16:18:42" + ) + # video track + assert len(mkv.video_tracks) == 1 + assert mkv.video_tracks[0].type == VIDEO_TRACK + assert mkv.video_tracks[0].number == 1 + assert mkv.video_tracks[0].name is None + assert mkv.video_tracks[0].language == "und" + assert mkv.video_tracks[0].enabled == True + assert mkv.video_tracks[0].default == False + assert mkv.video_tracks[0].forced == False + assert mkv.video_tracks[0].lacing == False + assert mkv.video_tracks[0].codec_id == "V_MPEG4/ISO/AVC" + assert mkv.video_tracks[0].codec_name is None + assert mkv.video_tracks[0].width == 1024 + assert mkv.video_tracks[0].height == 576 + assert mkv.video_tracks[0].interlaced == False + assert mkv.video_tracks[0].stereo_mode is None + assert mkv.video_tracks[0].crop == {} + assert mkv.video_tracks[0].display_width is None + assert mkv.video_tracks[0].display_height is None + assert mkv.video_tracks[0].display_unit is None + assert mkv.video_tracks[0].aspect_ratio_type is None + # audio track + assert len(mkv.audio_tracks) == 1 + assert mkv.audio_tracks[0].type == AUDIO_TRACK + assert mkv.audio_tracks[0].number == 2 + assert mkv.audio_tracks[0].name is None + assert mkv.audio_tracks[0].language == "und" + assert mkv.audio_tracks[0].enabled == True + assert mkv.audio_tracks[0].default == False + assert mkv.audio_tracks[0].forced == False + assert mkv.audio_tracks[0].lacing == True + assert mkv.audio_tracks[0].codec_id == "A_AAC" + assert mkv.audio_tracks[0].codec_name is None + assert mkv.audio_tracks[0].sampling_frequency == 48000.0 + assert mkv.audio_tracks[0].channels == 2 + assert mkv.audio_tracks[0].output_sampling_frequency is None + assert mkv.audio_tracks[0].bit_depth is None + # subtitle track + assert len(mkv.subtitle_tracks) == 0 + # chapters + assert len(mkv.chapters) == 0 + # tags + assert len(mkv.tags) == 1 + assert len(mkv.tags[0].simpletags) == 3 + assert mkv.tags[0].simpletags[0].name == "TITLE" + assert mkv.tags[0].simpletags[0].default == True + assert mkv.tags[0].simpletags[0].language == "und" + assert mkv.tags[0].simpletags[0].string == "Big Buck Bunny - test 8" + assert mkv.tags[0].simpletags[0].binary is None + assert mkv.tags[0].simpletags[1].name == "DATE_RELEASED" + assert mkv.tags[0].simpletags[1].default == True + assert mkv.tags[0].simpletags[1].language == "und" + assert mkv.tags[0].simpletags[1].string == "2010" + assert mkv.tags[0].simpletags[1].binary is None + assert mkv.tags[0].simpletags[2].name == "COMMENT" + assert mkv.tags[0].simpletags[2].default == True + assert mkv.tags[0].simpletags[2].language == "und" + assert ( + mkv.tags[0].simpletags[2].string + == "Matroska Validation File 8, audio missing between timecodes 6.019s and 6.360s" + ) + assert mkv.tags[0].simpletags[2].binary is None diff --git a/enzyme/tests/test_parsers.py b/tests/test_parsers.py similarity index 52% rename from enzyme/tests/test_parsers.py rename to tests/test_parsers.py index 0fa320c..d5da18b 100644 --- a/enzyme/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from enzyme.parsers import ebml import io import os.path @@ -9,31 +8,43 @@ # Test directory -TEST_DIR = os.path.join(os.path.dirname(__file__), os.path.splitext(__file__)[0]) +TEST_DIR = os.path.join(os.path.dirname(__file__), "data") # EBML validation directory -EBML_VALIDATION_DIR = os.path.join(os.path.dirname(__file__), 'parsers', 'ebml') +EBML_VALIDATION_DIR = os.path.join(os.path.dirname(__file__), "parsers", "ebml") def setUpModule(): if not os.path.exists(TEST_DIR): - r = requests.get('http://downloads.sourceforge.net/project/matroska/test_files/matroska_test_w1_1.zip') - with zipfile.ZipFile(io.BytesIO(r.content), 'r') as f: + r = requests.get("http://downloads.sourceforge.net/project/matroska/test_files/matroska_test_w1_1.zip") + with zipfile.ZipFile(io.BytesIO(r.content), "r") as f: f.extractall(TEST_DIR) class EBMLTestCase(unittest.TestCase): def setUp(self): - self.stream = io.open(os.path.join(TEST_DIR, 'test1.mkv'), 'rb') - with io.open(os.path.join(EBML_VALIDATION_DIR, 'test1.mkv.yml'), 'r') as yml: + self.stream = io.open(os.path.join(TEST_DIR, "test1.mkv"), "rb") + with io.open(os.path.join(EBML_VALIDATION_DIR, "test1.mkv.yml"), "r") as yml: self.validation = yaml.safe_load(yml) self.specs = ebml.get_matroska_specs() def tearDown(self): self.stream.close() - def check_element(self, element_id, element_type, element_name, element_level, element_position, element_size, element_data, element, - ignore_element_types=None, ignore_element_names=None, max_level=None): + def check_element( + self, + element_id, + element_type, + element_name, + element_level, + element_position, + element_size, + element_data, + element, + ignore_element_types=None, + ignore_element_names=None, + max_level=None, + ): """Recursively check an element""" # base self.assertTrue(element.id == element_id) @@ -58,16 +69,34 @@ def check_element(self, element_id, element_type, element_name, element_level, e return self.assertTrue(len(element.data) == len(element_data)) for i in range(len(element.data)): - self.check_element(element_data[i][0], element_data[i][1], element_data[i][2], element_data[i][3], - element_data[i][4], element_data[i][5], element_data[i][6], element.data[i], ignore_element_types, - ignore_element_names, max_level) + self.check_element( + element_data[i][0], + element_data[i][1], + element_data[i][2], + element_data[i][3], + element_data[i][4], + element_data[i][5], + element_data[i][6], + element.data[i], + ignore_element_types, + ignore_element_names, + max_level, + ) def test_parse_full(self): result = ebml.parse(self.stream, self.specs) self.assertTrue(len(result) == len(self.validation)) for i in range(len(self.validation)): - self.check_element(self.validation[i][0], self.validation[i][1], self.validation[i][2], self.validation[i][3], - self.validation[i][4], self.validation[i][5], self.validation[i][6], result[i]) + self.check_element( + self.validation[i][0], + self.validation[i][1], + self.validation[i][2], + self.validation[i][3], + self.validation[i][4], + self.validation[i][5], + self.validation[i][6], + result[i], + ) def test_parse_ignore_element_types(self): ignore_element_types = [ebml.INTEGER, ebml.BINARY] @@ -75,17 +104,35 @@ def test_parse_ignore_element_types(self): self.validation = [e for e in self.validation if e[1] not in ignore_element_types] self.assertTrue(len(result) == len(self.validation)) for i in range(len(self.validation)): - self.check_element(self.validation[i][0], self.validation[i][1], self.validation[i][2], self.validation[i][3], - self.validation[i][4], self.validation[i][5], self.validation[i][6], result[i], ignore_element_types=ignore_element_types) + self.check_element( + self.validation[i][0], + self.validation[i][1], + self.validation[i][2], + self.validation[i][3], + self.validation[i][4], + self.validation[i][5], + self.validation[i][6], + result[i], + ignore_element_types=ignore_element_types, + ) def test_parse_ignore_element_names(self): - ignore_element_names = ['EBML', 'SimpleBlock'] + ignore_element_names = ["EBML", "SimpleBlock"] result = ebml.parse(self.stream, self.specs, ignore_element_names=ignore_element_names) self.validation = [e for e in self.validation if e[2] not in ignore_element_names] self.assertTrue(len(result) == len(self.validation)) for i in range(len(self.validation)): - self.check_element(self.validation[i][0], self.validation[i][1], self.validation[i][2], self.validation[i][3], - self.validation[i][4], self.validation[i][5], self.validation[i][6], result[i], ignore_element_names=ignore_element_names) + self.check_element( + self.validation[i][0], + self.validation[i][1], + self.validation[i][2], + self.validation[i][3], + self.validation[i][4], + self.validation[i][5], + self.validation[i][6], + result[i], + ignore_element_names=ignore_element_names, + ) def test_parse_max_level(self): max_level = 3 @@ -93,12 +140,22 @@ def test_parse_max_level(self): self.validation = [e for e in self.validation if e[3] <= max_level] self.assertTrue(len(result) == len(self.validation)) for i in range(len(self.validation)): - self.check_element(self.validation[i][0], self.validation[i][1], self.validation[i][2], self.validation[i][3], - self.validation[i][4], self.validation[i][5], self.validation[i][6], result[i], max_level=max_level) + self.check_element( + self.validation[i][0], + self.validation[i][1], + self.validation[i][2], + self.validation[i][3], + self.validation[i][4], + self.validation[i][5], + self.validation[i][6], + result[i], + max_level=max_level, + ) def generate_yml(filename, specs): """Generate a validation file for the test video""" + def _to_builtin(elements): """Recursively convert elements to built-in types""" result = [] @@ -106,10 +163,21 @@ def _to_builtin(elements): if isinstance(e, ebml.MasterElement): result.append((e.id, e.type, e.name, e.level, e.position, e.size, _to_builtin(e.data))) else: - result.append((e.id, e.type, e.name, e.level, e.position, e.size, None if isinstance(e.data, io.BytesIO) else e.data)) + result.append( + ( + e.id, + e.type, + e.name, + e.level, + e.position, + e.size, + None if isinstance(e.data, io.BytesIO) else e.data, + ) + ) return result - video = io.open(os.path.join(TEST_DIR, filename), 'rb') - yml = io.open(os.path.join(EBML_VALIDATION_DIR, filename + '.yml'), 'w') + + video = io.open(os.path.join(TEST_DIR, filename), "rb") + yml = io.open(os.path.join(EBML_VALIDATION_DIR, filename + ".yml"), "w") yaml.safe_dump(_to_builtin(ebml.parse(video, specs)), yml) @@ -118,5 +186,6 @@ def suite(): suite.addTest(unittest.TestLoader().loadTestsFromTestCase(EBMLTestCase)) return suite -if __name__ == '__main__': + +if __name__ == "__main__": unittest.TextTestRunner().run(suite())