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/.gitignore b/.gitignore
new file mode 100644
index 0000000..6133544
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,235 @@
+# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
+# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,python
+# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,python
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
+
+# LSP config files
+pyrightconfig.json
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,python
+
+# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
+
+test audio
+.vscode
+files
+.idea
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
new file mode 100644
index 0000000..8f01a15
--- /dev/null
+++ b/README.md
@@ -0,0 +1,71 @@
+# Bulk Audio Extract Tool
+
+
+
+## About
+
+Bulk Audio Extract Tool (BAET) is a commandline tool to bulk export audio tracks from within a single directory.
+
+## Install
+
+### Requirements
+
+BAET will run on Windows, macOS, and Linux. Listed below the pre-installation requirements:
+
+- FFmpeg ([website](https://ffmpeg.org))
+- Python v3.11+ ([website](https://www.python.org))
+
+### Installing BAET
+
+Installation is done via `pip`.
+Depending on your platform, to call python, you may need to use the command `python` or `python3`.
+Typing `python3 --version` or `python --version` should display the currently installed python environment your PATH.
+For the remainder of this document, replace instances of `python` with the appropriate alias on your machine.
+
+To install the most recent stable release, use:
+
+```bash
+python -m pip install baet
+```
+
+For pre-releases, use:
+
+```bash
+python -m pip install baet --pre
+```
+
+To update/upgrade to a new version, use:
+
+```bash
+python -m pip install baet -U [--pre]
+```
+
+To verify your install, call
+
+```bash
+baet --version
+```
+
+## Usage
+
+Once installed, calling `baet --help` will display the general help screen, showing a list of options you can use.
+
+To simply extract the audio tracks of all videos in a directory `~/inputs`,
+and extract each into a subdirectory of `~/outputs`, call
+
+```bash
+baet -i "~/inputs" -o "~/outputs"
+```
+
+Unless you add the option `--no-subdirs`, a video `~/inputs/my_video.mp4` will have each audio track individually
+exported to an audio file located in `./outputs/my_video/`.
+
+### Note on the help screen
+
+Currently, the help screen contains descriptions starting with `[TODO]`.
+This indicates that the associated option may or may not be implemented fully or at all.
+
+## Known issues
+
+- The option `--no-subdirs` may cause BAET to misbehave if two files are generated with the same name,
+ unless the option `--overwrite` is given, in which case one file will be overwritten.
diff --git a/images/help-preview.svg b/images/help-preview.svg
new file mode 100644
index 0000000..59309f8
--- /dev/null
+++ b/images/help-preview.svg
@@ -0,0 +1,280 @@
+
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
new file mode 100644
index 0000000..5791a28
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,826 @@
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+]
+
+[[package]]
+name = "astroid"
+version = "3.0.2"
+description = "An abstract syntax tree for Python with inference support."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"},
+ {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"},
+]
+
+[[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"
+version = "23.12.1"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"},
+ {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"},
+ {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"},
+ {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"},
+ {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"},
+ {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"},
+ {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"},
+ {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"},
+ {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"},
+ {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"},
+ {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"},
+ {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"},
+ {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"},
+ {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"},
+ {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"},
+ {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"},
+ {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"},
+ {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"},
+ {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"},
+ {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"},
+ {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"},
+ {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+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"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "dill"
+version = "0.3.7"
+description = "serialize all of Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
+ {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
+]
+
+[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"
+description = "Python bindings for FFmpeg - with complex filtering support"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
+ {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
+]
+
+[package.dependencies]
+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"
+description = "Clean single-source support for Python 3 and 2"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+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"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+ {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.1.0"
+description = "More routines for operating on iterables, beyond itertools"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"},
+ {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"},
+]
+
+[[package]]
+name = "mypy"
+version = "1.8.0"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
+ {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
+ {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
+ {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
+ {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
+ {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
+ {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
+ {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
+ {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
+ {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
+ {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
+ {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
+ {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
+ {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
+ {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
+ {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
+ {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
+ {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
+ {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
+ {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
+ {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
+ {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
+ {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
+ {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
+ {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
+ {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
+ {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.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"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.1.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
+ {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
+]
+
+[package.extras]
+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"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"},
+ {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.14.6"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.14.6"
+description = ""
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"},
+ {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"},
+ {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"},
+ {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"},
+ {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"},
+ {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"},
+ {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"},
+ {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"},
+ {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"},
+ {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"},
+ {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"},
+ {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"},
+ {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"},
+ {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"},
+ {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"},
+ {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"},
+ {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"},
+ {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"},
+ {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"},
+ {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"},
+ {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"},
+ {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"},
+ {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"},
+ {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"},
+ {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"},
+ {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pygments"
+version = "2.17.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pylint"
+version = "3.0.3"
+description = "python code static checker"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"},
+ {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"},
+]
+
+[package.dependencies]
+astroid = ">=3.0.1,<=3.1.0-dev0"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+dill = [
+ {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"
+tomlkit = ">=0.10.1"
+
+[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"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
+ {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "rich-argparse"
+version = "1.4.0"
+description = "Rich help formatters for argparse and optparse"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "rich_argparse-1.4.0-py3-none-any.whl", hash = "sha256:68b263d3628d07b1d27cfe6ad896da2f5a5583ee2ba226aeeb24459840023b38"},
+ {file = "rich_argparse-1.4.0.tar.gz", hash = "sha256:c275f34ea3afe36aec6342c2a2298893104b5650528941fb53c21067276dba19"},
+]
+
+[package.dependencies]
+rich = ">=11.0.0"
+
+[[package]]
+name = "setuptools"
+version = "69.0.3"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {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"
+description = "Style preserving TOML library"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"},
+ {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"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+ {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.11"
+content-hash = "61dc16e8051da6ede25203efd6c51da28888958fb313ad06f5f20694df78ac16"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ac23e08
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,66 @@
+[tool.poetry]
+name = "BAET"
+version = "0.1.3"
+description = "A tool to bulk extract audio tracks from video using FFmpeg"
+authors = ["TimeTravelPenguin "]
+readme = "README.md"
+packages = [{ include = "BAET", from = "src" }]
+classifiers = [
+ "Environment :: Console",
+ "Topic :: Multimedia :: Sound/Audio",
+ "Topic :: Multimedia :: Sound/Audio :: Conversion",
+ "Topic :: Multimedia :: Video :: Conversion",
+ "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 = "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.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"
+
+[tool.black]
+line-length = 120
+
+[tool.isort]
+profile = "black"
+line_length = 120
+
+[tool.mypy]
+mypy_path = "src"
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.poetry-dynamic-versioning]
+enable = true
+metadata = false
+vcs = "git"
+
+[build-system]
+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/__init__.py b/src/BAET/__init__.py
new file mode 100644
index 0000000..5f53972
--- /dev/null
+++ b/src/BAET/__init__.py
@@ -0,0 +1,5 @@
+from ._console import app_console
+from ._logging import configure_logging, create_logger
+from ._theme import app_theme
+
+__all__ = ["app_console", "configure_logging", "create_logger", "app_theme"]
diff --git a/src/BAET/__main__.py b/src/BAET/__main__.py
new file mode 100644
index 0000000..217a6e2
--- /dev/null
+++ b/src/BAET/__main__.py
@@ -0,0 +1,40 @@
+import sys
+from datetime import datetime
+from pathlib import Path
+
+import rich
+from rich.live import Live
+from rich.traceback import install
+
+from BAET import app_console, configure_logging, create_logger
+from BAET.app_args import get_args
+from BAET.extract import MultiTrackAudioBulkExtractor
+
+install(show_locals=True)
+
+
+def main() -> None:
+ args = get_args()
+
+ if args.debug_options.print_args:
+ rich.print(args)
+ sys.exit(0)
+
+ if args.debug_options.logging:
+ log_path = Path("~/.baet").expanduser()
+ log_path.mkdir(parents=True, exist_ok=True)
+ log_file = log_path / f"logs_{datetime.now()}.txt"
+ log_file.touch()
+
+ configure_logging(enable_logging=True, file_out=log_file)
+ else:
+ configure_logging(enable_logging=False, file_out=None)
+
+ logger = create_logger()
+ 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..3c61f15
--- /dev/null
+++ b/src/BAET/_logging.py
@@ -0,0 +1,41 @@
+import inspect
+import logging
+from logging import FileHandler, Logger
+from pathlib import Path
+
+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
+
+
+def configure_logging(*, enable_logging: bool = True, file_out: Path | None = None) -> None:
+ if not enable_logging:
+ logging.disable(logging.CRITICAL)
+
+ if file_out is not None:
+ handler = FileHandler(filename=file_out)
+ handler.setFormatter(rich_handler.formatter)
+ app_logger.addHandler(handler)
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/_path.py b/src/BAET/_path.py
new file mode 100644
index 0000000..10ed5bc
--- /dev/null
+++ b/src/BAET/_path.py
@@ -0,0 +1,57 @@
+import subprocess
+from os import PathLike
+
+from ._console import error_console
+from ._logging import create_logger
+
+logger = create_logger()
+
+
+def which_ffmpeg() -> str | PathLike[str] | None:
+ from shutil import which
+
+ return which("ffmpeg")
+
+
+def get_ffmpeg_version() -> str | None:
+ try:
+ ffmpeg = which_ffmpeg()
+
+ if not ffmpeg:
+ return None
+
+ proc = subprocess.run([ffmpeg, "-version"], capture_output=True)
+
+ if proc.returncode != 0:
+ err = proc.stderr.decode("utf-8")
+ raise RuntimeError(f"FFmpeg returned non-zero exit code when getting version:\n{err}")
+
+ output = proc.stdout.decode("utf-8")
+ return output[14 : output.find("Copyright")].strip()
+
+ except RuntimeError as e:
+ logger.critical("%s: %s", type(e).__name__, e)
+ error_console.print_exception()
+ raise e
+
+
+class FFmpegVersionInfo:
+ def __init__(self) -> None:
+ self._version: str | None = None
+
+ @property
+ def version(self) -> str:
+ if not self._version:
+ self._version = get_ffmpeg_version()
+
+ return self._version or "None"
+
+ def __str__(self) -> str:
+ return self.version
+
+
+ffmpeg_version_info = FFmpegVersionInfo()
+
+if __name__ == "__main__":
+ version_info = FFmpegVersionInfo()
+ print("Version:", version_info)
diff --git a/src/BAET/_theme.py b/src/BAET/_theme.py
new file mode 100644
index 0000000..d64efad
--- /dev/null
+++ b/src/BAET/_theme.py
@@ -0,0 +1,16 @@
+from rich.theme import Theme
+
+app_theme = Theme(
+ {
+ "app.version": "italic bright_cyan",
+ "argparse.arg_default": "dim italic",
+ "argparse.arg_default_parens": "dim",
+ "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/app_args.py b/src/BAET/app_args.py
new file mode 100644
index 0000000..bdefff8
--- /dev/null
+++ b/src/BAET/app_args.py
@@ -0,0 +1,351 @@
+import argparse
+import re
+from argparse import ArgumentParser
+from pathlib import Path
+from re import Pattern
+
+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
+from rich.table import Table
+from rich.terminal_theme import DIMMED_MONOKAI
+from rich.text import Text
+from rich_argparse import HelpPreviewAction, RichHelpFormatter
+from typing_extensions import Annotated
+
+from ._console import app_console
+from ._metadata import app_version
+from ._path import ffmpeg_version_info
+
+file_type_pattern = re.compile(r"^\.?(\w+)$")
+
+
+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):
+ 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:
+ @staticmethod
+ def __rich_console__(console: Console, options: ConsoleOptions) -> RenderResult:
+ yield Padding(Markdown("# Bulk Audio Extract Tool"), pad=(1, 0))
+ yield "Extract audio from a directory of videos using FFMPEG.\n"
+
+ website_link = "https://github.com/TimeTravelPenguin/BulkAudioExtractTool"
+ desc_kvps = [
+ (
+ Text("App name:", justify="right"),
+ Text(
+ "Bulk Audio Extract Tool (BAET)",
+ style="argparse.prog",
+ justify="left",
+ ),
+ ),
+ (
+ Text("App Version:", justify="right"),
+ Text(app_version(), style="app.version", justify="left"),
+ ),
+ (
+ Text("FFmpeg Version:", justify="right"),
+ Text(ffmpeg_version_info.version, style="app.version", justify="left"),
+ ),
+ (
+ Text("Author:", justify="right"),
+ Text("Phillip Smith", style="bright_yellow", justify="left"),
+ ),
+ (
+ Text("Website:", justify="right"),
+ Text(
+ website_link,
+ style=f"underline blue link {website_link}",
+ justify="left",
+ ),
+ ),
+ ]
+
+ grid = Table.grid(expand=True)
+ grid.add_column(justify="left")
+ grid.add_column(justify="right")
+ for key, value in desc_kvps:
+ grid.add_row(Padding(key, (0, 3, 0, 0)), value)
+
+ yield grid
+
+
+def new_empty_argparser() -> ArgumentParser:
+ 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()
+
+ RichHelpFormatter.highlights.append(
+ r"(?P\((?PDefault: (?P.*))\))"
+ )
+
+ 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(
+ "Phillip Smith, 2024",
+ justify="right",
+ style="argparse.prog",
+ ),
+ formatter_class=get_formatter,
+ )
+
+
+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()}[/]",
+ )
+
+ io_group = parser.add_argument_group(
+ "Input/Output",
+ "Options to control the source and destination directories of input and output files.",
+ )
+
+ io_group.add_argument(
+ "-i",
+ "--input-dir",
+ action="store",
+ type=DirectoryPath,
+ metavar="INPUT_DIR",
+ required=True,
+ help="Source directory.",
+ )
+
+ io_group.add_argument(
+ "-o",
+ "--output-dir",
+ default=None,
+ action="store",
+ type=Path,
+ help="Destination directory. Default is set to the input directory. To use the current directory, "
+ 'use [blue]"."[/]. (Default: None)',
+ )
+
+ query_group = parser.add_argument_group(
+ title="Input Filter Configuration",
+ description="Configure how the application includes and excludes files to process.",
+ )
+
+ query_group.add_argument(
+ "--include",
+ default=None,
+ metavar="REGEX",
+ help='[TODO] If provided, only include files that match a regex pattern. (Default: ".*")',
+ )
+
+ query_group.add_argument(
+ "--exclude",
+ default=None,
+ metavar="REGEX",
+ help="[TODO] If provided, exclude files that match a regex pattern. (Default: None)",
+ )
+
+ output_group = parser.add_argument_group(
+ title="Output Configuration",
+ description="Override the default output behavior of the application.",
+ )
+
+ output_group.add_argument(
+ "--overwrite-existing",
+ "--overwrite",
+ default=False,
+ action="store_true",
+ help="Overwrite a file if it already exists. (Default: False)",
+ )
+
+ output_group.add_argument(
+ "--no-output-subdirs",
+ default=False,
+ action="store_true",
+ help="Do not create subdirectories for each video's extracted audio tracks in the output directory. "
+ "(Default: True)",
+ )
+
+ output_group.add_argument(
+ "--acodec",
+ default="pcm_s16le",
+ metavar="CODEC",
+ help='[TODO] The audio codec to use when extracting audio. (Default: "pcm_s16le")',
+ )
+
+ output_group.add_argument(
+ "--fallback-sample-rate",
+ default=48000,
+ metavar="RATE",
+ help="[TODO] The sample rate to use if it cannot be determined via [blue]ffprobe[/]. (Default: 48000)",
+ )
+
+ output_group.add_argument(
+ "--file-type",
+ default="wav",
+ metavar="EXT",
+ help='[TODO] The file type to use for the extracted audio. (Default: "wav")',
+ )
+
+ debug_group = parser.add_argument_group(
+ "Debugging",
+ "Options to help debug the application.",
+ )
+
+ debug_group.add_argument(
+ "--run-synchronously",
+ "--sync",
+ default=False,
+ action="store_true",
+ help="[TODO] Run each each job in order. This should reduce the CPU workload, but may increase runtime. A "
+ "'job' is per file input, regardless of whether ffmpeg commands are merged (see: "
+ "`--output-streams-separately`). (Default: False)",
+ )
+
+ debug_group.add_argument(
+ "--logging",
+ default=False,
+ action="store_true",
+ help="[TODO] Show the logging of application execution. (Default: False)",
+ )
+
+ debug_group.add_argument(
+ "--print-args",
+ default=False,
+ action="store_true",
+ help="Print the parsed arguments and exit. (Default: False)",
+ )
+
+ debug_group.add_argument(
+ "--dry-run",
+ default=False,
+ action="store_true",
+ help="Run the program without actually extracting any audio. (Default: False)",
+ )
+
+ debug_group.add_argument(
+ "--show-ffmpeg-cmd",
+ "--cmds",
+ default=False,
+ action="store_true",
+ help="[TODO] Print to the console the generated ffmpeg command. (Default: False)",
+ )
+
+ debug_group.add_argument(
+ "--trim-short",
+ default=None,
+ type=int,
+ dest="trim",
+ help="[TODO] Trim the audio to the specified number of seconds. This is useful for testing. (Default: None)",
+ )
+
+ parser.add_argument(
+ "--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
+ help=argparse.SUPPRESS,
+ )
+
+ args = parser.parse_args()
+
+ input_filters = InputFilters(include=args.include, exclude=args.exclude)
+
+ output_config = OutputConfigurationOptions(
+ overwrite_existing=args.overwrite_existing,
+ no_output_subdirs=args.no_output_subdirs,
+ acodec=args.acodec,
+ fallback_sample_rate=args.fallback_sample_rate,
+ file_type=args.file_type,
+ )
+
+ debug_options = DebugOptions(
+ logging=args.logging,
+ dry_run=args.dry_run,
+ trim=args.trim,
+ print_args=args.print_args,
+ show_ffmpeg_cmd=args.show_ffmpeg_cmd,
+ run_synchronously=args.run_synchronously,
+ )
+
+ output_dir = args.output_dir or args.input_dir
+ output_dir = output_dir.expanduser()
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ app_args: AppArgs = AppArgs.model_validate(
+ {
+ "input_dir": args.input_dir.expanduser(),
+ "output_dir": output_dir,
+ "input_filters": input_filters,
+ "output_configuration": output_config,
+ "debug_options": debug_options,
+ },
+ strict=True,
+ )
+
+ return app_args
diff --git a/src/BAET/extract.py b/src/BAET/extract.py
new file mode 100644
index 0000000..99c0d35
--- /dev/null
+++ b/src/BAET/extract.py
@@ -0,0 +1,183 @@
+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.prompt import Confirm
+from rich.table import Table
+
+from ._aliases import AudioStream
+from ._console import app_console, 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:
+ logger.warning("No audio streams found")
+ yield []
+ return
+
+ 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
+
+
+def can_write_file(file: Path, has_overwrite_permission: bool) -> bool:
+ if not file.exists():
+ return True
+
+ if has_overwrite_permission:
+ return True
+
+ return Confirm.ask( # type: ignore
+ f'The file "{file.name}" already exists. Overwrite?',
+ console=app_console,
+ )
+
+
+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:
+ logger.info(f'Building job for "{file}"')
+
+ audio_streams: list[AudioStream] = []
+ indexed_outputs: MutableMapping[int, Stream] = {}
+
+ file = file.expanduser()
+ with probe_audio_streams(file) as streams:
+ for idx, stream in enumerate(streams):
+ 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,
+ )
+
+ can_write = can_write_file(
+ file=output_path,
+ has_overwrite_permission=self._output_configuration.overwrite_existing,
+ )
+
+ if can_write:
+ # Add stream here since otherwise there will possibly be more streams to indexes
+ # TODO: Maybe make a function/class to help with this?
+ audio_streams.append(stream)
+
+ indexed_outputs[stream_index] = (
+ ffmpeg.output(
+ ffmpeg_input[f"a:{idx}"],
+ str(output_path),
+ acodec=self._output_configuration.acodec,
+ audio_bitrate=sample_rate,
+ )
+ .overwrite_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:
+ logger.info("Starting MultiTrackAudioBulkExtractor")
+
+ self._app_args = app_args
+ self._extractor_jobs = MultitrackAudioBulkExtractorJobs(
+ app_args.input_dir,
+ app_args.output_dir,
+ app_args.input_filters,
+ app_args.output_configuration,
+ )
+
+ # Need to compile the jobs here, otherwise rich bugs out when prompting.
+ # This is because run_synchronously is called within a live display, which is buggy.
+ # In the future, a refactor to separate the running of jobs from this class can make things less complex
+ self._jobs = list(self._extractor_jobs)
+
+ self.display = Table.grid()
+
+ 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._jobs]
+ self.display.add_row(Padding(Group(*job_progresses), pad=(1, 2)))
+
+ logger.info("Starting synchronous execution of queued jobs")
+ for progress in job_progresses:
+ logger.info(f'Starting job "{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..36cdfb1
--- /dev/null
+++ b/src/BAET/job_progress.py
@@ -0,0 +1,129 @@
+import ffmpeg
+from bidict import MutableBidirectionalMapping, bidict
+from rich.console import Console, ConsoleOptions, ConsoleRenderable, Group, RenderResult
+from rich.highlighter import ReprHighlighter
+from rich.padding import Padding
+from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn, TimeRemainingColumn
+
+from ._aliases import StreamTaskBiMap
+from ._console import app_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]}"', highlighter=ReprHighlighter()),
+ BarColumn(
+ complete_style=bar_blue,
+ finished_style="green",
+ pulse_style=bar_yellow,
+ ),
+ TextColumn("Completed {task.completed} of {task.total}", highlighter=ReprHighlighter()),
+ 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]}", highlighter=ReprHighlighter()),
+ 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="[plum4]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: int = 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)
+ raise e
+
+ def start(self) -> None:
+ self._overall_progress.start_task(self._overall_progress_task)
+ logger.info(f"Stream index to job task ID bimap: {self._stream_task_bimap}")
+ for task in self._stream_task_bimap.values():
+ self._stream_task_progress.start_task(task)
+
+ self._stream_task_progress.update(task, status="[italic cornflower_blue]Working[/]")
+
+ try:
+ self._run_task(task)
+ self._stream_task_progress.update(task, status="[bold green]Complete[/]")
+ except RuntimeError as e:
+ self._stream_task_progress.update(task, status="[bold red]ERROR[/]")
+ finally:
+ 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..9cff596
--- /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,
+ )
+
+ 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/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