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