From ff5acdcf796812e3f3f8b78022ed43da41e0e2e8 Mon Sep 17 00:00:00 2001 From: rlaphoenix Date: Sat, 25 Mar 2023 09:14:29 +0000 Subject: [PATCH] Initial commit --- .deepsource.toml | 9 + .flake8 | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 27 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/cd.yml | 44 ++ .github/workflows/ci.yml | 46 ++ .gitignore | 109 ++++ .pre-commit-config.yaml | 18 + CHANGELOG.md | 12 + LICENSE | 674 ++++++++++++++++++++++ README.md | 143 +++++ mpgg/__init__.py | 5 + mpgg/mpgg.py | 419 ++++++++++++++ mpgg/utilities.py | 87 +++ poetry.lock | 427 ++++++++++++++ pyproject.toml | 46 ++ 16 files changed, 2089 insertions(+) create mode 100644 .deepsource.toml create mode 100644 .flake8 create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 mpgg/__init__.py create mode 100644 mpgg/mpgg.py create mode 100644 mpgg/utilities.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..9bff191 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,9 @@ +version = 1 + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" + max_line_length = 120 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..99efb28 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = .venv,build,dist,*.pyi +max-line-length = 120 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2a49387 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Clone repository '...' +2. Run '....' +3. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..c0eb3b7 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,44 @@ +name: cd + +on: + push: + tags: + - "v*" + +jobs: + tagged-release: + name: Tagged Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10.x' + - name: Install VapourSynth + uses: rlaphoenix/install-vapoursynth-action@v2.0.1 + with: + version: 61 + cache: true + - name: Install Poetry + uses: abatilo/actions-poetry@v2.2.0 + - name: Install dependencies + run: poetry install + - name: Build wheel + run: poetry build + - name: Upload wheel + uses: actions/upload-artifact@v2.2.4 + with: + name: Python Wheel + path: "dist/*.whl" + - name: Deploy release + uses: marvinpinto/action-automatic-releases@latest + with: + prerelease: false + repo_token: "${{ secrets.GITHUB_TOKEN }}" + files: | + dist/*.whl + - name: Publish to PyPI + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + run: poetry publish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..78fdcfc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: ci + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + # We only test what the latest versions of VapourSynth supports on Linux. + # Usually that is 3.8 (to keep Windows 7 support) and the latest version. + python-version: ['3.8', '3.10'] + vapoursynth-version: [58, 59, 60, 61] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install VapourSynth R${{ matrix.vapoursynth-version }} + uses: rlaphoenix/install-vapoursynth-action@v2.0.1 + with: + version: ${{ matrix.vapoursynth-version }} + cache: true + - name: Install Poetry + uses: abatilo/actions-poetry@v2.3.0 + - name: Install project + run: poetry install --no-dev + - name: Install flake8 and isort + run: python -m pip install flake8 isort + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Check import order with isort + run: isort --check-only --diff . + - name: Build project + run: poetry build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a5bd3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# JetBrains project settings +.idea + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.directory +.idea/dataSources.local.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d6340d4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fbface7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2023-03-30 + +Initial release as MPGG (was previously known as PD2V/PDeinterlacer). + +[1.0.0]: https://github.com/rlaphoenix/mpgg/releases/tag/v1.0.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..871ce8e --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..40282ef --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +

+ MPGG +
+ Streamlined MPEG-1 and MPEG-2 source loader and helper utility for VapourSynth +

+ +

+ + Build status + + + Python version + + + VapourSynth version + + + DeepSource + +

+ +## Features + +- 🎥 Supports MPEG-1 and MPEG-2 Sources +- 🧠 Understands Mixed-scan Sources +- 🤖 VFR to CFR (Variable to Constant frame rate) +- 🛠️ Automatic Frame-indexing using DGIndex +- ⚙️ Zero-configuration +- 🧩 Easy installation via PIP/PyPI +- ❤️ Fully Open-Source! Pull Requests Welcome + +## Installation + +```shell +$ pip install mpgg +``` + +Voilà 🎉! You now have the `mpgg` package installed, and you can now import it from a VapourSynth script. + +### Dependencies + +The following is a list of software that needs to be installed manually. MPGG cannot install these automatically +on your behalf. + +#### Software + +- [MKVToolnix] (specifically mkvextract) for demuxing MPEG streams from MKV containers. +- [DGIndex] for automatic frame-indexing of MPEG streams. + +Make sure you put them in your current working directory, in the installation directory, or put the directory path in +your `PATH` environment variable. If you do not do this then their binaries will not be able to be found. + + [MKVToolNix]: + [DGIndex]: + +#### VapourSynth Plugins + +- [d2vsource] for loading an indexed DGIndex project file. + +These plugins may be installed using [vsrepo] on Windows, or from a package repository on Linux. + + [d2vsource]: + [vsrepo]: + +## Usage + +The following is an example of using MPGG to get a clean CFR Fully Progressive stream from an +Animated Mixed-scan VFR DVD-Video source. + +```python +import functools + +from mpgg import MPGG +from havsfunc import QTGMC + +# load the source with verbose information printed +mpg = MPGG(r"C:\Users\John\Videos\animated_dvd_video.mkv", verbose=True) + +# recover progressive frames where possible, and show which frames were recovered +mpg.recover(verbose=True) + +# deinterlace any remaining interlaced frames with QTGMC, and show which frames were deinterlaced +mpg.deinterlace( + kernel=functools.partial(QTGMC, Preset="Very Slow", FPSDivisor=2), + verbose=True +) + +# convert VFR to CFR by duplicating frames in a pattern +mpg.ceil() + +# get the final clip (you may use the clip in between actions as well) +clip = mpg.clip + +# ... + +clip.set_output() +``` + +You can also chain calls! This is the same script but chained, + +```python +import functools + +from mpgg import MPGG +from havsfunc import QTGMC + +# load MPEG, recover progressive frames, deinterlace what's left, and finally VFR to CFR +clip = MPGG(r"C:\Users\John\Videos\animated_dvd_video.mkv", verbose=True).\ + recover(verbose=True).\ + deinterlace(kernel=functools.partial(QTGMC, Preset="Very Slow", FPSDivisor=2), verbose=True).\ + ceil().\ + clip + +# ... + +clip.set_output() +``` + +There are more methods not shown here. I recommend taking a look at the MPGG class for further +information, methods, and more. + +> __Warning__ Do not copy/paste and re-use these examples. Read each method's doc-string information +> as they each have their own warnings, tips, and flaws that you need to be aware of. For example, +> recover() shouldn't be used on all MPEG sources, floor() shouldn't be used with recover(), you +> may not want to use ceil() if you want to keep encoding as VFR, and such. + +## Terminology + +| Term | Meaning | +|----------------|--------------------------------------------------------------------------------| +| CFR | Constant frame-rate, the source uses a set frame rate on playback | +| VFR | Variable frame-rate, the source switches frame rate at least once on playback | +| Scan | The technology used to show images on screens, i.e., Interlaced or Progressive | +| Mixed-scan | Source with both Progressive and Interlaced frames within the video data | +| Frame-indexing | Analyzing a source to index frame/field information for frame-serving | + +## Contributors + + + +## License + +© 2021-2023 rlaphoenix — [GNU General Public License, Version 3.0](LICENSE) diff --git a/mpgg/__init__.py b/mpgg/__init__.py new file mode 100644 index 0000000..c38de0d --- /dev/null +++ b/mpgg/__init__.py @@ -0,0 +1,5 @@ +__version__ = "1.0.0" + +from .mpgg import MPGG + +__ALL__ = (__version__, MPGG) diff --git a/mpgg/mpgg.py b/mpgg/mpgg.py new file mode 100644 index 0000000..650256f --- /dev/null +++ b/mpgg/mpgg.py @@ -0,0 +1,419 @@ +from __future__ import annotations + +import functools +import math +from collections import Counter +from pathlib import Path +from typing import Optional + +import vapoursynth as vs +from more_itertools import split_at +from pyd2v import D2V +from pymediainfo import MediaInfo +from vapoursynth import core + +from mpgg.utilities import get_aspect_ratio, get_par, get_standard, group_numbers, list_select_every + + +class MPGG: + def __init__(self, file: str, verbose: bool = False): + """ + Index MPEG-1 or MPEG-2 file with DGIndex and load with Derek dwbuiten's d2v source loader. + + Automatic indexing will take place if no D2V for the file was found. You may + explicitly load a D2V file if the MPEG file it indexed is somewhere else. + + Specific settings are used when indexing to have full compatibility will all + kinds of scan configurations. Pre-existing D2V files should be deleted before + being used with MPGG, unless it was indexed by MPGG. This is because they may + have been indexed with unsupported settings. + + Warning: The integrity of the `flags` data relative to the internal `clip` is + crucial for the accuracy of any function called from hence forth. If you + modify the internal clip, please make sure to update the `flags` data + appropriately. Modifying the internal clip is not recommended. + + Parameters: + file: The video file to load. It may be an MKV, VOB, MPG/MPEG, or a D2V. + verbose: Print useful information about the source, and it's scan type. + """ + if not hasattr(core, "d2v"): + raise EnvironmentError( + "Required plugin 'd2vsource' is not installed. " + "See https://github.com/dwbuiten/d2vsource" + ) + + # TODO: Somehow add a check that the D2V was indexed by pyd2v + self.d2v = D2V.load(Path(file)) + + self.file = self.d2v.path + self.flags = self._get_flags(self.d2v) + self.pulldown, self.pulldown_str = self._get_pulldown(self.flags) + self.total_frames = len(self.flags) + self.p_frames = sum(f["progressive_frame"] for f in self.flags) + self.i_frames = self.total_frames - self.p_frames + + # A DVD-spec MPEG stream is considered VFR if there's interlaced and progressive frames, + # otherwise it would only ever be NTSC (30000/1001i) or PAL (25i), therefore constant. + self.vfr = any(f["progressive_frame"] and f["rff"] and f["tff"] for f in self.flags) and any( + not f["progressive_frame"] for f in self.flags) + + # Do not apply RFF (Repeat First Field) as this would be the flags for Software Pulldown. + # We do not want to cause further interlacing. Instead, we will handle this later using + # either ceil() or floor(). Or, the user can leave it alone to retain VFR. + self.clip = core.d2v.Source(self.file, rff=False) + + # We must copy the flags to each frame/field of the clip, or we cannot make important + # decisions after frame rate adjustments as the mapping to `flags` would be lost. + self.clip = self._stamp_frames(self.clip, self.flags) + + # Override the _ColorRange value set by core.d2v.Source with one obtained from + # the container/stream if available, or fallback and assume limited/TV. + # This makes YUVRGB_Scale setting redundant to reduce possibilities of mistakes. + video_track = next(iter(MediaInfo.parse(self.d2v.videos[0]).video_tracks), None) + if video_track and getattr(video_track, "color_range", None): + color_range = {"Full": 0, "Limited": 1}[video_track.color_range] + else: + color_range = 1 # assume limited/TV as MPEGs most likely are + self.clip = core.std.SetFrameProp(self.clip, "_ColorRange", color_range) + + self.standard = get_standard(self.clip.fps.numerator / self.clip.fps.denominator) + self.dar = self.d2v.settings["Aspect_Ratio"] + if isinstance(self.dar, list): + self.dar = self.dar[0] + self.sar = get_aspect_ratio(self.clip.width, self.clip.height) + self.par = get_par(self.clip.width, self.clip.height, *[int(x) for x in self.dar.split(":")]) + + if verbose: + progressive_p = (self.p_frames / self.total_frames) * 100 + self.clip = core.text.Text( + self.clip, + text=" " + (" \n ".join([ + f"Progressive: {progressive_p:05.2f}% ({self.p_frames})" + ( + f" w/ Pulldown {self.pulldown_str} (Cycle: {self.pulldown})" if self.pulldown else + " - No Pulldown" + ), + f"Interlaced: {100 - progressive_p:05.2f}% ({self.total_frames - self.p_frames})", + f"VFR? {self.vfr} DAR: {self.dar} SAR: {self.sar} PAR: {self.par}", + self.standard + ])) + " ", + alignment=1, + scale=1 + ) + + if not self.vfr and not self.i_frames: + # MPEG is fully progressive (via Pulldown flags), so core.d2v.Source gives it + # an FPS of e.g., 30000/1001, when it should be e.g., 24000/1001. + self.floor() + self.pulldown = None + + def recover(self, verbose=False, **kwargs): + """ + Recovers progressive frames from an interlaced clip using VIVTC VFM. + + Do not provide `order` (TFF/BFF) argument manually unless you need to override the auto-detected + order, or it could not be auto-detected. + + For possible arguments, see the VIVTC docs here: + + + Tips: - Only use this on sources where the majority of combed frames are recoverable (e.g. Animation), + otherwise you are risking jerkiness and making your script slower for likely no gain. + - Use this before *any* frame rate, length, or visual adjustments, including before deinterlacing. + - This may add irregular duplicate frames, You should use VDecimate afterwards. + - Do NOT use `floor()` if you use this method, it will not be safe. + """ + if not isinstance(self.clip, vs.VideoNode): + raise TypeError("This is not a clip") + + matched_tff = core.vivtc.VFM(self.clip, order=1, **kwargs) + matched_bff = core.vivtc.VFM(self.clip, order=0, **kwargs) + + def _m(n: int, f: vs.VideoFrame, c: vs.VideoNode, tff: vs.VideoNode, bff: vs.VideoNode): + # frame marked as progressive, skip matching + if f.props["PVSFlagProgressiveFrame"] or f.props.get("_Combed") == 0: + if c.format and tff.format and c.format.id != tff.format.id: + c = core.resize.Point(c, format=tff.format.id) + return core.text.Text(c, "Progressive", alignment=3) if verbose else c + # interlaced frame, match (if _FieldBased is > 0) + rc = {0: c, 1: bff, 2: tff}[f.props["_FieldBased"]] # type: ignore + return core.text.Text( + rc, + "Matched (%s)" % {0: "Recovered", 1: "Combed "}[rc.get_frame(n).props["_Combed"]], + alignment=3 + ) if verbose else rc + + self.clip = core.std.FrameEval( + matched_tff, + functools.partial( + _m, + c=self.clip, + tff=matched_tff, + bff=matched_bff + ), + prop_src=self.clip + ) + return self + + def deinterlace(self, kernel: functools.partial, verbose: bool = False) -> MPGG: + """ + Deinterlace clip only on frames that are marked as interlaced. + + Frames that are recovered with :meth:`recover` will be skipped. However, if + VFM detected combing in the frame, it will be deinterlaced. + + The kernel should be a function that accepts a `clip` VideoNode in the first + positional argument. It must also accept a `TFF`/`tff` keyword argument. + You can pass kernel settings via `functools.partial`. For example:: + + kernel = functools.partial(QTGMC, FPSDivisor=2, Preset="VeryFast") + + If the kernel you want to use does not accept a `TFF`/`tff` keyword argument, + you can manually proxy it to another argument with a lambda function, e.g.,:: + + def foo(clip, field: int, preset: str): + # pseudo-kernel using `field` to indicate tff/bff + # ... + + kernel = functools.partial(lambda clip, tff: foo(clip, field=tff, preset="Fast")) + + You should not manually specify TFF/field order as the field order is automatically + determined. In fact, it actually supports clips which switch field order on the fly! + + Parameters: + kernel: A function to pass the clip through when it needs deinterlacing. + verbose: Print useful information about the deinterlacing result. + """ + if not isinstance(self.clip, vs.VideoNode): + raise TypeError(f"Expected clip to be a {vs.VideoNode}, not {self.clip!r}") + if not callable(kernel): + raise TypeError(f"Expected kernel to be a function, not {kernel!r}") + if len(kernel.args) > 1: + # causes conflicts with the clip positional argument + raise ValueError("Invalid kernel arguments, no positional arguments are allowed") + + deinterlaced_tff = kernel(self.clip, TFF=True) + deinterlaced_bff = kernel(self.clip, TFF=False) + + fps_factor = deinterlaced_tff.fps.numerator / deinterlaced_tff.fps.denominator + fps_factor = fps_factor / (self.clip.fps.numerator / self.clip.fps.denominator) + if fps_factor not in (1.0, 2.0): + # TODO: Add support for more, we might already support mod2, e.g., 2/4/8/16/e.t.c + raise ValueError( + f"The kernel returned an unsupported frame-rate ({deinterlaced_tff.fps}). " + + "Only single-rate and double-rate deinterlacing is currently supported." + ) + + def _d(n: int, f: vs.VideoFrame, c: vs.VideoNode, tff: vs.VideoNode, bff: vs.VideoNode, ff: int): + # Frame marked as progressive by DGIndex or VFM, skip deinterlacing + if f.props["PVSFlagProgressiveFrame"] or f.props.get("_Combed") == 0: + # duplicate if not a single-rate fps output + rc = core.std.Interleave([c] * ff) if ff > 1 else c + if rc.format and tff.format and rc.format.id != tff.format.id: + rc = core.resize.Spline16(rc, format=tff.format.id) + return core.text.Text( + rc, + # space it to keep recover()'s verbose logs visible + "Progressive" + ["", "\n"]["_Combed" in f.props], + alignment=3 + ) if verbose else rc + # Frame otherwise assumed to be interlaced or progressively encoded interlacing. + # It won't deinterlace progressive frames here unless recover() was run and detected + # that the frame was interlaced by detecting visual combing artifacts. + # Do note that deinterlacing progressively encoded interlaced frames don't always look + # the best, but not much can really be done in those cases. + order = f.props["_FieldBased"] + if f.props.get("_Combed", 0) != 0: + order = 2 # TODO: Don't assume TFF + rc = {0: c, 1: bff, 2: tff}[order] # type: ignore + field_order = {0: "Progressive ", 1: "BFF", 2: "TFF"}[order] # type: ignore + return core.text.Text( + rc, + ("Deinterlaced (%s)" % field_order) + ["", "\n"]["_Combed" in f.props], + alignment=3 + ) if verbose else rc + + self.clip = core.std.FrameEval( + deinterlaced_tff, + functools.partial( + _d, + c=self.clip, + tff=deinterlaced_tff, + bff=deinterlaced_bff, + ff=int(fps_factor) # TODO: floor/ceil instead? + ), + prop_src=self.clip + ) + + return self + + def ceil(self) -> MPGG: + """ + VFR to CFR by duplicating progressive frames with the RFF flag. + + This does the same operation as honoring pulldown/RFF, but without + interlacing the progressive frames, resulting in a mixed-scan stream + and no further spatial loss. + """ + if not self.vfr: + return self + + pf = [i for i, f in enumerate(self.flags) if f["progressive_frame"] and f["rff"] and f["tff"]] + + self.clip = core.std.DuplicateFrames(self.clip, pf) + + def disable_rff(n: int, f: vs.VideoFrame) -> vs.VideoFrame: + f = f.copy() + f.props["PVSFlagRff"] = 0 + return f + + self.clip = core.std.ModifyFrame(self.clip, self.clip, disable_rff) + self.flags = [ + flag + for i, f in enumerate(self.flags) + for flag in [dict(f, rff=False)] * (2 if i in pf else 1) + ] + + self.vfr = False + return self + + def floor(self, cycle: int = None, offsets: list[int] = None) -> MPGG: + """ + VFR to CFR by decimating interlaced sections to match progressive sections. + + Warning: Do not use this function if it causes the duration to change. Do not use + this function if you used recover() or VFM. If duplicate frames are in an + irregular pattern then the constant cycle and pattern decimation method of + this function will result in an incorrect duration with audio and subtitle + desyncing. This should only be used on a clean source where any software or + hard pulldown is in a consistent pattern. If the duration is changing, use + VDecimate instead. + + Parameters: + cycle: Defaults to the pulldown cycle. + offsets: Defaults to keeping the last frame of each cycle. + """ + cycle = cycle or self.pulldown + if cycle: + offsets = offsets + if offsets is None: + offsets = list(range(cycle - 1)) + if not offsets or len(offsets) >= cycle: + raise ValueError("Invalid offsets, cannot be empty or have >= items of cycle") + + wanted_fps_num = self.clip.fps.numerator - (self.clip.fps.numerator / cycle) + progressive_sections = group_numbers([n for n, f in enumerate(self.flags) if f["progressive_frame"]]) + interlaced_sections = group_numbers([n for n, f in enumerate(self.flags) if not f["progressive_frame"]]) + + self.clip = core.std.Splice([x for _, x in sorted( + [ + ( + x[0], # first frame # of the section, used for sorting when splicing + core.std.AssumeFPS( + self.clip[x[0]:x[-1] + 1], + fpsnum=wanted_fps_num, + fpsden=self.clip.fps.denominator + ) + ) for x in progressive_sections + ] + [ + ( + x[0], + core.std.SelectEvery( + self.clip[x[0]:x[-1] + 1], + cycle, + offsets + ) + ) for x in interlaced_sections + ], + key=lambda section: int(section[0]) + )]) + interlaced_frames = [ + n + for s in interlaced_sections + for n in list_select_every(s, cycle, offsets, inverse=True) + ] + self.flags = [f for i, f in enumerate(self.flags) if i not in interlaced_frames] + self.vfr = False + return self + + @staticmethod + def _stamp_frames(clip: vs.VideoNode, flags: list[dict]) -> vs.VideoNode: + """Stamp frames with prop data that may be needed.""" + + def _set_flag_props(n, f, c, fl): + for key, value in fl[n].items(): + if isinstance(value, bool): + value = 1 if value else 0 + if isinstance(value, bytes): + value = value.decode("utf-8") + c = core.std.SetFrameProp(c, **{ + ("intval" if isinstance(value, int) else "data"): value + }, prop="PVSFlag%s" % key.title().replace("_", "")) + return c[n] + + vob_indexes = [v for _, v in {f["vob"]: n for n, f in enumerate(flags)}.items()] + vob_indexes = [ + "%s-%s" % ((0 if n == 0 else (vob_indexes[n - 1] + 1)), i) + for n, i in enumerate(vob_indexes) + ] + clip = core.std.SetFrameProp(clip, prop="PVSVobIdIndexes", data=" ".join(vob_indexes)) + + return core.std.FrameEval( + clip, + functools.partial( + _set_flag_props, + c=clip, + fl=flags + ), + prop_src=clip + ) + + @staticmethod + def _get_flags(d2v: D2V) -> list[dict]: + """Get Flag Data from D2V object.""" + return [ + dict(**flag, vob=d["vob"], cell=d["cell"]) + for d in d2v.data + for flag in d["flags"] + ] + + @staticmethod + def _get_pulldown(flags: list[dict]) -> tuple[int, Optional[str]]: + """ + Get most commonly used Pulldown cycle and syntax string. + Returns tuple (pulldown, cycle), or (0, None) if Pulldown is not used. + """ + # TODO: Find a safe way to get cycle, i.e. not resort to most common digit. + # Previously I would do this code on all progressive rff indexes, but when it entered and + # exited interlaced sections the right index vs left index were very far apart, messing up + # the accuracy of the cycles. I cannot find out why my test source (Family Guy S01E01 USA + # NTSC) is still having random different numbers in each (now progressive only) sections. + sections = [ + section + for split in split_at( + [dict(x, i=n) for n, x in enumerate(flags)], + lambda flag: not flag["progressive_frame"] + ) + for section in [[flag["i"] for flag in split if flag["rff"] and flag["tff"]]] + if section and len(section) > 1 + ] + if not sections: + return 0, None + + cycle = Counter([ + Counter([ + (right - left) + for left, right in zip(indexes[::2], indexes[1::2]) + ]).most_common(1)[0][0] + for indexes in sections + ]).most_common(1)[0][0] + 1 + + pulldown = ["2"] * math.floor(cycle / 2) + if cycle % 2: + pulldown.pop() + pulldown.append("3") + + return cycle, ":".join(pulldown) + + +__ALL__ = (MPGG,) diff --git a/mpgg/utilities.py b/mpgg/utilities.py new file mode 100644 index 0000000..bbe02bf --- /dev/null +++ b/mpgg/utilities.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import math +from itertools import groupby +from typing import Any, Union + + +def get_aspect_ratio(width: int, height: int) -> str: + """Calculate the aspect-ratio gcd string from resolution.""" + r = math.gcd(width, height) + return "%d:%d" % (int(width / r), int(height / r)) + + +def get_par(width: int, height: int, aspect_ratio_w: int, aspect_ratio_h: int) -> str: + """Calculate the pixel-aspect-ratio string from resolution.""" + par_w = height * aspect_ratio_w + par_h = width * aspect_ratio_h + par_gcd = math.gcd(par_w, par_h) + par_w = int(par_w / par_gcd) + par_h = int(par_h / par_gcd) + return "%d:%d" % (par_w, par_h) + + +def get_standard(aspect: float) -> str: + """Convert an aspect float to a standard string.""" + return { + 0: "?", + 24 / 1: "FILM", + 25 / 1: "PAL", + 50 / 1: "PALi", + 30000 / 1001: "NTSC", + 60000 / 1001: "NTSCi", + 24000 / 1001: "NTSC (FILM)" + }[aspect] + + +def group_numbers(numbers: list[int]) -> list[list[int]]: + """ + Group consecutive numbers into sub-lists. + + Note: It does not pre-sort the input numbers. + + For example: + >>> group_numbers([1, 2, 3, 5, 6, 7, 9]) + [[1, 2, 3], [5, 6, 7], [9]] + + Parameters: + numbers: list of numbers to group. + """ + for k, g in groupby(enumerate(numbers), lambda x: x[0] - x[1]): + yield list(map(lambda x: x[1], g)) + + +def list_select_every(data: list[Any], cycle: int, offsets: list[int], inverse: Union[bool, int] = False) -> list[Any]: + """ + VapourSynth's SelectEvery for generic list data, and inverse. + + Parameters: + data: data to select entries from. + cycle: number of entries to assess at a time. + offsets: offsets of entries to take per cycle (zero-indexed). + inverse: invert the offsets and take the entries it did not want. + """ + if not isinstance(cycle, int) or cycle < 1: + raise ValueError("Cycle must be an int greater than or equal to 1.") + + if not isinstance(offsets, list): + raise TypeError(f"Expected offsets to be a {list!r}, not {offsets}") + if not offsets: + raise ValueError("Offsets must not be empty.") + if any(not isinstance(x, int) for x in offsets): + raise TypeError(f"Expected offsets to be a {list!r} of {int!r}, not {offsets}") + + if not isinstance(inverse, (bool, int)) or (isinstance(inverse, int) and inverse not in (0, 1)): + raise TypeError(f"Expected inverse to be a {bool!r} or boolean-like {int!r}, not {inverse}") + + # TODO: Should this be removed to allow duplicates? + offsets = set(offsets) + + return [ + x + for n, x in enumerate(data) + if (n % cycle in offsets) ^ inverse + ] + + +__ALL__ = (get_aspect_ratio, get_par, get_standard, group_numbers, list_select_every) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6b8ad83 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,427 @@ +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "construct" +version = "2.8.8" +description = "A powerful declarative parser/builder for binary data" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"}, +] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "filelock" +version = "3.10.7" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.10.7-py3-none-any.whl", hash = "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536"}, + {file = "filelock-3.10.7.tar.gz", hash = "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "identify" +version = "2.5.22" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, + {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jsonpickle" +version = "2.2.0" +description = "Python library for serializing any arbitrary object graph into JSON" +category = "main" +optional = false +python-versions = ">=2.7" +files = [ + {file = "jsonpickle-2.2.0-py2.py3-none-any.whl", hash = "sha256:de7f2613818aa4f234138ca11243d6359ff83ae528b2185efdd474f62bcf9ae1"}, + {file = "jsonpickle-2.2.0.tar.gz", hash = "sha256:7b272918b0554182e53dc340ddd62d9b7f902fec7e7b05620c04f3ccef479a0e"}, +] + +[package.extras] +docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["ecdsa", "enum34", "feedparser", "jsonlib", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (<1.1.0)", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] +testing-libs = ["simplejson", "ujson", "yajl"] + +[[package]] +name = "more-itertools" +version = "9.1.0" +description = "More routines for operating on iterables, beyond itertools" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, + {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, +] + +[[package]] +name = "mypy" +version = "1.1.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"}, + {file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"}, + {file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"}, + {file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"}, + {file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"}, + {file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"}, + {file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"}, + {file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"}, + {file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"}, + {file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"}, + {file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"}, + {file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"}, + {file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"}, + {file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"}, + {file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"}, + {file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"}, + {file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"}, + {file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"}, + {file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"}, + {file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pre-commit" +version = "3.2.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.2.1-py2.py3-none-any.whl", hash = "sha256:a06a7fcce7f420047a71213c175714216498b49ebc81fe106f7716ca265f5bb6"}, + {file = "pre_commit-3.2.1.tar.gz", hash = "sha256:b5aee7d75dbba21ee161ba641b01e7ae10c5b91967ebf7b2ab0dfae12d07e1f1"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyd2v" +version = "1.3.0" +description = "A Python Parser for DGMPGDec's D2V Project Files." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "pyd2v-1.3.0-py3-none-any.whl", hash = "sha256:813743387bd2d201ac05ee1824d0c042f1adf00e394dc2e540d05d48c7e87b66"}, + {file = "pyd2v-1.3.0.tar.gz", hash = "sha256:0135fd83ef4843aedeae9e9dbd68440538b2003b4e51fda90b0588dba6af1140"}, +] + +[package.dependencies] +click = ">=8.0.1,<9.0.0" +jsonpickle = ">=2.0.0,<3.0.0" + +[[package]] +name = "pymediainfo" +version = "6.0.1" +description = "A Python wrapper for the mediainfo library." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymediainfo-6.0.1-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:81165e895e1e362fa11c128ce2bc976cb8a74224f96f309a88ee047106041b0a"}, + {file = "pymediainfo-6.0.1-py3-none-win32.whl", hash = "sha256:bb3a48ac9706626fd2fa7881f4271728459a1c9a082917deb0c7dd343d8a1be5"}, + {file = "pymediainfo-6.0.1-py3-none-win_amd64.whl", hash = "sha256:c38e79d4d2062732ae555b564c3cac18a6de4f36e033066c617f386cf5e77564"}, + {file = "pymediainfo-6.0.1.tar.gz", hash = "sha256:96e04bac0dfcb726bed70c314b1219121c4b9344c66a98f426ce27d7f9abffb0"}, +] + +[[package]] +name = "pymp4" +version = "1.3.2" +description = "A Python parser for MP4 boxes" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pymp4-1.3.2-py2.py3-none-any.whl", hash = "sha256:8bb9d761181fcc5513fa89e0f458855e1a62d56a4ff0139a46b8b377a9e13029"}, + {file = "pymp4-1.3.2.tar.gz", hash = "sha256:a87cf9adbb9cd9db0f3bd08aeb74fb84356274d6380429630c440d0ae68bd2d5"}, +] + +[package.dependencies] +construct = "2.8.8" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "setuptools" +version = "67.6.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "vapoursynth" +version = "61" +description = "A frameserver for the 21st century" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "VapourSynth-61-cp310-cp310-win32.whl", hash = "sha256:4f0d74a626f9ac7da79c016eeacbda41d97065462b01ca7d34d794367c278294"}, + {file = "VapourSynth-61-cp310-cp310-win_amd64.whl", hash = "sha256:6f3b7c668031840c8612c2e3cf40f7aad35f5f04dc34b4ac76bd221dc2abca4e"}, + {file = "VapourSynth-61-cp38-cp38-win32.whl", hash = "sha256:a1a7465bafd8727a599a4687a836a19bb9712bd91f29155301c196f03a1a8b00"}, + {file = "VapourSynth-61-cp38-cp38-win_amd64.whl", hash = "sha256:1ff652f070418afddf635c348dc3595ea4db57ceefcea0d53db2701450af7ec2"}, + {file = "VapourSynth-61.zip", hash = "sha256:9b0f7f53008ab7a7495ca4e3d8402c8049be279d562a4aef3977ce62b191277c"}, +] + +[[package]] +name = "virtualenv" +version = "20.21.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, + {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<4" + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8 || ^3.10" +content-hash = "27e899b68b179502c2aae47d2be70c7e2f76cc63af7fe45c138d1bea0a5b950f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..35e6f0e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ['poetry-core>=1.0.0'] +build-backend = 'poetry.core.masonry.api' + +[tool.poetry] +name = 'mpgg' +version = '1.0.0' +description = "Streamlined MPEG-1 and MPEG-2 source loader and helper utility for VapourSynth" +license = 'GPL-3.0-only' +authors = ['rlaphoenix '] +readme = 'README.md' +homepage = 'https://github.com/rlaphoenix/mpgg' +repository = 'https://github.com/rlaphoenix/mpgg' +keywords = ['vapoursynth', 'dvd', 'mpeg', 'mpeg2'] +classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Other Environment', + 'Intended Audience :: End Users/Desktop', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Topic :: Multimedia :: Video', +] + +[tool.poetry.dependencies] +python = "^3.8 || ^3.10" +pymp4 = "^1.2.0" +pyd2v = "^1.3.0" +vapoursynth = ">=50" +more-itertools = "^9.1.0" +pymediainfo = "^6.0.1" + +[tool.poetry.dev-dependencies] +pre-commit = "^3.2.1" +mypy = "^1.1.1" +isort = "^5.12.0" + +[tool.isort] +line_length = 120 + +[tool.mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +follow_imports = 'silent' +ignore_missing_imports = true +no_implicit_optional = true