From 6f71628706f2af021c16bcbfc33d78ff3dfae35a Mon Sep 17 00:00:00 2001 From: TimeTravelPenguins Date: Mon, 25 Dec 2023 01:10:42 +1100 Subject: [PATCH] Moved division to a multiplication to reduce computation cost Refined concept, but introduced errors within rich. Rename refactor and defined __all__ Moved type aliases and constants to own files to avoid cyclic referencing Refactored code to a more maintainable state Moved into Extractor.py to avoid cyclic referencing Changed imports along with refeactor WIP: Progress printing Finished PoC revision of progress display Refactor: Moved files. Continued working on progress display. Adjusted version Continued working on progress display Black reformat Refactor & Updated pyproject.toml Fixed issues with progress not displaying Need to fix logging option and fixed printing bug Corrected progress duplicating Black reformat Update project version Moved main.py into own submodule Updated to set up pre-commit Cleanup with mypy Corrected pre-commits Updated pyproject Added pytest Changed to start app from __main__.py Create CI.yml Create python-publish.yml Formatted Updated workflows Reformatted file Added poetry-dynamic-versioning plugin Added dynamic version plugin Removed explicit python version Changed install order Added version Added basic tests Added app_theme to __all__ Refactor to target python 3.8 updated Updated to target lower python version Refactor to target python 3.8+ Replaced built-in Corrected types Updated project to better use pyproject, pre-commits, and poetry plugins Squash commit: Corrected pre-commit Updated project layout Formatted Updated workflows Added poetry-dynamic-versioning plugin Removed explicit python version Changed install order Added version Removed Added simple test Refactor to target python 3.8+ Corrected typing Corrected typing Changed lowest target version to python 3.11 Corrected tests Squashed commit of the following: commit 4a17e5dce3c9dd379745c2eebf41dc1080dbf14e Merge: 1fc7702 5280ebb Author: TimeTravelPenguin Date: Thu Jan 4 22:37:04 2024 +1100 Merge branch 'rich-progress-bar' of https://github.com/TimeTravelPenguin/BulkAudioExtractTool into rich-progress-bar commit 1fc770238dea6fe244a3f6d651eea1ad4d3a3c6b Author: TimeTravelPenguin Date: Thu Jan 4 22:28:33 2024 +1100 Corrected tests Update Squashed commit of the following: commit 7d7253ec01352bade9efda02ec50034277cea2ab Author: TimeTravelPenguin Date: Thu Jan 4 23:24:15 2024 +1100 Removed commit 78fb10003c2498fb50ed2052e657d11350439912 Author: TimeTravelPenguin Date: Thu Jan 4 23:04:07 2024 +1100 Update commit 4a17e5dce3c9dd379745c2eebf41dc1080dbf14e Merge: 1fc7702 5280ebb Author: TimeTravelPenguin Date: Thu Jan 4 22:37:04 2024 +1100 Merge branch 'rich-progress-bar' of https://github.com/TimeTravelPenguin/BulkAudioExtractTool into rich-progress-bar commit 1fc770238dea6fe244a3f6d651eea1ad4d3a3c6b Author: TimeTravelPenguin Date: Thu Jan 4 22:28:33 2024 +1100 Corrected tests Updated version Corrected versioning Corrected invalid typing Corrected versioning Version update --- .github/workflows/CI.yml | 75 ++++++ .pre-commit-config.yaml | 69 +++++ README.md | 2 +- mypy.ini | 27 ++ poetry.lock | 307 +++++++++++++++++++++-- pyproject.toml | 48 +++- requirements.txt | 146 +++++++++++ src/BAET/AudioExtractor.py | 162 ------------ src/BAET/Console.py | 6 - src/BAET/FFmpegExtract/CommandBuilder.py | 126 ---------- src/BAET/FFmpegExtract/FFmpegProcess.py | 86 ------- src/BAET/FFmpegExtract/__init__.py | 0 src/BAET/Helpers/Printing.py | 21 -- src/BAET/Helpers/__init__.py | 0 src/BAET/Logging.py | 26 -- src/BAET/Types.py | 103 -------- src/BAET/__init__.py | 5 + src/BAET/__main__.py | 35 +++ src/BAET/_aliases.py | 18 ++ src/BAET/_console.py | 6 + src/BAET/_constants.py | 5 + src/BAET/_logging.py | 30 +++ src/BAET/_metadata.py | 5 + src/BAET/{Theme.py => _theme.py} | 4 + src/BAET/{AppArgs.py => app_args.py} | 108 ++++++-- src/BAET/extract.py | 149 +++++++++++ src/BAET/job_progress.py | 123 +++++++++ src/BAET/jobs.py | 44 ++++ src/BAET/progress_list.py | 46 ++++ src/BAET/progress_status.py | 34 +++ src/BAET/progress_style.py | 132 ++++++++++ src/main.py | 45 ---- tests/__init__.py | 5 + tests/test_progress_style.py | 14 ++ 34 files changed, 1377 insertions(+), 635 deletions(-) create mode 100644 .github/workflows/CI.yml create mode 100644 .pre-commit-config.yaml create mode 100644 mypy.ini create mode 100644 requirements.txt delete mode 100644 src/BAET/AudioExtractor.py delete mode 100644 src/BAET/Console.py delete mode 100644 src/BAET/FFmpegExtract/CommandBuilder.py delete mode 100644 src/BAET/FFmpegExtract/FFmpegProcess.py delete mode 100644 src/BAET/FFmpegExtract/__init__.py delete mode 100644 src/BAET/Helpers/Printing.py delete mode 100644 src/BAET/Helpers/__init__.py delete mode 100644 src/BAET/Logging.py delete mode 100644 src/BAET/Types.py create mode 100644 src/BAET/__main__.py create mode 100644 src/BAET/_aliases.py create mode 100644 src/BAET/_console.py create mode 100644 src/BAET/_constants.py create mode 100644 src/BAET/_logging.py create mode 100644 src/BAET/_metadata.py rename src/BAET/{Theme.py => _theme.py} (68%) rename src/BAET/{AppArgs.py => app_args.py} (75%) create mode 100644 src/BAET/extract.py create mode 100644 src/BAET/job_progress.py create mode 100644 src/BAET/jobs.py create mode 100644 src/BAET/progress_list.py create mode 100644 src/BAET/progress_status.py create mode 100644 src/BAET/progress_style.py delete mode 100644 src/main.py create mode 100644 tests/__init__.py create mode 100644 tests/test_progress_style.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..600fb9f --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,75 @@ +name: CI + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + env: + DISPLAY: :0 + PYTEST_ADDOPTS: "--color=yes" + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + python: [ "3.11","3.12" ] + defaults: + run: + shell: bash + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + #- name: Setup macOS PATH + # if: runner.os == 'macOS' + # run: | + # echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: ~/.cache + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Check format with Black + run: | + poetry run black --check . + + - name: Typecheck with mypy + run: | + poetry run mypy + + - name: Run tests + run: | + source $VENV + poetry run pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2a00972 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +fail_fast: false + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-merge-conflict + - id: check-json + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: check-vcs-permalinks + - id: check-shebang-scripts-are-executable + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-mock-methods + - id: python-no-log-warn + - id: python-use-type-annotations + - id: rst-directive-colons + - id: rst-inline-touching-normal + + - repo: https://github.com/hadialqattan/pycln + rev: v2.4.0 + hooks: + - id: pycln + args: [ --all ] + + - repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + args: [ --safe ] + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + language_version: "3.12" + args: [ "--profile", "black" ] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + + - repo: https://github.com/python-poetry/poetry + rev: 1.7.0 + hooks: + - id: poetry-check + - id: poetry-lock + - id: poetry-export + args: [ "--without", "dev", "-f", "requirements.txt", "-o", "requirements.txt" ] + verbose: true + - id: poetry-install diff --git a/README.md b/README.md index 6ee0448..c6044c1 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,4 @@ On Linux/MacOS, locate your `~/.bashrc` or `~/.zshrc` file and edit it, adding: BAET_PATH="/path/to/BulkAudioExtractTool/main.py" alias baet="python3 $(BAET_PATH)" ``` -Restart your terminal and enter `baet --help`. The application's help screen should now show from any directory. \ No newline at end of file +Restart your terminal and enter `baet --help`. The application's help screen should now show from any directory. diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..63d7be7 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,27 @@ +[mypy] +strict = True +python_version = 3.12 +files = src + +install_types = true +ignore_missing_imports = True +cache_fine_grained = True +warn_unused_ignores = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_decorators = False +disallow_subclassing_any = False +warn_return_any = True +implicit_optional = False +strict_optional = True +warn_unreachable = True + +[mypy-BAET._aliases] +disable_error_code = valid-type + +[mypy-BAET._constants] +disable_error_code = valid-type + +[mypy-BAET.progress_status] +disable_error_code = valid-type diff --git a/poetry.lock b/poetry.lock index 79384a3..5791a28 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,9 +11,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "astroid" version = "3.0.2" @@ -25,8 +22,34 @@ files = [ {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +[[package]] +name = "asyncio" +version = "3.4.3" +description = "reference implementation of PEP 3156" +optional = false +python-versions = "*" +files = [ + {file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"}, + {file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"}, + {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, + {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, +] + +[[package]] +name = "bidict" +version = "0.22.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, + {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton"] +lint = ["pre-commit"] +test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", "sphinx"] [[package]] name = "black" @@ -65,8 +88,6 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -74,6 +95,17 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "click" version = "8.1.7" @@ -113,6 +145,17 @@ files = [ [package.extras] graph = ["objgraph (>=1.7.2)"] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "ffmpeg-python" version = "0.2.0" @@ -130,6 +173,22 @@ future = "*" [package.extras] dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "future" version = "0.18.3" @@ -140,6 +199,31 @@ files = [ {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, ] +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isort" version = "5.13.2" @@ -249,7 +333,6 @@ files = [ [package.dependencies] mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.1.0" [package.extras] @@ -269,6 +352,20 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "23.2" @@ -306,6 +403,39 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pydantic" version = "2.5.3" @@ -472,21 +602,97 @@ files = [ astroid = ">=3.0.1,<=3.1.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "rich" version = "13.7.0" @@ -501,7 +707,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -521,16 +726,21 @@ files = [ rich = ">=11.0.0" [[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "tomlkit" version = "0.12.3" @@ -542,6 +752,43 @@ files = [ {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] +[[package]] +name = "types-docutils" +version = "0.20.0.3" +description = "Typing stubs for docutils" +optional = false +python-versions = "*" +files = [ + {file = "types-docutils-0.20.0.3.tar.gz", hash = "sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd"}, + {file = "types_docutils-0.20.0.3-py3-none-any.whl", hash = "sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321"}, +] + +[[package]] +name = "types-pygments" +version = "2.17.0.0" +description = "Typing stubs for Pygments" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-Pygments-2.17.0.0.tar.gz", hash = "sha256:4241c5f1b7448e559cd820143a564cf10de626a95ab10e2daa463449d16864e7"}, + {file = "types_Pygments-2.17.0.0-py3-none-any.whl", hash = "sha256:83c33c89037f433fd5315b1abf80f5cb8b589b2d0549444d7f63824c628142c7"}, +] + +[package.dependencies] +types-docutils = "*" +types-setuptools = "*" + +[[package]] +name = "types-setuptools" +version = "69.0.0.0" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-setuptools-69.0.0.0.tar.gz", hash = "sha256:b0a06219f628c6527b2f8ce770a4f47550e00d3e8c3ad83e2dc31bc6e6eda95d"}, + {file = "types_setuptools-69.0.0.0-py3-none-any.whl", hash = "sha256:8c86195bae2ad81e6dea900a570fe9d64a59dbce2b11cc63c046b03246ea77bf"}, +] + [[package]] name = "typing-extensions" version = "4.9.0" @@ -553,7 +800,27 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "c046bf0d295802a1b0e39010b214c7ce26b7a0929a22494ec6f54be1ef3a4d03" +python-versions = ">=3.11" +content-hash = "61dc16e8051da6ede25203efd6c51da28888958fb313ad06f5f20694df78ac16" diff --git a/pyproject.toml b/pyproject.toml index f696bbe..931848f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,41 +1,63 @@ [tool.poetry] name = "BAET" -version = "1.0.0-alpha" +version = "0.1.1" description = "" -authors = ["TimeTravelPenguins "] +authors = ["TimeTravelPenguin "] readme = "README.md" packages = [{ include = "BAET", from = "src" }] +classifiers = [ + "Environment :: Console", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://github.com/TimeTravelPenguin/BulkAudioExtractTool" [tool.poetry.scripts] -baet = "src.main:main" +baet = "BAET.__main__:main" [tool.poetry.dev-dependencies] black = "*" isort = "*" mypy = "*" +pre-commit = "^3.6.0" pylint = "*" +pytest = "^7.4.4" +types-pygments = "^2.17.0.0" [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.11" +asyncio = "^3.4.3" +bidict = "^0.22.1" ffmpeg-python = "^0.2.0" +more-itertools = "^10.1.0" pydantic = "^2.5.2" rich = "^13.7.0" rich-argparse = "^1.4.0" -more-itertools = "^10.1.0" [tool.black] -target-version = ["py312"] +line-length = 120 [tool.isort] profile = "black" -py_version = 312 +line_length = 120 [tool.mypy] -disallow_untyped_decorators = false -install_types = true -python_version = "3.12" -strict = true +mypy_path = "src" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.poetry-dynamic-versioning] +enable = true +metadata = false +vcs = "git" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..676bd65 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,146 @@ +annotated-types==0.6.0 ; python_version >= "3.11" \ + --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \ + --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d +asyncio==3.4.3 ; python_version >= "3.11" \ + --hash=sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41 \ + --hash=sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de \ + --hash=sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c \ + --hash=sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d +bidict==0.22.1 ; python_version >= "3.11" \ + --hash=sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d \ + --hash=sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b +ffmpeg-python==0.2.0 ; python_version >= "3.11" \ + --hash=sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127 \ + --hash=sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5 +future==0.18.3 ; python_version >= "3.11" \ + --hash=sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307 +markdown-it-py==3.0.0 ; python_version >= "3.11" \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +mdurl==0.1.2 ; python_version >= "3.11" \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba +more-itertools==10.1.0 ; python_version >= "3.11" \ + --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \ + --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6 +pydantic-core==2.14.6 ; python_version >= "3.11" \ + --hash=sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556 \ + --hash=sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e \ + --hash=sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411 \ + --hash=sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245 \ + --hash=sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c \ + --hash=sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66 \ + --hash=sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd \ + --hash=sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d \ + --hash=sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b \ + --hash=sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06 \ + --hash=sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948 \ + --hash=sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341 \ + --hash=sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0 \ + --hash=sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f \ + --hash=sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a \ + --hash=sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2 \ + --hash=sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51 \ + --hash=sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80 \ + --hash=sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8 \ + --hash=sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d \ + --hash=sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8 \ + --hash=sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb \ + --hash=sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590 \ + --hash=sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87 \ + --hash=sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534 \ + --hash=sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b \ + --hash=sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145 \ + --hash=sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba \ + --hash=sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b \ + --hash=sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2 \ + --hash=sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e \ + --hash=sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052 \ + --hash=sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622 \ + --hash=sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab \ + --hash=sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b \ + --hash=sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66 \ + --hash=sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e \ + --hash=sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4 \ + --hash=sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e \ + --hash=sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec \ + --hash=sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c \ + --hash=sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed \ + --hash=sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937 \ + --hash=sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f \ + --hash=sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9 \ + --hash=sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4 \ + --hash=sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96 \ + --hash=sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277 \ + --hash=sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23 \ + --hash=sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7 \ + --hash=sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b \ + --hash=sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91 \ + --hash=sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d \ + --hash=sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e \ + --hash=sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1 \ + --hash=sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2 \ + --hash=sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160 \ + --hash=sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9 \ + --hash=sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670 \ + --hash=sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7 \ + --hash=sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c \ + --hash=sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb \ + --hash=sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42 \ + --hash=sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d \ + --hash=sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8 \ + --hash=sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1 \ + --hash=sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6 \ + --hash=sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8 \ + --hash=sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf \ + --hash=sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e \ + --hash=sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a \ + --hash=sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9 \ + --hash=sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1 \ + --hash=sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40 \ + --hash=sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2 \ + --hash=sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d \ + --hash=sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f \ + --hash=sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f \ + --hash=sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af \ + --hash=sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7 \ + --hash=sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda \ + --hash=sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a \ + --hash=sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95 \ + --hash=sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0 \ + --hash=sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60 \ + --hash=sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149 \ + --hash=sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975 \ + --hash=sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4 \ + --hash=sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe \ + --hash=sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94 \ + --hash=sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03 \ + --hash=sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c \ + --hash=sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b \ + --hash=sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a \ + --hash=sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24 \ + --hash=sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391 \ + --hash=sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c \ + --hash=sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab \ + --hash=sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd \ + --hash=sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786 \ + --hash=sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08 \ + --hash=sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8 \ + --hash=sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6 \ + --hash=sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0 \ + --hash=sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421 +pydantic==2.5.3 ; python_version >= "3.11" \ + --hash=sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a \ + --hash=sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4 +pygments==2.17.2 ; python_version >= "3.11" \ + --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ + --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 +rich-argparse==1.4.0 ; python_version >= "3.11" \ + --hash=sha256:68b263d3628d07b1d27cfe6ad896da2f5a5583ee2ba226aeeb24459840023b38 \ + --hash=sha256:c275f34ea3afe36aec6342c2a2298893104b5650528941fb53c21067276dba19 +rich==13.7.0 ; python_version >= "3.11" \ + --hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \ + --hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235 +typing-extensions==4.9.0 ; python_version >= "3.11" \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd diff --git a/src/BAET/AudioExtractor.py b/src/BAET/AudioExtractor.py deleted file mode 100644 index ecd9c42..0000000 --- a/src/BAET/AudioExtractor.py +++ /dev/null @@ -1,162 +0,0 @@ -import contextlib -import getpass -import sys -from pathlib import Path -from subprocess import Popen - -import ffmpeg -from ffmpeg.nodes import InputNode -from rich.markdown import Markdown - -from BAET.AppArgs import AppArgs -from BAET.Console import console, error_console -from BAET.Logging import info_logger - - -def print_ffmpeg_cmd(output): - compiled = " ".join(ffmpeg.compile(output)) - - md_user = f"{getpass.getuser()}:~$" - cmd = Markdown(f"```console\n{md_user} {compiled}\n```") - - console.print(cmd) - console.print() - - -@contextlib.contextmanager -def probe_audio_streams(file: Path): - try: - info_logger.info('Probing "%s"', file) - probe = ffmpeg.probe(file) - - audio_streams = [ - stream - for stream in probe["streams"] - if "codec_type" in stream and stream["codec_type"] == "audio" - ] - - if not audio_streams: - raise ValueError("No audio streams found") - - info_logger.info("Found %d audio streams", len(audio_streams)) - yield audio_streams - - except (ffmpeg.Error, ValueError) as e: - info_logger.critical("%s: %s", type(e).__name__, e) - error_console.print_exception() - raise e - - -class AudioExtractor: - def __init__( - self, - file: Path, - app_args: AppArgs, - ): - self.file = file - self.output_dir = app_args.output_dir - self.input_filters = app_args.input_filters - self.output_configuration = app_args.output_configuration - self.debug_options = app_args.debug_options - - self.output_dir = ( - self.output_dir - if not self.output_configuration.no_output_subdirs - else self.output_dir / self.file.stem - ) - - def extract(self): - ffmpeg_input: InputNode = ffmpeg.input(self.file) - workers = [] - - with probe_audio_streams(self.file) as audio_streams: - # Check: does the indexing of audio stream relate to ffprobe index - # or to the index of the collected audio streams in ffmpeg_input["a:index"]? - for index, stream in enumerate(audio_streams): - # index = stream["index"] - - out = self._create_workers( - ffmpeg_input, - index, - stream["sample_rate"] or self.output_configuration.sample_rate, - ) - - workers.append(out) - - info_logger.info("Extracting audio to %s", self.output_dir) - - # if not self.output_configuration.output_streams_separately: - # output = ffmpeg.merge_outputs(*worker_pairs) - # self._run(output) - # return - # - # for output in worker_pairs: - # self._run(output) - - try: - for output in workers: - self._run_workers(output) - except Exception as e: - info_logger.critical("%s: %s", type(e).__name__, e) - error_console.print_exception() - raise e - - def _run_workers(self, ffmpeg_output): - # if self.debug_options.show_ffmpeg_cmd: - # print_ffmpeg_cmd(ffmpeg_write) - - if self.debug_options.dry_run: - return - - proc: Popen[bytes] = ffmpeg_output.run_async(pipe_stdout=True) - - while proc.poll() is None: - output = proc.stdout.readline().decode("utf-8") - console.print(output) - - if proc.returncode != 0: - info_logger.critical("Return code: %s", proc.returncode) - sys.exit(-1) - # todo: hangs here :( - - # progress_text = input_data.decode("utf-8") - # console.print(progress_text) - - # proc_write.stdin.write(input_data) - - # Look for "frame=xx" - # if progress_text.startswith("frame="): - # frame = int(progress_text.partition("=")[-1]) # Get the frame number - # console.print(f"q: {frame}") - - proc.wait() - - def _create_workers( - self, ffmpeg_input: InputNode, stream_index: int, sample_rate: int - ): - self.output_dir.mkdir(parents=True, exist_ok=True) - - output_filename = Path( - f"{self.file.stem}_track{stream_index}.{self.output_configuration.file_type}" - ) - - # TODO: MOVE - info_logger.info( - "Creating output dir %s", self.output_dir / output_filename.stem - ) - - opt_kwargs = { - "acodec": self.output_configuration.acodec, - "audio_bitrate": sample_rate - or self.output_configuration.fallback_sample_rate, - "format": self.output_configuration.file_type, - } - - output = ffmpeg_input[f"a:{stream_index}"].output( - str(output_filename), **opt_kwargs - ) - - if self.output_configuration.overwrite_existing: - output = output.overwrite_output() - - return output \ No newline at end of file diff --git a/src/BAET/Console.py b/src/BAET/Console.py deleted file mode 100644 index 02c379f..0000000 --- a/src/BAET/Console.py +++ /dev/null @@ -1,6 +0,0 @@ -from rich.console import Console - -from BAET.Theme import app_theme - -console = Console(theme=app_theme) -error_console = Console(stderr=True, style="bold red") diff --git a/src/BAET/FFmpegExtract/CommandBuilder.py b/src/BAET/FFmpegExtract/CommandBuilder.py deleted file mode 100644 index 10f289b..0000000 --- a/src/BAET/FFmpegExtract/CommandBuilder.py +++ /dev/null @@ -1,126 +0,0 @@ -import contextlib -import getpass -from pathlib import Path - -import ffmpeg -from ffmpeg.nodes import Stream -from rich.markdown import Markdown - -from BAET.AppArgs import AppArgs -from BAET.Console import console, error_console -from BAET.FFmpegExtract.FFmpegProcess import FFmpegProcessGroup, ProbedOutput -from BAET.Logging import info_logger - - -@contextlib.contextmanager -def probe_audio_streams(file: Path): - try: - info_logger.info('Probing "%s"', file) - probe = ffmpeg.probe(file) - - audio_streams = [ - stream - for stream in probe["streams"] - if "codec_type" in stream and stream["codec_type"] == "audio" - ] - - if not audio_streams: - raise ValueError("No audio streams found") - - info_logger.info("Found %d audio streams", len(audio_streams)) - yield audio_streams - - except (ffmpeg.Error, ValueError) as e: - info_logger.critical("%s: %s", type(e).__name__, e) - error_console.print_exception() - raise e - - -def print_ffmpeg_cmd(output: Stream): - compiled = " ".join(ffmpeg.compile(output)) - - md_user = f"{getpass.getuser()}:~$" - cmd = Markdown(f"```console\n{md_user} {compiled}\n```") - - console.print(cmd) - console.print() - - -class FFmpegProcBuilder: - def __init__(self, app_args: AppArgs): - self.input_dir = app_args.input_dir - self.output_dir = app_args.output_dir - self.input_filters = app_args.input_filters - self.output_configuration = app_args.output_configuration - self.debug_options = app_args.debug_options - - def __call__(self, file: Path): - output = self.build_args(file) - - # for output in outputs: - # if self.debug_options.show_ffmpeg_cmd: - # print_ffmpeg_cmd(output) - - # TODO - output.run_synchronously() - - def create_output_subdir(self, file: Path): - return ( - self.output_dir - if not self.output_configuration.no_output_subdirs - else self.output_dir / file.stem - ) - - def create_output_filepath( - self, file: Path, stream_index: int, sample_rate: int | None - ) -> Path: - filename = Path( - f"{file.stem}_track{stream_index}.{self.output_configuration.file_type}" - ) - - out_path = ( - self.output_dir - if self.output_configuration.no_output_subdirs - else self.output_dir / file.stem - ) - - info_logger.info('Preparing to write "%s" to "%s"', filename, out_path) - - out_path.mkdir(parents=True, exist_ok=True) - return out_path / filename - - def create_outputs_for_file( - self, - file: Path, - probe_info: list[dict], - ) -> list[ProbedOutput]: - ffmpeg_input = ffmpeg.input(str(file)) - - outputs: list[ProbedOutput] = [] - for idx, stream in enumerate(probe_info): - sample_rate = ( - stream.get("sample_rate", None) - or self.output_configuration.fallback_sample_rate - ) - - output = ffmpeg.output( - ffmpeg_input[f"a:{idx}"], - str(self.create_output_filepath(file, stream["index"], sample_rate)), - acodec=self.output_configuration.acodec, - audio_bitrate=sample_rate - or self.output_configuration.fallback_sample_rate, - ) - - outputs.append(ProbedOutput(output, stream)) - - return outputs - - def build_args(self, file: Path) -> FFmpegProcessGroup: - if not file.resolve(strict=True).is_file(): - raise FileNotFoundError(f'The file "{file}" does not exist') - - with probe_audio_streams(file) as audio_streams: - return FFmpegProcessGroup( - self.create_outputs_for_file(file, audio_streams), - self.output_configuration.overwrite_existing, - ) \ No newline at end of file diff --git a/src/BAET/FFmpegExtract/FFmpegProcess.py b/src/BAET/FFmpegExtract/FFmpegProcess.py deleted file mode 100644 index c1ba833..0000000 --- a/src/BAET/FFmpegExtract/FFmpegProcess.py +++ /dev/null @@ -1,86 +0,0 @@ -from fractions import Fraction - -import ffmpeg -from rich.progress import Progress - -from BAET.Console import console, error_console -from BAET.Logging import info_logger - - -class ProbedOutput: - def __init__(self, ffmpeg_output, stream: dict): - self.output = ffmpeg_output - self.stream = stream - - def get_stream_index(self): - return self.stream["index"] - - -class FFmpegProcess: - def __init__( - self, - output, - probe_stream, - overwrite: bool, - ): - self.ffmpeg_output = output.global_args("-progress", "-", "-nostats") - self.probe_stream = probe_stream - self.overwrite = overwrite - - def run_synchronously(self, progress_handle) -> None: - proc = ffmpeg.run_async( - self.ffmpeg_output, - overwrite_output=self.overwrite, - pipe_stdout=True, - pipe_stderr=True, - ) - - try: - while proc.poll() is None: - output = proc.stdout.readline().decode("utf-8").strip() - if "out_time_ms" in output: - val = output.split("=", 1)[1] - progress_handle( - float(val) / 1_000_000, - desc=f"Stream index: {self.probe_stream['index']}", - ) - - if proc.returncode != 0: - # TODO - raise Exception("Unknown Error") - except Exception as e: - info_logger.critical("%s: %s", type(e).__name__, e) - error_console.print_exception() - raise e - - -class FFmpegProcessGroup: - def __init__( - self, - probed_outputs: list[ProbedOutput], - overwrite: bool, - ): - self.outputs = [ - FFmpegProcess( - output.output, - output.stream, - overwrite, - ) - for output in probed_outputs - ] - - def run_synchronously(self) -> None: - with Progress(console=console) as progress: - for output in self.outputs: - task = progress.add_task( - f"Preparing...", - total=float(output.probe_stream["duration_ts"]) - * float(Fraction(output.probe_stream["time_base"])), - ) - - def update_task(prog: int, desc: str | None = None): - progress.update( - task, completed=prog, description=desc, refresh=True - ) - - output.run_synchronously(update_task) \ No newline at end of file diff --git a/src/BAET/FFmpegExtract/__init__.py b/src/BAET/FFmpegExtract/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/BAET/Helpers/Printing.py b/src/BAET/Helpers/Printing.py deleted file mode 100644 index 7a64a47..0000000 --- a/src/BAET/Helpers/Printing.py +++ /dev/null @@ -1,21 +0,0 @@ -from more_itertools import consecutive_groups - - -def find_missing(lst): - # src: https://www.geeksforgeeks.org/python-find-missing-numbers-in-a-sorted-list-range/ - return sorted(set(range(lst[0], lst[-1])) - set(lst)) - - -def pretty_range(*nums: int): - ranges = consecutive_groups(nums) - - outputs = [] - for nums in ranges: - nums = list(nums) - - if len(nums) == 1: - outputs.append(str(nums[0])) - continue - - outputs.append(f"{min(nums)}-{max(nums)}") - return ", ".join(outputs) \ No newline at end of file diff --git a/src/BAET/Helpers/__init__.py b/src/BAET/Helpers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/BAET/Logging.py b/src/BAET/Logging.py deleted file mode 100644 index c7ceee8..0000000 --- a/src/BAET/Logging.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging - -from rich.logging import RichHandler - -__ALL__ = ["info_logger"] - -logging.basicConfig( - level=logging.INFO, - format="%(message)s", - datefmt="[%X]", - handlers=[RichHandler(rich_tracebacks=True)], -) - -info_logger = logging.getLogger("info_logger") - - -class LevelFilter(logging.Filter): - def __init__(self, loglevel: int) -> None: - super().__init__() - self.loglevel = loglevel - - def filter(self, record): - return record.levelno == self.loglevel - - -info_logger.addFilter(LevelFilter(logging.INFO)) diff --git a/src/BAET/Types.py b/src/BAET/Types.py deleted file mode 100644 index 43ef7a0..0000000 --- a/src/BAET/Types.py +++ /dev/null @@ -1,103 +0,0 @@ -import re -from re import Pattern - -from pydantic import BaseModel, ConfigDict, DirectoryPath, Field, field_validator -from typing_extensions import Annotated - - -file_type_pattern = re.compile(r"^\.?(\w+)$") - - -class AppVersion: - def __init__( - self, - major, - minor=None, - patch=None, - prerelease=None, - build=None, - ) -> None: - self.major = major - self.minor = minor or "0" - self.patch = patch or "0" - self.prerelease = prerelease - self.build = build - - def __repr__(self) -> str: - prerelease = f"-{self.prerelease}" if self.prerelease else "" - build = f"+{self.build}" if self.build else "" - return f"{self.major}.{self.minor}.{self.patch}{prerelease}{build}" - - -class InputFilters(BaseModel): - include: Pattern | None = Field(...) - exclude: Pattern | None = Field(...) - - @field_validator("include", mode="before") - @classmethod - def validate_include_nonempty(cls, v: str): - if not v or not str.strip(v): - return ".*" - return v - - @field_validator("exclude", mode="before") - @classmethod - def validate_exclude_nonempty(cls, v: str): - if not v or not str.strip(v): - return None - return v - - @field_validator("include", "exclude", mode="before") - @classmethod - def compile_to_pattern(cls, v: str): - if not v: - return None - if isinstance(v, str): - return re.compile(v) - else: - return v - - -class OutputConfigurationOptions(BaseModel): - output_streams_separately: bool = Field(...) - overwrite_existing: bool = Field(...) - no_output_subdirs: bool = Field(...) - acodec: str = Field(...) - fallback_sample_rate: Annotated[int, Field(gt=0)] = Field(...) - file_type: str = Field(...) - - @field_validator("file_type", mode="before") - @classmethod - def validate_file_type(cls, v: str): - matched = file_type_pattern.match(v) - if matched: - return matched.group(1) - raise ValueError(f"Invalid file type: {v}") - - -class DebugOptions(BaseModel): - logging: bool = Field(...) - dry_run: bool = Field(...) - trim: Annotated[int, Field(gt=0)] | None = Field(...) - print_args: bool = Field(...) - show_ffmpeg_cmd: bool = Field(...) - run_synchronously: bool = Field(...) - - -class AppArgs(BaseModel): - """Application commandline arguments. - - Raises: - ValueError: The provided path is not a directory. - - Returns: - DirectoryPath: Validated directory path. - """ - - model_config = ConfigDict(frozen=True, from_attributes=True) - - input_dir: DirectoryPath = Field(...) - output_dir: DirectoryPath = Field(...) - input_filters: InputFilters = Field(...) - output_configuration: OutputConfigurationOptions = Field(...) - debug_options: DebugOptions = Field(...) \ No newline at end of file diff --git a/src/BAET/__init__.py b/src/BAET/__init__.py index e69de29..4220a00 100644 --- a/src/BAET/__init__.py +++ b/src/BAET/__init__.py @@ -0,0 +1,5 @@ +from ._console import app_console +from ._logging import create_logger +from ._theme import app_theme + +__all__ = ["app_console", "create_logger", "app_theme"] diff --git a/src/BAET/__main__.py b/src/BAET/__main__.py new file mode 100644 index 0000000..63887fa --- /dev/null +++ b/src/BAET/__main__.py @@ -0,0 +1,35 @@ +import logging +import sys + +import rich +from rich.live import Live +from rich.traceback import install + +from BAET import app_console, create_logger +from BAET.app_args import get_args +from BAET.extract import MultiTrackAudioBulkExtractor + +install(show_locals=True) + +VIDEO_EXTENSIONS = [".mp4", ".mkv"] + + +def main() -> None: + args = get_args() + + if args.debug_options.print_args: + rich.print(args) + sys.exit(0) + + if not args.debug_options.logging: + logging.disable(logging.CRITICAL) + + logger = create_logger() + logger.info("Building extractor jobs") + extractor = MultiTrackAudioBulkExtractor(args) + + with Live(extractor, console=app_console): + logger.info("Running jobs") + extractor.run_synchronously() + + sys.exit(0) diff --git a/src/BAET/_aliases.py b/src/BAET/_aliases.py new file mode 100644 index 0000000..1ebbe23 --- /dev/null +++ b/src/BAET/_aliases.py @@ -0,0 +1,18 @@ +from collections.abc import Mapping +from typing import Any, TypeAlias + +from bidict import BidirectionalMapping +from ffmpeg import Stream +from rich.progress import TaskID + +# Numbers +Millisecond: TypeAlias = int | float + +# FFmpeg +StreamIndex: TypeAlias = int +AudioStream: TypeAlias = dict[str, Any] + +# Mappings +IndexedOutputs: TypeAlias = Mapping[StreamIndex, Stream] +IndexedAudioStream: TypeAlias = Mapping[StreamIndex, AudioStream] +StreamTaskBiMap: TypeAlias = BidirectionalMapping[StreamIndex, TaskID] diff --git a/src/BAET/_console.py b/src/BAET/_console.py new file mode 100644 index 0000000..50f8c9b --- /dev/null +++ b/src/BAET/_console.py @@ -0,0 +1,6 @@ +from rich.console import Console + +from BAET._theme import app_theme + +app_console = Console(theme=app_theme) +error_console = Console(stderr=True, style="bold red", theme=app_theme) diff --git a/src/BAET/_constants.py b/src/BAET/_constants.py new file mode 100644 index 0000000..7a53143 --- /dev/null +++ b/src/BAET/_constants.py @@ -0,0 +1,5 @@ +from typing import Literal, Sequence, TypeAlias + +VideoExtension: TypeAlias = Literal[".mp4", ".mkv"] + +VIDEO_EXTENSIONS: Sequence[VideoExtension] = [".mp4", ".mkv"] diff --git a/src/BAET/_logging.py b/src/BAET/_logging.py new file mode 100644 index 0000000..9233a21 --- /dev/null +++ b/src/BAET/_logging.py @@ -0,0 +1,30 @@ +import inspect +import logging +from logging import Logger + +from rich.logging import RichHandler + +from BAET._console import app_console + +rich_handler = RichHandler(rich_tracebacks=True, console=app_console) +logging.basicConfig( + level=logging.INFO, + format="%(threadName)s: %(message)s", + datefmt="[%X]", + handlers=[rich_handler], +) + +app_logger = logging.getLogger("app_logger") + + +def create_logger() -> Logger: + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + + if module is None: + raise RuntimeError("Could not inspect module") + + module_name = module.__name__ + + logger = app_logger.getChild(module_name) + return logger diff --git a/src/BAET/_metadata.py b/src/BAET/_metadata.py new file mode 100644 index 0000000..5ea2ce2 --- /dev/null +++ b/src/BAET/_metadata.py @@ -0,0 +1,5 @@ +import importlib.metadata + + +def app_version() -> str: + return importlib.metadata.version("BAET") diff --git a/src/BAET/Theme.py b/src/BAET/_theme.py similarity index 68% rename from src/BAET/Theme.py rename to src/BAET/_theme.py index 2cc9947..d64efad 100644 --- a/src/BAET/Theme.py +++ b/src/BAET/_theme.py @@ -8,5 +8,9 @@ "argparse.arg_default_value": "not italic bold dim", "argparse.help_keyword": "bold blue", "argparse.debug_todo": "reverse bold indian_red", + "status.waiting": "dim bright_white", + "status.running": "bright_cyan", + "status.completed": "bright_green", + "status.error": "bright_red", } ) diff --git a/src/BAET/AppArgs.py b/src/BAET/app_args.py similarity index 75% rename from src/BAET/AppArgs.py rename to src/BAET/app_args.py index df69311..db4cab1 100644 --- a/src/BAET/AppArgs.py +++ b/src/BAET/app_args.py @@ -1,8 +1,10 @@ import argparse +import re from argparse import ArgumentParser from pathlib import Path +from re import Pattern -from pydantic import DirectoryPath +from pydantic import BaseModel, ConfigDict, DirectoryPath, Field, field_validator from rich.console import Console, ConsoleOptions, RenderResult from rich.markdown import Markdown from rich.padding import Padding @@ -10,24 +12,62 @@ from rich.terminal_theme import DIMMED_MONOKAI from rich.text import Text from rich_argparse import HelpPreviewAction, RichHelpFormatter +from typing_extensions import Annotated -from BAET.Console import console -from BAET.Types import ( - AppArgs, - AppVersion, - DebugOptions, - InputFilters, - OutputConfigurationOptions, -) +from BAET._console import app_console +from BAET._metadata import app_version +file_type_pattern = re.compile(r"^\.?(\w+)$") -APP_VERSION = AppVersion(1, 0, 0, "alpha") + +class InputFilters(BaseModel): + include: Pattern[str] = Field(...) + exclude: Pattern[str] | None = Field(...) + + @field_validator("include", mode="before") + @classmethod + def validate_include_nonempty(cls, v: str) -> Pattern[str]: + if v is None or not v.strip(): + return re.compile(".*") + return re.compile(v) + + @field_validator("exclude", mode="before") + @classmethod + def validate_exclude_nonempty(cls, v: str) -> Pattern[str] | None: + if v is None or not v.strip(): + return None + return re.compile(v) + + +class OutputConfigurationOptions(BaseModel): + output_streams_separately: bool = Field(...) + overwrite_existing: bool = Field(...) + no_output_subdirs: bool = Field(...) + acodec: str = Field(...) + fallback_sample_rate: Annotated[int, Field(gt=0)] = Field(...) + file_type: str = Field(...) + + @field_validator("file_type", mode="before") + @classmethod + def validate_file_type(cls, v: str) -> str: + matched = file_type_pattern.match(v) + if matched: + return matched.group(1) + raise ValueError(f"Invalid file type: {v}") + + +class DebugOptions(BaseModel): + logging: bool = Field(...) + dry_run: bool = Field(...) + trim: Annotated[int, Field(gt=0)] | None = Field(...) + print_args: bool = Field(...) + show_ffmpeg_cmd: bool = Field(...) + run_synchronously: bool = Field(...) class AppDescription: - def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: + @staticmethod + def __rich_console__(console: Console, options: ConsoleOptions) -> RenderResult: yield Markdown("# Bulk Audio Extract Tool (src)") yield "Extract audio from a directory of videos using FFMPEG.\n" @@ -43,7 +83,7 @@ def __rich_console__( ), ( Padding(Text("Version:", justify="right"), (0, 5, 0, 0)), - Text(str(APP_VERSION), style="app.version", justify="left"), + Text(app_version(), style="app.version", justify="left"), ), ( Padding(Text("Author:", justify="right"), (0, 5, 0, 0)), @@ -69,8 +109,8 @@ def __rich_console__( def new_empty_argparser() -> ArgumentParser: - def get_formatter(prog): - return RichHelpFormatter(prog, max_help_position=40, console=console) + def get_formatter(prog: str) -> RichHelpFormatter: + return RichHelpFormatter(prog, max_help_position=40, console=app_console) # todo: use console protocol https://rich.readthedocs.io/en/stable/protocol.html#console-protocol description = AppDescription() @@ -80,28 +120,46 @@ def get_formatter(prog): ) RichHelpFormatter.highlights.append(r"(?Pffmpeg|ffprobe)") - RichHelpFormatter.highlights.append(r"(?P\[TODO\])") return argparse.ArgumentParser( prog="Bulk Audio Extract Tool (src)", description=description, # type: ignore - epilog=Markdown( + epilog=Markdown( # type: ignore "Phillip Smith, 2023", justify="right", style="argparse.prog", - ), # type: ignore + ), formatter_class=get_formatter, ) -def GetArgs() -> AppArgs: +class AppArgs(BaseModel): + """Application commandline arguments. + + Raises: + ValueError: The provided path is not a directory. + + Returns: + DirectoryPath: Validated directory path. + """ + + model_config = ConfigDict(frozen=True, from_attributes=True) + + input_dir: DirectoryPath = Field(...) + output_dir: DirectoryPath = Field(...) + input_filters: InputFilters = Field(...) + output_configuration: OutputConfigurationOptions = Field(...) + debug_options: DebugOptions = Field(...) + + +def get_args() -> AppArgs: parser = new_empty_argparser() parser.add_argument( "--version", action="version", - version=f"[argparse.prog]%(prog)s[/] version [i]{APP_VERSION}[/]", + version=f"[argparse.prog]%(prog)s[/] version [i]{app_version()}[/]", ) io_group = parser.add_argument_group( @@ -257,9 +315,7 @@ def GetArgs() -> AppArgs: "--generate-help-preview", action=HelpPreviewAction, path="help-preview.svg", # (optional) or "help-preview.html" or "help-preview.txt" - export_kwds={ - "theme": DIMMED_MONOKAI - }, # (optional) keywords passed to console.save_... methods + export_kwds={"theme": DIMMED_MONOKAI}, # (optional) keywords passed to console.save_... methods help=argparse.SUPPRESS, ) @@ -285,7 +341,7 @@ def GetArgs() -> AppArgs: run_synchronously=args.run_synchronously, ) - app_args = AppArgs.model_validate( + app_args: AppArgs = AppArgs.model_validate( { "input_dir": args.input_dir.expanduser(), "output_dir": args.output_dir or args.input_dir.expanduser(), @@ -296,4 +352,4 @@ def GetArgs() -> AppArgs: strict=True, ) - return app_args \ No newline at end of file + return app_args diff --git a/src/BAET/extract.py b/src/BAET/extract.py new file mode 100644 index 0000000..0b61529 --- /dev/null +++ b/src/BAET/extract.py @@ -0,0 +1,149 @@ +import contextlib +from collections.abc import Generator, Iterator, MutableMapping +from pathlib import Path +from typing import Any + +import ffmpeg +from ffmpeg import Stream +from rich.console import Console, ConsoleOptions, Group, RenderResult +from rich.padding import Padding +from rich.table import Table + +from ._aliases import AudioStream +from ._console import error_console +from ._constants import VIDEO_EXTENSIONS +from ._logging import create_logger +from .app_args import AppArgs, InputFilters, OutputConfigurationOptions +from .job_progress import FFmpegJobProgress +from .jobs import FFmpegJob + +logger = create_logger() + + +@contextlib.contextmanager +def probe_audio_streams(file: Path) -> Iterator[list[AudioStream]]: + try: + logger.info('Probing file "%s"', file) + probe = ffmpeg.probe(file) + + audio_streams = sorted( + [stream for stream in probe["streams"] if "codec_type" in stream and stream["codec_type"] == "audio"], + key=lambda stream: stream["index"], + ) + + if not audio_streams: + raise ValueError("No audio streams found") + + logger.info("Found %d audio streams", len(audio_streams)) + yield audio_streams + + except (ffmpeg.Error, ValueError) as e: + logger.critical("%s: %s", type(e).__name__, e) + error_console.print_exception() + raise e + + +class FileSourceDirectory: + def __init__(self, directory: Path, filters: InputFilters): + if not directory.is_dir(): + raise NotADirectoryError(directory) # FIXME: Is this right? + + self._directory = directory.resolve(strict=True) + self._filters = filters + + def __iter__(self) -> Generator[Path, Any, None]: + for file in self._directory.iterdir(): + if not file.is_file(): + continue + + # TODO: This should be moved. + # Rather than checking a global variable, it should be provided somehow. + # Perhaps via command line args. + if file.suffix not in VIDEO_EXTENSIONS: + continue + + if self._filters.include.match(file.name): + if self._filters.exclude is None: + yield file + elif self._filters.exclude.match(file.name): + yield file + + +class MultitrackAudioBulkExtractorJobs: + def __init__( + self, + input_dir: Path, + output_dir: Path, + filters: InputFilters, + output_configuration: OutputConfigurationOptions, + ): + self._input_dir = input_dir + self._output_dir = output_dir + self._filters = filters + self._output_configuration = output_configuration + self._file_source_directory = FileSourceDirectory(input_dir, filters) + + def _create_output_filepath(self, file: Path, stream_index: int) -> Path: + filename = Path(f"{file.stem}_track{stream_index}.{self._output_configuration.file_type}") + + out_path = self._output_dir if self._output_configuration.no_output_subdirs else self._output_dir / file.stem + + out_path.mkdir(parents=True, exist_ok=True) + return out_path / filename + + def build_job(self, file: Path) -> FFmpegJob: + audio_streams: list[AudioStream] = [] + indexed_outputs: MutableMapping[int, Stream] = {} + + with probe_audio_streams(file) as streams: + for idx, stream in enumerate(streams): + audio_streams.append(stream) + + ffmpeg_input = ffmpeg.input(str(file)) + stream_index = stream["index"] + output_path = self._create_output_filepath(file, stream_index) + sample_rate = stream.get( + "sample_rate", + self._output_configuration.fallback_sample_rate, + ) + + ffmpeg_output = ffmpeg.output( + ffmpeg_input[f"a:{idx}"], + str(output_path), + acodec=self._output_configuration.acodec, + audio_bitrate=sample_rate, + ) + + if self._output_configuration.overwrite_existing: + ffmpeg_output = ffmpeg.overwrite_output(ffmpeg_output) + + indexed_outputs[stream_index] = ffmpeg_output.global_args("-progress", "-", "-nostats") + + return FFmpegJob(file, audio_streams, indexed_outputs) + + def __iter__(self) -> Generator[FFmpegJob, Any, None]: + yield from map(self.build_job, self._file_source_directory) + + +class MultiTrackAudioBulkExtractor: + def __init__(self, app_args: AppArgs) -> None: + self._extractor_jobs = MultitrackAudioBulkExtractorJobs( + app_args.input_dir, + app_args.output_dir, + app_args.input_filters, + app_args.output_configuration, + ) + + self.display = Table.grid() + self._logger = logger + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield self.display + + def run_synchronously(self) -> None: + job_progresses = [FFmpegJobProgress(job) for job in self._extractor_jobs] + self.display.add_row(Padding(Group(*job_progresses), pad=(1, 2))) + + for progress in job_progresses: + self._logger.info(f"Processing input file '{progress.job.input_file}'") + progress.start() diff --git a/src/BAET/job_progress.py b/src/BAET/job_progress.py new file mode 100644 index 0000000..9ba75fa --- /dev/null +++ b/src/BAET/job_progress.py @@ -0,0 +1,123 @@ +import ffmpeg +from bidict import MutableBidirectionalMapping, bidict +from rich.console import Console, ConsoleOptions, ConsoleRenderable, Group, RenderResult +from rich.padding import Padding +from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn, TimeRemainingColumn + +from ._aliases import StreamTaskBiMap +from ._console import app_console, error_console +from ._logging import create_logger +from .jobs import FFmpegJob + +logger = create_logger() + + +class FFmpegJobProgress(ConsoleRenderable): + # TODO: Need mediator to consumer/producer printing + def __init__(self, job: FFmpegJob): + self.job = job + + bar_blue = "#5079AF" + bar_yellow = "#CAAF39" + + self._overall_progress = Progress( + TextColumn("Progress for {task.fields[filename]}"), + BarColumn( + complete_style=bar_blue, + finished_style="green", + pulse_style=bar_yellow, + ), + TextColumn("Completed {task.completed} of {task.total}"), + TimeElapsedColumn(), + TimeRemainingColumn(), + console=app_console, + ) + + self._overall_progress_task = self._overall_progress.add_task( + "Waiting...", + start=False, + filename=job.input_file.name, + total=len(self.job.audio_streams), + ) + + self._stream_task_progress = Progress( + TextColumn("Audio Stream {task.fields[stream_index]}"), + BarColumn( + complete_style=bar_blue, + finished_style="green", + pulse_style=bar_yellow, + ), + TextColumn("{task.fields[status]}"), + TimeElapsedColumn(), + TimeRemainingColumn(), + console=app_console, + ) + + self._stream_task_bimap: StreamTaskBiMap = bidict() + + stream_task_bimap: MutableBidirectionalMapping[int, TaskID] = bidict() + for stream in self.job.audio_streams: + stream_index = stream["index"] + + task = self._stream_task_progress.add_task( + "Waiting...", + start=False, + total=self.job.stream_duration_ms(stream), + stream_index=stream_index, + status="Waiting", + ) + + stream_task_bimap[stream_index] = task + + self._stream_task_bimap = stream_task_bimap + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield Group( + self._overall_progress, + Padding(self._stream_task_progress, (1, 0, 1, 5)), + ) + + def _run_task(self, task: TaskID) -> None: + stream_index = self._stream_task_bimap.inverse[task] + + logger.info(f"Extracting audio stream {stream_index} of {self.job.input_file.name}") + + output = self.job.indexed_outputs[stream_index] + + proc = ffmpeg.run_async( + output, + pipe_stdout=True, + pipe_stderr=True, + ) + + try: + with proc as p: + for line in p.stdout: + decoded = line.decode("utf-8").strip() + if "out_time_ms" in decoded: + val = decoded.split("=", 1)[1] + self._stream_task_progress.update( + task, + completed=float(val), + ) + + if p.returncode != 0: + raise RuntimeError(p.stderr.read().decode("utf-8")) + except RuntimeError as e: + logger.critical("%s: %s", type(e).__name__, e) + error_console.print_exception() + raise e + + def start(self) -> None: + self._overall_progress.start_task(self._overall_progress_task) + for task in self._stream_task_bimap.values(): + self._stream_task_progress.start_task(task) + self._stream_task_progress.update(task, status="Working") + + self._run_task(task) + + self._stream_task_progress.update(task, status="Done") + self._stream_task_progress.stop_task(task) + self._overall_progress.advance(self._overall_progress_task, advance=1) + + self._overall_progress.stop_task(self._overall_progress_task) diff --git a/src/BAET/jobs.py b/src/BAET/jobs.py new file mode 100644 index 0000000..e0a6869 --- /dev/null +++ b/src/BAET/jobs.py @@ -0,0 +1,44 @@ +from fractions import Fraction +from pathlib import Path + +from more_itertools import first_true + +from ._aliases import AudioStream, IndexedAudioStream, IndexedOutputs, Millisecond, StreamIndex + + +class FFmpegJob: + def __init__( + self, + input_file: Path, + audio_streams: list[AudioStream], + indexed_outputs: IndexedOutputs, + ): + self.input_file: Path = input_file + self.indexed_outputs: IndexedOutputs = indexed_outputs + self.audio_streams = audio_streams + + indexed_audio_streams = {} + for stream in audio_streams: + indexed_audio_streams[stream["index"]] = stream + self.indexed_audio_streams: IndexedAudioStream = indexed_audio_streams + + # TODO: Do we need this? + self.durations_ms_dict: dict[StreamIndex, Millisecond] = { + stream["index"]: self.stream_duration_ms(stream) for stream in audio_streams + } + + @classmethod + def stream_duration_ms(cls, stream: AudioStream) -> Millisecond: + return 1_000_000 * float(stream["duration_ts"]) * float(Fraction(stream["time_base"])) + + def stream(self, index: StreamIndex) -> AudioStream: + stream: AudioStream | None = first_true( + self.audio_streams, + default=None, + pred=lambda st: st["index"] == index, # type: ignore + ) + + if stream is None: + raise IndexError(f'Stream with index "{index}" not found') + + return stream diff --git a/src/BAET/progress_list.py b/src/BAET/progress_list.py new file mode 100644 index 0000000..52219d3 --- /dev/null +++ b/src/BAET/progress_list.py @@ -0,0 +1,46 @@ +from rich.console import Console +from rich.progress import Progress, TextColumn + +from .progress_status import ProgressStatus +from .progress_style import ProgressStyle + + +class ProgressCheckList: + def __init__( + self, + waiting_description: str, + running_description: str, + completed_description: str, + error_description: str, + progress_style: ProgressStyle | None = None, + console: Console | None = None, + ): + self.descriptions: dict[ProgressStatus, str] = { + ProgressStatus.Waiting: waiting_description, + ProgressStatus.Running: running_description, + ProgressStatus.Completed: completed_description, + ProgressStatus.Error: error_description, + } + + self.progress_style = progress_style or ProgressStyle() + + self.overall_progress = Progress( + TextColumn("{task.description}"), + console=console, + ) + + self.overall_progress_task = self.overall_progress.add_task( + waiting_description, + total=None, + start=False, + ) + + def add_item( + self, + waiting_description: str, + running_description: str, + completed_description: str, + error_description: str, + ) -> None: + # TODO + pass diff --git a/src/BAET/progress_status.py b/src/BAET/progress_status.py new file mode 100644 index 0000000..447d254 --- /dev/null +++ b/src/BAET/progress_status.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from enum import StrEnum +from typing import Literal, TypeAlias + +from rich.console import Console, ConsoleOptions, RenderResult +from rich.repr import Result + +ProgressStatusLiteral: TypeAlias = Literal["Waiting", "Running", "Completed", "Error"] + + +class ProgressStatus(StrEnum): + Waiting = "Waiting" + Running = "Running" + Completed = "Completed" + Error = "Error" + + def __rich_repr__(self) -> Result: + yield repr(self) + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield repr(self) + + +ProgressStatusType: TypeAlias = ProgressStatus | ProgressStatusLiteral + +if __name__ == "__main__": + import rich + + waiting = ProgressStatus("Waiting") + completed = ProgressStatus(ProgressStatus.Completed) + + rich.print(waiting) + rich.print(completed) diff --git a/src/BAET/progress_style.py b/src/BAET/progress_style.py new file mode 100644 index 0000000..8aa526e --- /dev/null +++ b/src/BAET/progress_style.py @@ -0,0 +1,132 @@ +from collections.abc import Generator, Mapping +from typing import Any + +from rich.console import Console, ConsoleOptions, RenderResult +from rich.repr import Result +from rich.style import Style +from rich.text import Text + +from BAET._theme import app_theme +from BAET.progress_status import ProgressStatus, ProgressStatusLiteral, ProgressStatusType + + +def parse_style(style: Style | str) -> Style: + if isinstance(style, Style): + return style + + return Style.parse(style) + + +class ProgressStyle: + def __init__( + self, + style_dict: dict[ProgressStatusType, str | Style] | None = None, + *, + waiting_style: str | Style | None = None, + running_style: str | Style | None = None, + completed_style: str | Style | None = None, + error_style: str | Style | None = None, + ): + default_style_dict: dict[ProgressStatus, Style] = { + key: parse_style(value or default) + for key, value, default in [ + (ProgressStatus.Waiting, waiting_style, "dim bright_white"), + (ProgressStatus.Running, running_style, "bright_cyan"), + (ProgressStatus.Completed, completed_style, "bright_green"), + (ProgressStatus.Error, error_style, "bright_red"), + ] + } + + if style_dict is not None: + parsed_styles = {ProgressStatus(key): parse_style(val) for key, val in style_dict.items()} + + default_style_dict.update(parsed_styles) + + self.style_dict: Mapping[ProgressStatus, Style] = default_style_dict + + def __call__( + self, + text: str, + status: ProgressStatus | ProgressStatusLiteral, + *args: Any, + style: Style | None = None, + **kwargs: Any, + ) -> Text: + return Text( + text, + self.style_dict[ProgressStatus(status)] + style, + *args, + **kwargs, + ) + + def __rich_repr__(self) -> Result: + yield "style_dict", self.style_dict + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + with console.capture() as capture: + console.print(self.style_dict) + yield Text.from_ansi(capture.get()) + + +if __name__ == "__main__": + from rich.console import Group, group + from rich.padding import Padding + from rich.panel import Panel + from rich.pretty import pprint + + console = Console(theme=app_theme) + + basic_style = ProgressStyle( + {"Completed": "bold green", ProgressStatus.Error: "bold red"}, + running_style="italic bold cyan", + waiting_style="underline yellow", + ) + + theme_style = ProgressStyle( + { + ProgressStatus.Waiting: app_theme.styles["status.waiting"], + ProgressStatus.Running: app_theme.styles["status.running"], + ProgressStatus.Completed: app_theme.styles["status.completed"], + ProgressStatus.Error: app_theme.styles["status.error"], + }, + ) + + @group() + def apply_styles(style: ProgressStyle, message: str) -> Generator[Text, Any, None]: + for status in ["Waiting", "Completed", "Running", "Error"]: + yield style(message.format(status), status) # type: ignore + + @group() + def make_style_group(style: ProgressStyle, message: str) -> Generator[Text, Any, None]: + with console.capture() as pretty_capture: + pprint( + {key.value: val.color.name for key, val in style.style_dict.items() if val.color}, + console=console, + expand_all=True, + indent_guides=False, + ) + with console.capture() as repr_capture: + console.print( + style, + "\n[u]Demo Application:[/]", + Padding(apply_styles(style, message), pad=(0, 0, 0, 3)), + ) + + yield Text.from_ansi(f"Input Style: {pretty_capture.get()}\n") + yield Text.from_ansi(f"Resulting ProgressStyle: {repr_capture.get()}") + + layout = Group( + Panel( + make_style_group(basic_style, "This is the style for ProgressStyle.{0}"), + title="[bold green]Basic Style", + ), + Panel( + make_style_group( + theme_style, + 'This is the style for "ProgressStyle.{0}", taken from app_theme in BAET', + ), + title="[bold green]Theme Style", + ), + ) + + console.print(layout) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 89338b1..0000000 --- a/src/main.py +++ /dev/null @@ -1,45 +0,0 @@ -import sys - -import rich -from rich.traceback import install - -from BAET.AppArgs import GetArgs -from BAET.Console import console -from BAET.FFmpegExtract.CommandBuilder import FFmpegProcBuilder -from BAET.Logging import info_logger - - -install(show_locals=True) - -VIDEO_EXTENSIONS = [".mp4", ".mkv"] - - -def main(): - args = GetArgs() - - if not args.debug_options.logging: - info_logger.disabled = True - - if args.debug_options.print_args: - rich.print(args) - sys.exit(0) - - files = [ - file for file in args.input_dir.iterdir() if file.suffix in VIDEO_EXTENSIONS - ] - - if not files: - path = args.input_dir.absolute() - console.print(f'No video files found in "[link file://{path}]{path}[/]".') - sys.exit(0) - - ex = FFmpegProcBuilder(args) - for file in files: - info_logger.info("Processing input file '%s'", file) - ex(file) - - sys.exit(0) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..758c60d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +from rich.console import Console + +from BAET import app_theme + +test_console = Console(theme=app_theme) diff --git a/tests/test_progress_style.py b/tests/test_progress_style.py new file mode 100644 index 0000000..f198410 --- /dev/null +++ b/tests/test_progress_style.py @@ -0,0 +1,14 @@ +from rich.style import Style +from rich.text import Text + +from BAET.progress_status import ProgressStatus, ProgressStatusType +from BAET.progress_style import ProgressStyle + + +def test_call_applies_style() -> None: + waiting_style: dict[ProgressStatusType, str | Style] = {ProgressStatus.Running: "blue"} + input_str = "test message" + style = ProgressStyle(style_dict=waiting_style) + test_style = Style.parse(waiting_style[ProgressStatus.Running]) + + assert style(input_str, status="Running").markup == Text(input_str, style=test_style).markup