├── .deepsource.toml ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── poetry.lock ├── pymplschapters ├── MPLS │ ├── LICENSE.txt │ ├── README.md │ ├── __init__.py │ ├── load_AppInfoPlayList.py │ ├── load_ExtensionData.py │ ├── load_PlayItem.py │ ├── load_PlayList.py │ ├── load_PlayListMark.py │ ├── load_STNTable.py │ ├── load_StreamAttributes.py │ ├── load_StreamEntry.py │ ├── load_SubPath.py │ ├── load_SubPlayItem.py │ └── load_header.py └── __init__.py └── pyproject.toml /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" 9 | max_line_length = 120 10 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | tagged-release: 10 | name: Tagged Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.10.x' 18 | - name: Install Poetry 19 | uses: abatilo/actions-poetry@v2.2.0 20 | with: 21 | poetry-version: '1.2.2' 22 | - name: Configure poetry 23 | run: poetry config virtualenvs.in-project true 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip wheel 27 | poetry install 28 | - name: Build a wheel 29 | run: poetry build 30 | - name: Upload wheel 31 | uses: actions/upload-artifact@v2.2.4 32 | with: 33 | name: Python Wheel 34 | path: "dist/*.whl" 35 | - name: Deploy release 36 | uses: marvinpinto/action-automatic-releases@latest 37 | with: 38 | prerelease: false 39 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 40 | files: | 41 | dist/*.whl 42 | - name: Publish to PyPI 43 | env: 44 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 45 | run: poetry publish 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ['3.7', '3.8', '3.9', '3.10'] 17 | poetry-version: [1.2.2] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install poetry 26 | uses: abatilo/actions-poetry@v2.2.0 27 | with: 28 | poetry-version: ${{ matrix.poetry-version }} 29 | - name: Install project 30 | run: | 31 | poetry install --no-dev 32 | python -m pip install flake8 pytest 33 | 34 | - name: Lint with flake8 35 | run: | 36 | # stop the build if there are Python syntax errors or undefined names 37 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 38 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 39 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 40 | # - name: Test with pytest 41 | # run: | 42 | # pytest 43 | 44 | - name: Build project 45 | run: poetry build 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # Jetbrains Project settings 124 | .idea 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## 1.0.4 4 | 5 | - Add the ability to select an output directory for the chapter XMLs via `-d` option. 6 | - Advertise the new `-d` option in the README. 7 | - Small code cleanup by adding more newlines for readability, using f-strings where fit, etc. 8 | - Update the setuptools package information to represent my new GitHub username and author name and email. 9 | - Add 2 bash `.sh` packaging helper scripts. 10 | 11 | ## 1.0.3 12 | 13 | Update never existed. Accidentally skipped. Whoops... 14 | 15 | ## 1.0.2 16 | 17 | - Fix console script as the `main()` it targets did not yet exist. 18 | - Add `main()` to `pymplschapters\__init__.py` and a "Finished..." print at the end of execution. 19 | 20 | ## 1.0.1 21 | 22 | - Add console script `pymplschapters` via setuptools. 23 | - Update README for the new way of calling via the console script. 24 | 25 | ## 1.0.0 26 | 27 | - Initial release. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 PRAGMA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymplschapters 2 | 3 | [![License](https://img.shields.io/github/license/rlaphoenix/pymplschapters)](https://github.com/rlaphoenix/pymplschapters/blob/master/LICENSE) 4 | [![Build status](https://github.com/rlaphoenix/pymplschapters/actions/workflows/ci.yml/badge.svg)](https://github.com/rlaphoenix/pymplschapters/actions/workflows/ci.yml) 5 | [![Python versions](https://img.shields.io/pypi/pyversions/pymplschapters)](https://pypi.python.org/pypi/pymplschapters) 6 | [![PyPI version](https://img.shields.io/pypi/v/pymplschapters)](https://pypi.python.org/pypi/pymplschapters) 7 | [![GitHub issues](https://img.shields.io/github/issues/rlaphoenix/pymplschapters)](https://github.com/rlaphoenix/pymplschapters/issues) 8 | [![DeepSource issues](https://deepsource.io/gh/rlaphoenix/pymplschapters.svg/?label=active+issues)](https://deepsource.io/gh/rlaphoenix/pymplschapters) 9 | 10 | Extract chapters from a Blu-ray .mpls to a Matroska recognized XML file. 11 | 12 | ## Installation 13 | 14 | pip install pymplschapters 15 | 16 | ## Usage 17 | 18 | pymplschapters -p "C:\Path\To\The\Playlist.mpls" 19 | 20 | It will place any found chapters next to the input playlist file or to a specified directory with `-d`. 21 | 22 | ## Credit 23 | 24 | Thanks [PyGuymer](https://github.com/Guymer/PyGuymer) for the MPLS parsing code, it 25 | has been modified a bit to suit my needs. 26 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "lxml" 3 | version = "4.9.2" 4 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" 8 | 9 | [package.extras] 10 | cssselect = ["cssselect (>=0.7)"] 11 | html5 = ["html5lib"] 12 | htmlsoup = ["BeautifulSoup4"] 13 | source = ["Cython (>=0.29.7)"] 14 | 15 | [metadata] 16 | lock-version = "1.1" 17 | python-versions = ">=3.7,<3.11" 18 | content-hash = "906ad8548df260bfcf6c635f28f90a50e5b3d256e2e6fecf3ad67f615d54c571" 19 | 20 | [metadata.files] 21 | lxml = [ 22 | {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, 23 | {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, 24 | {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, 25 | {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, 26 | {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, 27 | {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, 28 | {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, 29 | {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, 30 | {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, 31 | {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, 32 | {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, 33 | {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, 34 | {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, 35 | {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, 36 | {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, 37 | {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, 38 | {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, 39 | {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, 40 | {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, 41 | {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, 42 | {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, 43 | {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, 44 | {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, 45 | {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, 46 | {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, 47 | {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, 48 | {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, 49 | {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, 50 | {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, 51 | {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, 52 | {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, 53 | {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, 54 | {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, 55 | {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, 56 | {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, 57 | {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, 58 | {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, 59 | {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, 60 | {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, 61 | {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, 62 | {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, 63 | {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, 64 | {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, 65 | {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, 66 | {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, 67 | {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, 68 | {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, 69 | {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, 70 | {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, 71 | {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, 72 | {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, 73 | {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, 74 | {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, 75 | {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, 76 | {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, 77 | {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, 78 | {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, 79 | {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, 80 | {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, 81 | {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, 82 | {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, 83 | {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, 84 | {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, 85 | {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, 86 | {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, 87 | {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, 88 | {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, 89 | {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, 90 | {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, 91 | {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, 92 | {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, 93 | {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, 94 | {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, 95 | {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, 96 | {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, 97 | {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, 98 | {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, 99 | ] 100 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/README.md: -------------------------------------------------------------------------------- 1 | # PyGuymer.MPLS 2 | 3 | This sub-module is a native Python implementation of a parser for Blu-ray MPLS files. It has used [an excellent MPLS Wiki](https://github.com/lerks/BluRay/wiki/MPLS) with a little help from [a WikiBook](https://en.wikibooks.org/wiki/User:Bdinfo/mpls) too. This project was started because, as of February 2018, [ffprobe](https://www.ffmpeg.org/ffprobe.html) v3.4 doesn't return the language information for the audio streams in a Blu-ray playlist. 4 | 5 | For example, running `ffprobe -probesize 3G -analyzeduration 1800M -playlist 820 bluray:/path/to/br` yields: 6 | 7 | ``` 8 | ffprobe version 3.4 Copyright (c) 2007-2017 the FFmpeg developers 9 | built with FreeBSD clang version 4.0.0 (tags/RELEASE_400/final 297347) (based on LLVM 4.0.0) 10 | configuration: --prefix=/usr/local --mandir=/usr/local/man --datadir=/usr/local/share/ffmpeg --pkgconfigdir=/usr/local/libdata/pkgconfig --enable-shared --enable-pic --enable-gpl --enable-postproc --enable-avfilter --enable-avresample --enable-pthreads --cc=cc --disable-alsa --disable-libopencore-amrnb --disable-libopencore-amrwb --enable-libass --disable-libbs2b --disable-libcaca --disable-libcdio --disable-libcelt --disable-chromaprint --disable-libdc1394 --disable-debug --disable-htmlpages --disable-libdrm --enable-libfdk-aac --disable-ffserver --disable-libflite --enable-fontconfig --enable-libfreetype --enable-frei0r --disable-libfribidi --disable-libgme --disable-libgsm --enable-iconv --disable-libilbc --disable-jack --disable-libkvazaar --disable-ladspa --enable-libmp3lame --enable-libbluray --disable-librsvg --disable-libxml2 --enable-mmx --disable-libmodplug --disable-openal --disable-opencl --enable-libopencv --disable-opengl --disable-libopenh264 --disable-libopenjpeg --enable-optimizations --disable-libopus --disable-libpulse --enable-runtime-cpudetect --disable-librubberband --disable-sdl2 --disable-libsmbclient --disable-libsnappy --disable-sndio --disable-libsoxr --disable-libspeex --enable-sse --disable-libssh --disable-libtesseract --enable-libtheora --disable-libtwolame --enable-libv4l2 --enable-vaapi --enable-vdpau --disable-libvidstab --enable-libvorbis --disable-libvo-amrwbenc --enable-libvpx --disable-libwavpack --disable-libwebp --enable-libx264 --enable-libx265 --disable-libxcb --enable-libxvid --disable-outdev=xv --disable-libzimg --disable-libzmq --disable-libzvbi --disable-gcrypt --enable-gmp --disable-librtmp --enable-gnutls --disable-openssl --enable-version3 --enable-nonfree --disable-libmysofa 11 | libavutil 55. 78.100 / 55. 78.100 12 | libavcodec 57.107.100 / 57.107.100 13 | libavformat 57. 83.100 / 57. 83.100 14 | libavdevice 57. 10.100 / 57. 10.100 15 | libavfilter 6.107.100 / 6.107.100 16 | libavresample 3. 7. 0 / 3. 7. 0 17 | libswscale 4. 8.100 / 4. 8.100 18 | libswresample 2. 9.100 / 2. 9.100 19 | libpostproc 54. 7.100 / 54. 7.100 20 | [bluray @ 0x80d07e000] 13 usable playlists: 21 | Input #0, mpegts, from 'bluray:/path/to/br': 22 | Duration: 01:01:38.57, start: 11.650667, bitrate: 33068 kb/s 23 | Program 1 24 | Stream #0:0[0x1011]: Video: h264 (High) (HDMV / 0x564D4448), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 90k tbn, 47.95 tbc 25 | Stream #0:1[0x1100]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 640 kb/s 26 | Stream #0:2[0x1101]: Audio: truehd (AC-3 / 0x332D4341), 48000 Hz, 7.1, s32 (24 bit) 27 | Stream #0:3[0x1101]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 640 kb/s 28 | Stream #0:4[0x1102]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s 29 | Stream #0:5[0x1103]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, stereo, fltp, 256 kb/s 30 | Stream #0:6[0x1104]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s 31 | Stream #0:7[0x1105]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s 32 | Stream #0:8[0x1106]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s 33 | Stream #0:9[0x1107]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s 34 | Stream #0:10[0x1200]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 35 | Stream #0:11[0x1201]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 36 | Stream #0:12[0x1202]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 37 | Stream #0:13[0x1203]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 38 | Stream #0:14[0x1204]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 39 | Stream #0:15[0x1205]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 40 | Stream #0:16[0x1206]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 41 | Stream #0:17[0x1207]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 42 | Stream #0:18[0x1208]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 43 | Stream #0:19[0x1209]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 44 | Stream #0:20[0x120a]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 45 | Stream #0:21[0x120b]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 46 | Stream #0:22[0x1a00]: Audio: eac3 ([161][0][0][0] / 0x00A1), 48000 Hz, stereo, fltp, 192 kb/s 47 | Stream #0:23[0x1b00]: Video: h264 (High) (HDMV / 0x564D4448), yuv420p(progressive), 720x480 [SAR 40:33 DAR 20:11], 23.98 fps, 23.98 tbr, 90k tbn, 47.95 tbc 48 | ``` 49 | 50 | The Blu-ray itself has language information and opening the tiny binary file `00820.mpls` in a text editor shows strings such as `eng` and `spa` amongst all the gibberish. I decided that instead of writing feature requests in both [ffmpeg](https://www.ffmpeg.org/) and [libbluray](https://www.videolan.org/developers/libbluray.html) it would be *much* quicker to code up a binary reader for the file and add its data to the data provided by `ffprobe ...`. My function [return_dict_of_media_audio_streams](../return_dict_of_media_audio_streams.py) now calls this sub-module and adds the language code to each stream. 51 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Load sub-functions ... 4 | from .load_AppInfoPlayList import load_AppInfoPlayList 5 | from .load_ExtensionData import load_ExtensionData 6 | from .load_PlayItem import load_PlayItem 7 | from .load_PlayList import load_PlayList 8 | from .load_PlayListMark import load_PlayListMark 9 | from .load_STNTable import load_STNTable 10 | from .load_StreamAttributes import load_StreamAttributes 11 | from .load_StreamEntry import load_StreamEntry 12 | from .load_SubPath import load_SubPath 13 | from .load_SubPlayItem import load_SubPlayItem 14 | from .load_header import load_header 15 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_AppInfoPlayList.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_AppInfoPlayList(fobj): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/AppInfoPlayList 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length1 = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) 15 | fobj.read(1); length1 += 1 16 | ans[u"PlaybackType"], = struct.unpack(u">B", fobj.read(1)); length1 += 1 17 | if ans[u"PlaybackType"] == int(0x02) or ans[u"PlaybackType"] == int(0x03): 18 | ans[u"PlaybackCount"], = struct.unpack(u">H", fobj.read(2)); length1 += 2 19 | else: 20 | fobj.read(2); length1 += 2 21 | ans[u"UOMaskTable"], = struct.unpack(u">Q", fobj.read(8)); length1 += 8 22 | ans[u"MiscFlags"], = struct.unpack(u">H", fobj.read(2)); length1 += 2 23 | 24 | # Pad out the read ... 25 | if length1 != ans[u"Length"]: 26 | l = ans[u"Length"] - length1 # [B] 27 | fobj.read(l); length1 += l 28 | 29 | # Return answer ... 30 | return ans, length1 31 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_ExtensionData.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_ExtensionData(fobj): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/ExtensionData 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length4 = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) 15 | if ans[u"Length"] != 0: 16 | ans[u"DataBlockStartAddress"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 17 | fobj.read(3); length4 += 4 18 | ans[u"NumberOfExtDataEntries"], = struct.unpack(u">B", fobj.read(1)); length4 += 1 19 | ans[u"ExtDataEntries"] = [] 20 | for i in range(ans[u"NumberOfExtDataEntries"]): 21 | tmp = {} 22 | tmp[u"ExtDataType"], = struct.unpack(u">H", fobj.read(2)); length4 += 2 23 | tmp[u"ExtDataVersion"], = struct.unpack(u">H", fobj.read(2)); length4 += 2 24 | tmp[u"ExtDataStartAddress"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 25 | tmp[u"ExtDataLength"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 26 | ans[u"ExtDataEntries"].append(tmp) 27 | 28 | # NOTE: ExtDataEntries is not implemented 29 | 30 | # Pad out the read ... 31 | if length4 != ans[u"Length"]: 32 | l = ans[u"Length"] - length4 # [B] 33 | fobj.read(l); length4 += l 34 | 35 | # Return answer ... 36 | return ans, length4 37 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_PlayItem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_PlayItem(fobj, length2): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/PlayItem 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Load sub-functions ... 10 | from .load_STNTable import load_STNTable 11 | 12 | # Initialize variables ... 13 | ans = {} 14 | length2a = 0 # [B] 15 | 16 | # Read the binary data ... 17 | ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 18 | ans[u"ClipInformationFileName"] = fobj.read(5).decode('utf-8'); length2 += 5; length2a += 5 19 | ans[u"ClipCodecIdentifier"] = fobj.read(4).decode('utf-8'); length2 += 4; length2a += 4 20 | ans[u"MiscFlags1"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 21 | ans[u"IsMultiAngle"] = bool(ans[u"MiscFlags1"]&(1<<11)) 22 | ans[u"RefToSTCID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 23 | ans[u"INTime"], = struct.unpack(u">I", fobj.read(4)); length2 += 4; length2a += 4 24 | ans[u"OUTTime"], = struct.unpack(u">I", fobj.read(4)); length2 += 4; length2a += 4 25 | ans[u"UOMaskTable"], = struct.unpack(u">Q", fobj.read(8)); length2 += 8; length2a += 8 26 | ans[u"MiscFlags2"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 27 | ans[u"StillMode"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 28 | if ans[u"StillMode"] == int(0x01): 29 | ans[u"StillTime"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 30 | else: 31 | fobj.read(2).decode('utf-8'); length2 += 2; length2a += 2 32 | if ans[u"IsMultiAngle"]: 33 | raise Exception("IsMultiAngle has not been implemented as the specification is not byte-aligned (IsDifferentAudios is 6-bit and IsSeamlessAngleChange is 1-bit)") 34 | 35 | # Load STNTable section ... 36 | res, length2, length2a, length2b = load_STNTable(fobj, length2, length2a) 37 | ans[u"STNTable"] = res 38 | 39 | # Pad out the read ... 40 | if length2a != ans[u"Length"]: 41 | l = ans[u"Length"] - length2a # [B] 42 | fobj.read(l); length2 += l; length2a += l 43 | 44 | # Return answer ... 45 | return ans, length2, length2a 46 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_PlayList.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_PlayList(fobj): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/PlayList 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Load sub-functions ... 10 | from .load_PlayItem import load_PlayItem 11 | from .load_SubPath import load_SubPath 12 | 13 | # Initialize variables ... 14 | ans = {} 15 | length2 = 0 # [B] 16 | 17 | # Read the binary data ... 18 | ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) 19 | fobj.read(2); length2 += 2 20 | ans[u"NumberOfPlayItems"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 21 | ans[u"NumberOfSubPaths"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 22 | 23 | # Loop over PlayItems ... 24 | ans[u"PlayItems"] = [] 25 | for i in range(ans[u"NumberOfPlayItems"]): 26 | # Load PlayItem section and append to PlayItems list ... 27 | res, length2, length2a = load_PlayItem(fobj, length2) 28 | ans[u"PlayItems"].append(res) 29 | 30 | # Loop over SubPaths ... 31 | ans[u"SubPaths"] = [] 32 | for i in range(ans[u"NumberOfSubPaths"]): 33 | # Load SubPath section and append to SubPaths list ... 34 | res, length2, length2a = load_SubPath(fobj, length2) 35 | ans[u"SubPaths"].append(res) 36 | 37 | # Pad out the read ... 38 | if length2 != ans[u"Length"]: 39 | l = ans[u"Length"] - length2 # [B] 40 | fobj.read(l); length2 += l 41 | 42 | # Return answer ... 43 | return ans, length2 44 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_PlayListMark.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_PlayListMark(fobj): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/PlayListMark 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length3 = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) 15 | ans[u"NumberOfPlayListMarks"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 16 | ans[u"PlayListMarks"] = [] 17 | for i in range(ans[u"NumberOfPlayListMarks"]): 18 | tmp = {} 19 | fobj.read(1); length3 += 1 20 | tmp[u"MarkType"], = struct.unpack(u">B", fobj.read(1)); length3 += 1 21 | tmp[u"RefToPlayItemID"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 22 | tmp[u"MarkTimeStamp"], = struct.unpack(u">I", fobj.read(4)); length3 += 4 23 | tmp[u"EntryESPID"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 24 | tmp[u"Duration"], = struct.unpack(u">I", fobj.read(4)); length3 += 4 25 | ans[u"PlayListMarks"].append(tmp) 26 | 27 | # Pad out the read ... 28 | if length3 != ans[u"Length"]: 29 | l = ans[u"Length"] - length3 # [B] 30 | fobj.read(l); length3 += l 31 | 32 | # Return answer ... 33 | return ans, length3 34 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_STNTable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_STNTable(fobj, length2, length2a): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/STNTable 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Load sub-functions ... 10 | from .load_StreamAttributes import load_StreamAttributes 11 | from .load_StreamEntry import load_StreamEntry 12 | 13 | # Initialize variables ... 14 | ans = {} 15 | length2b = 0 # [B] 16 | 17 | # Read the binary data ... 18 | ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 19 | fobj.read(2); length2 += 2; length2a += 2; length2b += 2 20 | ans[u"NumberOfPrimaryVideoStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 21 | ans[u"NumberOfPrimaryAudioStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 22 | ans[u"NumberOfPrimaryPGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 23 | ans[u"NumberOfPrimaryIGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 24 | ans[u"NumberOfSecondaryAudioStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 25 | ans[u"NumberOfSecondaryVideoStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 26 | ans[u"NumberOfSecondaryPGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 27 | fobj.read(5); length2 += 5; length2a += 5; length2b += 5 28 | 29 | # Loop over stream list names ... 30 | for name in [u"PrimaryVideoStreamEntries", u"PrimaryAudioStreamEntries", u"PrimaryPGStreamEntries", u"SecondaryPGStreamEntries", u"PrimaryIGStreamEntries", u"SecondaryAudioStreamEntries", u"SecondaryVideoStreamEntries"]: 31 | # Loop over entries and add to list ... 32 | ans[name] = [] 33 | for i in range(ans[u"NumberOf{0:s}".format(name)]): 34 | tmp = {} 35 | res, length2, length2a, length2b, length2c = load_StreamEntry(fobj, length2, length2a, length2b) 36 | tmp[u"StreamEntry"] = res 37 | res, length2, length2a, length2b, length2c = load_StreamAttributes(fobj, length2, length2a, length2b) 38 | tmp[u"StreamAttributes"] = res 39 | ans[name].append(tmp) 40 | 41 | # Pad out the read ... 42 | if length2b != ans[u"Length"]: 43 | l = ans[u"Length"] - length2b # [B] 44 | fobj.read(l); length2 += l; length2a += l; length2b += l 45 | 46 | # Return answer ... 47 | return ans, length2, length2a, length2b 48 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_StreamAttributes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_StreamAttributes(fobj, length2, length2a, length2b): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/StreamAttributes 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length2c = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"Length"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 15 | if ans[u"Length"] != 0: 16 | ans[u"StreamCodingType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 17 | if ans[u"StreamCodingType"] in [int(0x02), int(0x1B), int(0xEA)]: 18 | ans[u"VideoFormat+FrameRate"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 19 | if ans[u"StreamCodingType"] in [int(0x80), int(0x81), int(0x82), int(0x83), int(0x84), int(0x85), int(0x86), int(0xA1), int(0xA2)]: 20 | ans[u"AudioFormat+SampleRate"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 21 | ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 22 | if ans[u"StreamCodingType"] in [int(0x90), int(0x91)]: 23 | ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 24 | if ans[u"StreamCodingType"] in [int(0x92)]: 25 | ans[u"CharacterCode"] = fobj.read(1).decode('utf-8'); length2 += 1; length2a += 1; length2b += 1; length2c += 1 26 | ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 27 | 28 | # Pad out the read ... 29 | if length2c != ans[u"Length"]: 30 | l = ans[u"Length"] - length2c # [B] 31 | fobj.read(l); length2 += l; length2a += l; length2b += l; length2c += l 32 | 33 | # Return answer ... 34 | return ans, length2, length2a, length2b, length2c 35 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_StreamEntry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_StreamEntry(fobj, length2, length2a, length2b): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/StreamEntry 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length2c = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans = {} 15 | ans[u"Length"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 16 | if ans[u"Length"] != 0: 17 | ans[u"StreamType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 18 | if ans[u"StreamType"] == int(0x01): 19 | tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 20 | ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) 21 | if ans[u"StreamType"] == int(0x02): 22 | ans[u"RefToSubPathID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 23 | ans[u"RefToSubClipID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 24 | tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 25 | ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) 26 | if ans[u"StreamType"] == int(0x03): 27 | tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 28 | ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) 29 | if ans[u"StreamType"] == int(0x04): 30 | ans[u"RefToSubPathID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 31 | ans[u"RefToSubClipID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 32 | tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 33 | ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) 34 | 35 | # Pad out the read ... 36 | if length2c != ans[u"Length"]: 37 | l = ans[u"Length"] - length2c # [B] 38 | fobj.read(l); length2 += l; length2a += l; length2b += l; length2c += l 39 | 40 | # Return answer ... 41 | return ans, length2, length2a, length2b, length2c 42 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_SubPath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_SubPath(fobj, length2): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/SubPath 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Load sub-functions ... 10 | from .load_SubPlayItem import load_SubPlayItem 11 | 12 | # Initialize variables ... 13 | ans = {} 14 | length2a = 0 # [B] 15 | 16 | # Read the binary data ... 17 | ans[u"Length"], = struct.unpack(u">I", fobj.read(4)); length2 += 4 18 | fobj.read(1); length2 += 1; length2a += 1 19 | ans[u"SubPathType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 20 | ans[u"MiscFlags1"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 21 | ans[u"NumberOfSubPlayItems"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 22 | ans[u"SubPlayItems"] = [] 23 | for i in range(ans[u"NumberOfSubPlayItems"]): 24 | res, length2, length2a, length2b = load_SubPlayItem(fobj, length2, length2a) 25 | ans[u"SubPlayItems"].append(res) 26 | 27 | # Pad out the read ... 28 | if length2a != ans[u"Length"]: 29 | l = ans[u"Length"] - length2a # [B] 30 | fobj.read(l); length2 += l; length2a += l 31 | 32 | return ans, length2, length2a 33 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_SubPlayItem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_SubPlayItem(fobj, length2, length2a): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/SubPlayItem 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length2b = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 15 | 16 | # NOTE: SubPlayItem is not implemented 17 | 18 | # Pad out the read ... 19 | if length2b != ans[u"Length"]: 20 | l = ans[u"Length"] - length2b # [B] 21 | fobj.read(l); length2 += l; length2a += l; length2b += l 22 | 23 | # Return answer ... 24 | return ans, length2, length2a, length2b 25 | -------------------------------------------------------------------------------- /pymplschapters/MPLS/load_header.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def load_header(fobj): 4 | # NOTE: see https://github.com/lerks/BluRay/wiki/MPLS 5 | 6 | # Import modules ... 7 | import struct 8 | 9 | # Initialize variables ... 10 | ans = {} 11 | length0 = 0 # [B] 12 | 13 | # Read the binary data ... 14 | ans[u"TypeIndicator"] = fobj.read(4); length0 += 4 15 | ans[u"VersionNumber"] = fobj.read(4); length0 += 4 16 | ans[u"PlayListStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 17 | ans[u"PlayListMarkStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 18 | ans[u"ExtensionDataStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 19 | fobj.read(20); length0 += 20 20 | 21 | # Return answer ... 22 | return ans, length0 23 | -------------------------------------------------------------------------------- /pymplschapters/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from argparse import ArgumentParser 3 | from datetime import timedelta 4 | 5 | from lxml import etree 6 | 7 | from pymplschapters import MPLS 8 | 9 | 10 | def main(): 11 | arg_parser = ArgumentParser() 12 | arg_parser.add_argument("-p", "--playlist", 13 | help="Input path to an MPLS playlist", required=True) 14 | arg_parser.add_argument("-d", "--directory", required=False, 15 | help="Specify an output directory, by default it will save next to the playlist.") 16 | args = arg_parser.parse_args() 17 | 18 | with open(args.playlist, "rb") as f: 19 | print("Processing file...") 20 | for file_with_chapter in get_chapters(f): 21 | chapters = etree.Element("Chapters") 22 | edition_entry = etree.SubElement(chapters, "EditionEntry") 23 | edition_flag_hidden = etree.SubElement(edition_entry, "EditionFlagHidden") 24 | edition_flag_hidden.text = "0" 25 | edition_flag_default = etree.SubElement(edition_entry, "EditionFlagDefault") 26 | edition_flag_default.text = "0" 27 | for chapter in file_with_chapter: 28 | chapter_atom = etree.SubElement(edition_entry, "ChapterAtom") 29 | chapter_display = etree.SubElement(chapter_atom, "ChapterDisplay") 30 | chapter_string = etree.SubElement(chapter_display, "ChapterString") 31 | chapter_string.text = f"Chapter {str(chapter['number']).zfill(2)}" 32 | chapter_language = etree.SubElement(chapter_display, "ChapterLanguage") 33 | chapter_language.text = "eng" 34 | chapter_time_start = etree.SubElement(chapter_atom, "ChapterTimeStart") 35 | chapter_time_start.text = chapter["timespan"] 36 | chapter_flag_hidden = etree.SubElement(chapter_atom, "ChapterFlagHidden") 37 | chapter_flag_hidden.text = "0" 38 | chapter_flag_enabled = etree.SubElement(chapter_atom, "ChapterFlagEnabled") 39 | chapter_flag_enabled.text = "1" 40 | clip = file_with_chapter[0]["clip"] 41 | with open(os.path.join( 42 | args.directory or os.path.dirname(args.playlist), 43 | f"{os.path.splitext(os.path.basename(args.playlist))[0]}_{clip.split('.')[0]}.xml" 44 | ), "wb") as ff: 45 | ff.write( 46 | etree.tostring( 47 | chapters, 48 | encoding="utf-8", 49 | doctype='', 50 | xml_declaration=True, 51 | pretty_print=True, 52 | ) 53 | ) 54 | print("Extracted chapters for " + clip) 55 | print("Finished...") 56 | 57 | 58 | def get_chapters(f): 59 | header, _ = MPLS.load_header(f) 60 | 61 | f.seek(header["PlayListStartAddress"], os.SEEK_SET) 62 | playlist, _ = MPLS.load_PlayList(f) 63 | 64 | f.seek(header["PlayListMarkStartAddress"], os.SEEK_SET) 65 | playlist_marks, _ = MPLS.load_PlayListMark(f) 66 | playlist_marks = playlist_marks["PlayListMarks"] 67 | 68 | for i, play_item in enumerate(playlist["PlayItems"]): 69 | filename = f"{play_item['ClipInformationFileName']}.{play_item['ClipCodecIdentifier'].lower()}" 70 | 71 | play_item_marks = [ 72 | x 73 | for x in playlist_marks 74 | if x["MarkType"] == 1 and x["RefToPlayItemID"] == i 75 | ] 76 | if not play_item_marks: 77 | # no chapters for this play item? 78 | print("No chapters in Playlist for", filename) 79 | continue 80 | 81 | offset = play_item_marks[0]["MarkTimeStamp"] 82 | if play_item["INTime"] < offset: 83 | offset = play_item["INTime"] 84 | 85 | chapters = [] 86 | for n, play_item_mark in enumerate(play_item_marks): 87 | duration = ((play_item_mark["MarkTimeStamp"] - offset) / 45000) * 1000 88 | timespan = str(timedelta(milliseconds=duration)) 89 | if timespan.startswith("0:"): 90 | timespan = f"0{timespan}" 91 | if "." not in timespan: 92 | timespan = f"{timespan}.000000" 93 | chapters.append({ 94 | "clip": filename, 95 | "number": n + 1, 96 | "duration": duration, 97 | "timespan": timespan, 98 | }) 99 | yield chapters 100 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "pymplschapters" 7 | version = "1.0.4" 8 | description = "Extract chapters from a Blu-ray .mpls to a Matroska recognized XML file." 9 | license = "MIT" 10 | authors = ["rlaphoenix "] 11 | readme = "README.md" 12 | homepage = "https://github.com/rlaphoenix/pymplschapters" 13 | repository = "https://github.com/rlaphoenix/pymplschapters" 14 | keywords = ["mpls", "blu-ray", "playlist", "chapters", "matroska"] 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Environment :: Console", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: End Users/Desktop", 20 | "Natural Language :: English", 21 | "Operating System :: OS Independent", 22 | "Topic :: Multimedia" 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = ">=3.7,<3.11" 27 | lxml = "^4.9.2" 28 | 29 | [tool.poetry.scripts] 30 | pymplschapters = "pymplschapters.__init__:main" 31 | --------------------------------------------------------------------------------