diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49f7472 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.directory +.idea/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13be23f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 PRAGMA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f88cf8 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# pymplschapters +Extract chapters from a blu-ray mpls to a matroska recognized xml file + +## Installation + + pip install pymplschapters + +## Usage + + pymplschapters -p "C:/A Path/To/The/Playlist.mpls" + +It will place any found chapters next to the input playlist file. + +## Credit + +Thanks [PyGuymer](https://github.com/Guymer/PyGuymer) for the MPLS parsing code, it +has been modified a bit to suit my needs. \ No newline at end of file diff --git a/pymplschapters/MPLS/LICENSE.txt b/pymplschapters/MPLS/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/pymplschapters/MPLS/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pymplschapters/MPLS/README.md b/pymplschapters/MPLS/README.md new file mode 100644 index 0000000..93efa7b --- /dev/null +++ b/pymplschapters/MPLS/README.md @@ -0,0 +1,50 @@ +# PyGuymer.MPLS + +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. + +For example, running `ffprobe -probesize 3G -analyzeduration 1800M -playlist 820 bluray:/path/to/br` yields: + +``` +ffprobe version 3.4 Copyright (c) 2007-2017 the FFmpeg developers + built with FreeBSD clang version 4.0.0 (tags/RELEASE_400/final 297347) (based on LLVM 4.0.0) + 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 + libavutil 55. 78.100 / 55. 78.100 + libavcodec 57.107.100 / 57.107.100 + libavformat 57. 83.100 / 57. 83.100 + libavdevice 57. 10.100 / 57. 10.100 + libavfilter 6.107.100 / 6.107.100 + libavresample 3. 7. 0 / 3. 7. 0 + libswscale 4. 8.100 / 4. 8.100 + libswresample 2. 9.100 / 2. 9.100 + libpostproc 54. 7.100 / 54. 7.100 +[bluray @ 0x80d07e000] 13 usable playlists: +Input #0, mpegts, from 'bluray:/path/to/br': + Duration: 01:01:38.57, start: 11.650667, bitrate: 33068 kb/s + Program 1 + 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 + Stream #0:1[0x1100]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 640 kb/s + Stream #0:2[0x1101]: Audio: truehd (AC-3 / 0x332D4341), 48000 Hz, 7.1, s32 (24 bit) + Stream #0:3[0x1101]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 640 kb/s + Stream #0:4[0x1102]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s + Stream #0:5[0x1103]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, stereo, fltp, 256 kb/s + Stream #0:6[0x1104]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s + Stream #0:7[0x1105]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s + Stream #0:8[0x1106]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s + Stream #0:9[0x1107]: Audio: ac3 (AC-3 / 0x332D4341), 48000 Hz, 5.1(side), fltp, 448 kb/s + Stream #0:10[0x1200]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:11[0x1201]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:12[0x1202]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:13[0x1203]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:14[0x1204]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:15[0x1205]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:16[0x1206]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:17[0x1207]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:18[0x1208]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:19[0x1209]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:20[0x120a]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:21[0x120b]: Subtitle: hdmv_pgs_subtitle ([144][0][0][0] / 0x0090), 1920x1080 + Stream #0:22[0x1a00]: Audio: eac3 ([161][0][0][0] / 0x00A1), 48000 Hz, stereo, fltp, 192 kb/s + 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 +``` + +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. diff --git a/pymplschapters/MPLS/__init__.py b/pymplschapters/MPLS/__init__.py new file mode 100644 index 0000000..8146d66 --- /dev/null +++ b/pymplschapters/MPLS/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +# Load sub-functions ... +from .load_AppInfoPlayList import load_AppInfoPlayList +from .load_ExtensionData import load_ExtensionData +from .load_PlayItem import load_PlayItem +from .load_PlayList import load_PlayList +from .load_PlayListMark import load_PlayListMark +from .load_STNTable import load_STNTable +from .load_StreamAttributes import load_StreamAttributes +from .load_StreamEntry import load_StreamEntry +from .load_SubPath import load_SubPath +from .load_SubPlayItem import load_SubPlayItem +from .load_header import load_header diff --git a/pymplschapters/MPLS/load_AppInfoPlayList.py b/pymplschapters/MPLS/load_AppInfoPlayList.py new file mode 100644 index 0000000..5d2db6c --- /dev/null +++ b/pymplschapters/MPLS/load_AppInfoPlayList.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +def load_AppInfoPlayList(fobj): + # NOTE: see https://github.com/lerks/BluRay/wiki/AppInfoPlayList + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length1 = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) + fobj.read(1); length1 += 1 + ans[u"PlaybackType"], = struct.unpack(u">B", fobj.read(1)); length1 += 1 + if ans[u"PlaybackType"] == int(0x02) or ans[u"PlaybackType"] == int(0x03): + ans[u"PlaybackCount"], = struct.unpack(u">H", fobj.read(2)); length1 += 2 + else: + fobj.read(2); length1 += 2 + ans[u"UOMaskTable"], = struct.unpack(u">Q", fobj.read(8)); length1 += 8 + ans[u"MiscFlags"], = struct.unpack(u">H", fobj.read(2)); length1 += 2 + + # Pad out the read ... + if length1 != ans[u"Length"]: + l = ans[u"Length"] - length1 # [B] + fobj.read(l); length1 += l + + # Return answer ... + return ans, length1 diff --git a/pymplschapters/MPLS/load_ExtensionData.py b/pymplschapters/MPLS/load_ExtensionData.py new file mode 100644 index 0000000..93f2ed0 --- /dev/null +++ b/pymplschapters/MPLS/load_ExtensionData.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +def load_ExtensionData(fobj): + # NOTE: see https://github.com/lerks/BluRay/wiki/ExtensionData + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length4 = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) + if ans[u"Length"] != 0: + ans[u"DataBlockStartAddress"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 + fobj.read(3); length4 += 4 + ans[u"NumberOfExtDataEntries"], = struct.unpack(u">B", fobj.read(1)); length4 += 1 + ans[u"ExtDataEntries"] = [] + for i in range(ans[u"NumberOfExtDataEntries"]): + tmp = {} + tmp[u"ExtDataType"], = struct.unpack(u">H", fobj.read(2)); length4 += 2 + tmp[u"ExtDataVersion"], = struct.unpack(u">H", fobj.read(2)); length4 += 2 + tmp[u"ExtDataStartAddress"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 + tmp[u"ExtDataLength"], = struct.unpack(u">I", fobj.read(4)); length4 += 4 + ans[u"ExtDataEntries"].append(tmp) + + # NOTE: ExtDataEntries is not implemented + + # Pad out the read ... + if length4 != ans[u"Length"]: + l = ans[u"Length"] - length4 # [B] + fobj.read(l); length4 += l + + # Return answer ... + return ans, length4 diff --git a/pymplschapters/MPLS/load_PlayItem.py b/pymplschapters/MPLS/load_PlayItem.py new file mode 100644 index 0000000..4264ad3 --- /dev/null +++ b/pymplschapters/MPLS/load_PlayItem.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +def load_PlayItem(fobj, length2): + # NOTE: see https://github.com/lerks/BluRay/wiki/PlayItem + + # Import modules ... + import struct + + # Load sub-functions ... + from .load_STNTable import load_STNTable + + # Initialize variables ... + ans = {} + length2a = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 + ans[u"ClipInformationFileName"] = fobj.read(5).decode('utf-8'); length2 += 5; length2a += 5 + ans[u"ClipCodecIdentifier"] = fobj.read(4).decode('utf-8'); length2 += 4; length2a += 4 + ans[u"MiscFlags1"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 + ans[u"IsMultiAngle"] = bool(ans[u"MiscFlags1"]&(1<<11)) + ans[u"RefToSTCID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 + ans[u"INTime"], = struct.unpack(u">I", fobj.read(4)); length2 += 4; length2a += 4 + ans[u"OUTTime"], = struct.unpack(u">I", fobj.read(4)); length2 += 4; length2a += 4 + ans[u"UOMaskTable"], = struct.unpack(u">Q", fobj.read(8)); length2 += 8; length2a += 8 + ans[u"MiscFlags2"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 + ans[u"StillMode"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 + if ans[u"StillMode"] == int(0x01): + ans[u"StillTime"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 + else: + fobj.read(2).decode('utf-8'); length2 += 2; length2a += 2 + if ans[u"IsMultiAngle"]: + raise Exception("IsMultiAngle has not been implemented as the specification is not byte-aligned (IsDifferentAudios is 6-bit and IsSeamlessAngleChange is 1-bit)") + + # Load STNTable section ... + res, length2, length2a, length2b = load_STNTable(fobj, length2, length2a) + ans[u"STNTable"] = res + + # Pad out the read ... + if length2a != ans[u"Length"]: + l = ans[u"Length"] - length2a # [B] + fobj.read(l); length2 += l; length2a += l + + # Return answer ... + return ans, length2, length2a diff --git a/pymplschapters/MPLS/load_PlayList.py b/pymplschapters/MPLS/load_PlayList.py new file mode 100644 index 0000000..ddef200 --- /dev/null +++ b/pymplschapters/MPLS/load_PlayList.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +def load_PlayList(fobj): + # NOTE: see https://github.com/lerks/BluRay/wiki/PlayList + + # Import modules ... + import struct + + # Load sub-functions ... + from .load_PlayItem import load_PlayItem + from .load_SubPath import load_SubPath + + # Initialize variables ... + ans = {} + length2 = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) + fobj.read(2); length2 += 2 + ans[u"NumberOfPlayItems"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 + ans[u"NumberOfSubPaths"], = struct.unpack(u">H", fobj.read(2)); length2 += 2 + + # Loop over PlayItems ... + ans[u"PlayItems"] = [] + for i in range(ans[u"NumberOfPlayItems"]): + # Load PlayItem section and append to PlayItems list ... + res, length2, length2a = load_PlayItem(fobj, length2) + ans[u"PlayItems"].append(res) + + # Loop over SubPaths ... + ans[u"SubPaths"] = [] + for i in range(ans[u"NumberOfSubPaths"]): + # Load SubPath section and append to SubPaths list ... + res, length2, length2a = load_SubPath(fobj, length2) + ans[u"SubPaths"].append(res) + + # Pad out the read ... + if length2 != ans[u"Length"]: + l = ans[u"Length"] - length2 # [B] + fobj.read(l); length2 += l + + # Return answer ... + return ans, length2 diff --git a/pymplschapters/MPLS/load_PlayListMark.py b/pymplschapters/MPLS/load_PlayListMark.py new file mode 100644 index 0000000..2d9386c --- /dev/null +++ b/pymplschapters/MPLS/load_PlayListMark.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +def load_PlayListMark(fobj): + # NOTE: see https://github.com/lerks/BluRay/wiki/PlayListMark + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length3 = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">I", fobj.read(4)) + ans[u"NumberOfPlayListMarks"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 + ans[u"PlayListMarks"] = [] + for i in range(ans[u"NumberOfPlayListMarks"]): + tmp = {} + fobj.read(1); length3 += 1 + tmp[u"MarkType"], = struct.unpack(u">B", fobj.read(1)); length3 += 1 + tmp[u"RefToPlayItemID"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 + tmp[u"MarkTimeStamp"], = struct.unpack(u">I", fobj.read(4)); length3 += 4 + tmp[u"EntryESPID"], = struct.unpack(u">H", fobj.read(2)); length3 += 2 + tmp[u"Duration"], = struct.unpack(u">I", fobj.read(4)); length3 += 4 + ans[u"PlayListMarks"].append(tmp) + + # Pad out the read ... + if length3 != ans[u"Length"]: + l = ans[u"Length"] - length3 # [B] + fobj.read(l); length3 += l + + # Return answer ... + return ans, length3 diff --git a/pymplschapters/MPLS/load_STNTable.py b/pymplschapters/MPLS/load_STNTable.py new file mode 100644 index 0000000..cfcb795 --- /dev/null +++ b/pymplschapters/MPLS/load_STNTable.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +def load_STNTable(fobj, length2, length2a): + # NOTE: see https://github.com/lerks/BluRay/wiki/STNTable + + # Import modules ... + import struct + + # Load sub-functions ... + from .load_StreamAttributes import load_StreamAttributes + from .load_StreamEntry import load_StreamEntry + + # Initialize variables ... + ans = {} + length2b = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 + fobj.read(2); length2 += 2; length2a += 2; length2b += 2 + ans[u"NumberOfPrimaryVideoStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfPrimaryAudioStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfPrimaryPGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfPrimaryIGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfSecondaryAudioStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfSecondaryVideoStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + ans[u"NumberOfSecondaryPGStreamEntries"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + fobj.read(5); length2 += 5; length2a += 5; length2b += 5 + + # Loop over stream list names ... + for name in [u"PrimaryVideoStreamEntries", u"PrimaryAudioStreamEntries", u"PrimaryPGStreamEntries", u"SecondaryPGStreamEntries", u"PrimaryIGStreamEntries", u"SecondaryAudioStreamEntries", u"SecondaryVideoStreamEntries"]: + # Loop over entries and add to list ... + ans[name] = [] + for i in range(ans[u"NumberOf{0:s}".format(name)]): + tmp = {} + res, length2, length2a, length2b, length2c = load_StreamEntry(fobj, length2, length2a, length2b) + tmp[u"StreamEntry"] = res + res, length2, length2a, length2b, length2c = load_StreamAttributes(fobj, length2, length2a, length2b) + tmp[u"StreamAttributes"] = res + ans[name].append(tmp) + + # Pad out the read ... + if length2b != ans[u"Length"]: + l = ans[u"Length"] - length2b # [B] + fobj.read(l); length2 += l; length2a += l; length2b += l + + # Return answer ... + return ans, length2, length2a, length2b diff --git a/pymplschapters/MPLS/load_StreamAttributes.py b/pymplschapters/MPLS/load_StreamAttributes.py new file mode 100644 index 0000000..62fd4fd --- /dev/null +++ b/pymplschapters/MPLS/load_StreamAttributes.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +def load_StreamAttributes(fobj, length2, length2a, length2b): + # NOTE: see https://github.com/lerks/BluRay/wiki/StreamAttributes + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length2c = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + if ans[u"Length"] != 0: + ans[u"StreamCodingType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + if ans[u"StreamCodingType"] in [int(0x02), int(0x1B), int(0xEA)]: + ans[u"VideoFormat+FrameRate"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + if ans[u"StreamCodingType"] in [int(0x80), int(0x81), int(0x82), int(0x83), int(0x84), int(0x85), int(0x86), int(0xA1), int(0xA2)]: + ans[u"AudioFormat+SampleRate"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 + if ans[u"StreamCodingType"] in [int(0x90), int(0x91)]: + ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 + if ans[u"StreamCodingType"] in [int(0x92)]: + ans[u"CharacterCode"] = fobj.read(1).decode('utf-8'); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + ans[u"LanguageCode"] = fobj.read(3).decode('utf-8'); length2 += 3; length2a += 3; length2b += 3; length2c += 3 + + # Pad out the read ... + if length2c != ans[u"Length"]: + l = ans[u"Length"] - length2c # [B] + fobj.read(l); length2 += l; length2a += l; length2b += l; length2c += l + + # Return answer ... + return ans, length2, length2a, length2b, length2c diff --git a/pymplschapters/MPLS/load_StreamEntry.py b/pymplschapters/MPLS/load_StreamEntry.py new file mode 100644 index 0000000..1408324 --- /dev/null +++ b/pymplschapters/MPLS/load_StreamEntry.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +def load_StreamEntry(fobj, length2, length2a, length2b): + # NOTE: see https://github.com/lerks/BluRay/wiki/StreamEntry + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length2c = 0 # [B] + + # Read the binary data ... + ans = {} + ans[u"Length"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1 + if ans[u"Length"] != 0: + ans[u"StreamType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + if ans[u"StreamType"] == int(0x01): + tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 + ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) + if ans[u"StreamType"] == int(0x02): + ans[u"RefToSubPathID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + ans[u"RefToSubClipID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 + ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) + if ans[u"StreamType"] == int(0x03): + tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 + ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) + if ans[u"StreamType"] == int(0x04): + ans[u"RefToSubPathID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + ans[u"RefToSubClipID"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1; length2b += 1; length2c += 1 + tmp, = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2; length2b += 2; length2c += 2 + ans[u"RefToStreamPID"] = "0x{0:<04x}".format(tmp) + + # Pad out the read ... + if length2c != ans[u"Length"]: + l = ans[u"Length"] - length2c # [B] + fobj.read(l); length2 += l; length2a += l; length2b += l; length2c += l + + # Return answer ... + return ans, length2, length2a, length2b, length2c diff --git a/pymplschapters/MPLS/load_SubPath.py b/pymplschapters/MPLS/load_SubPath.py new file mode 100644 index 0000000..8fb6c8c --- /dev/null +++ b/pymplschapters/MPLS/load_SubPath.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +def load_SubPath(fobj, length2): + # NOTE: see https://github.com/lerks/BluRay/wiki/SubPath + + # Import modules ... + import struct + + # Load sub-functions ... + from .load_SubPlayItem import load_SubPlayItem + + # Initialize variables ... + ans = {} + length2a = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">I", fobj.read(4)); length2 += 4 + fobj.read(1); length2 += 1; length2a += 1 + ans[u"SubPathType"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 + ans[u"MiscFlags1"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 + ans[u"NumberOfSubPlayItems"], = struct.unpack(u">B", fobj.read(1)); length2 += 1; length2a += 1 + ans[u"SubPlayItems"] = [] + for i in range(ans[u"NumberOfSubPlayItems"]): + res, length2, length2a, length2b = load_SubPlayItem(fobj, length2, length2a) + ans[u"SubPlayItems"].append(res) + + # Pad out the read ... + if length2a != ans[u"Length"]: + l = ans[u"Length"] - length2a # [B] + fobj.read(l); length2 += l; length2a += l + + return ans, length2, length2a diff --git a/pymplschapters/MPLS/load_SubPlayItem.py b/pymplschapters/MPLS/load_SubPlayItem.py new file mode 100644 index 0000000..df65e3b --- /dev/null +++ b/pymplschapters/MPLS/load_SubPlayItem.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +def load_SubPlayItem(fobj, length2, length2a): + # NOTE: see https://github.com/lerks/BluRay/wiki/SubPlayItem + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length2b = 0 # [B] + + # Read the binary data ... + ans[u"Length"], = struct.unpack(u">H", fobj.read(2)); length2 += 2; length2a += 2 + + # NOTE: SubPlayItem is not implemented + + # Pad out the read ... + if length2b != ans[u"Length"]: + l = ans[u"Length"] - length2b # [B] + fobj.read(l); length2 += l; length2a += l; length2b += l + + # Return answer ... + return ans, length2, length2a, length2b diff --git a/pymplschapters/MPLS/load_header.py b/pymplschapters/MPLS/load_header.py new file mode 100644 index 0000000..1bfadd6 --- /dev/null +++ b/pymplschapters/MPLS/load_header.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +def load_header(fobj): + # NOTE: see https://github.com/lerks/BluRay/wiki/MPLS + + # Import modules ... + import struct + + # Initialize variables ... + ans = {} + length0 = 0 # [B] + + # Read the binary data ... + ans[u"TypeIndicator"] = fobj.read(4); length0 += 4 + ans[u"VersionNumber"] = fobj.read(4); length0 += 4 + ans[u"PlayListStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 + ans[u"PlayListMarkStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 + ans[u"ExtensionDataStartAddress"], = struct.unpack(u">I", fobj.read(4)); length0 += 4 + fobj.read(20); length0 += 20 + + # Return answer ... + return ans, length0 diff --git a/pymplschapters/__init__.py b/pymplschapters/__init__.py new file mode 100644 index 0000000..8813bb7 --- /dev/null +++ b/pymplschapters/__init__.py @@ -0,0 +1,75 @@ +#!/usr/bin/python3 + +import os +import argparse +import datetime +from . import MPLS +from lxml import etree + +# -------------------------------- +# Arguments +# -------------------------------- +ArgParser = argparse.ArgumentParser() +ArgParser.add_argument('-p', '--playlist', help='Input path to an MPLS playlist', required=True) +args = ArgParser.parse_args() + +def get_chapters(mpls): + header, _ = MPLS.load_header(mpls) + + mpls.seek(header['PlayListStartAddress'], os.SEEK_SET) + pl, _ = MPLS.load_PlayList(mpls) + pl = pl['PlayItems'] + + mpls.seek(header['PlayListMarkStartAddress'], os.SEEK_SET) + marks, _ = MPLS.load_PlayListMark(mpls) + marks = marks["PlayListMarks"] + + for i, playItem in enumerate(pl): + chapters = [] + playItemMarks = [x for x in marks if x["MarkType"] == 1 and x["RefToPlayItemID"] == i] + offset = playItemMarks[0]["MarkTimeStamp"] + if playItem["INTime"] < offset: + offset = playItem["INTime"] + for n, mark in enumerate(playItemMarks): + duration = ((mark["MarkTimeStamp"] - offset) / 45000) * 1000 + timespan = str(datetime.timedelta(milliseconds=duration)) + if timespan == "0:00:00": + timespan = timespan + ".000000" + if timespan.startswith("0:"): + timespan = "0" + timespan + chapters.append({ + "clip": playItem["ClipInformationFileName"] + "." + playItem["ClipCodecIdentifier"].lower(), + "number": n+1, + "duration": duration, + "timespan": timespan + }) + yield chapters + + return chapters + +with open(args.playlist, "rb") as playlist_handle: + + for file_with_chapter in get_chapters(playlist_handle): + Chapters = etree.Element("Chapters") + EditionEntry = etree.SubElement(Chapters, "EditionEntry") + EditionFlagHidden = etree.SubElement(EditionEntry, "EditionFlagHidden") + EditionFlagHidden.text = "0" + EditionFlagDefault = etree.SubElement(EditionEntry, "EditionFlagDefault") + EditionFlagDefault.text = "0" + for chapter in file_with_chapter: + ChapterAtom = etree.SubElement(EditionEntry, "ChapterAtom") + ChapterDisplay = etree.SubElement(ChapterAtom, "ChapterDisplay") + ChapterString = etree.SubElement(ChapterDisplay, "ChapterString") + ChapterString.text = "Chapter " + str(chapter["number"]).zfill(2) + ChapterLanguage = etree.SubElement(ChapterDisplay, "ChapterLanguage") + ChapterLanguage.text = "eng" + ChapterTimeStart = etree.SubElement(ChapterAtom, "ChapterTimeStart") + ChapterTimeStart.text = chapter["timespan"] + ChapterFlagHidden = etree.SubElement(ChapterAtom, "ChapterFlagHidden") + ChapterFlagHidden.text = "0" + ChapterFlagEnabled = etree.SubElement(ChapterAtom, "ChapterFlagEnabled") + ChapterFlagEnabled.text = "1" + clip = file_with_chapter[0]["clip"] + with open(os.path.join(os.path.dirname(args.playlist), os.path.splitext(os.path.basename(args.playlist))[0] + "_" + clip.split('.')[0] + ".xml"), 'wb') as f: + f.write(etree.tostring(Chapters, encoding='utf-8', doctype="", xml_declaration=True, pretty_print=True)) + print("Extracted chapters for " + clip) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0a70a20 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lxml>=4.4.1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6e8eed8 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +from setuptools import setup, find_packages + +with open("README.md", "r") as f: + readme = f.read() + +setup( + name="pymplschapters", + version="1.0.0", + author="PRAGMA", + author_email="pragma.exe@gmail.com", + description="Extract chapters from a blu-ray mpls to a matroska recognized xml file", + license='MIT', + long_description=readme, + long_description_content_type="text/markdown", + url="https://github.com/imPRAGMA/pymplschapters", + packages=find_packages(), + install_requires=[ + 'lxml>=4.4.1' + ], + classifiers=[ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ] +) \ No newline at end of file