├── .deepsource.toml ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .vscode └── extensions.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── README.md ├── banner.png ├── banner.xcf └── preview.png ├── make.ps1 ├── poetry.lock ├── pslipstream ├── __init__.py ├── __main__.py ├── config.py ├── device.py ├── dvd.py ├── exceptions.py ├── gui │ ├── __init__.py │ ├── main_window.py │ ├── main_window.ui │ ├── main_window_ui.py │ └── workers.py ├── helpers.py ├── main.py └── static │ ├── README.md │ ├── img │ ├── icon.ico │ ├── icon.xcf │ ├── info-circle.svg │ ├── music-disc-with-luster.svg │ └── refresh.svg │ └── style.qss ├── pyinstaller.py ├── pyproject.toml └── setup.iss /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | exclude_patterns = ["**/gui/main_window_ui.py"] 4 | 5 | [[analyzers]] 6 | name = "python" 7 | enabled = true 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | max_line_length = 120 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{feature,json,md,yaml,yml}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | permissions: 3 | contents: "write" 4 | id-token: "write" 5 | packages: "write" 6 | pull-requests: "read" 7 | 8 | on: 9 | push: 10 | tags: 11 | - "v*" 12 | 13 | jobs: 14 | tagged-release: 15 | name: Tagged Release 16 | runs-on: windows-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: "true" 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: "3.11.x" 25 | - name: Install Poetry 26 | uses: abatilo/actions-poetry@v2 27 | with: 28 | poetry-version: 1.6.1 29 | - name: Install project 30 | run: poetry install --only main -E pyinstaller 31 | - name: Build Windows Executable with PyInstaller 32 | run: poetry run python pyinstaller.py 33 | - name: Create Windows Installer with Inno Setup 34 | run: | 35 | iscc setup.iss 36 | mv dist/Slipstream-Setup.exe . 37 | - name: Build Portable EXE with PyInstaller 38 | run: | 39 | poetry run python pyinstaller.py --one-file 40 | mv dist/Slipstream.exe Slipstream-${{ github.ref_name }}-portable.exe 41 | - name: Build project 42 | run: poetry build 43 | - name: Upload wheel 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: Python Wheel 47 | path: "dist/*.whl" 48 | - name: Deploy release 49 | uses: marvinpinto/action-automatic-releases@latest 50 | with: 51 | prerelease: false 52 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 53 | files: | 54 | dist/*.whl 55 | Slipstream-Setup.exe 56 | Slipstream-*-portable.exe 57 | - name: Publish to PyPI 58 | env: 59 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 60 | run: poetry publish 61 | -------------------------------------------------------------------------------- /.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 | lint: 11 | runs-on: windows-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.11" 18 | - name: Install poetry 19 | uses: abatilo/actions-poetry@v2 20 | with: 21 | poetry-version: 1.6.1 22 | - name: Install project 23 | run: poetry install 24 | - name: Run pre-commit which does various checks 25 | run: poetry run pre-commit run --all-files --show-diff-on-failure 26 | build: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | python-version: ["3.8", "3.9", "3.10", "3.11"] 31 | poetry-version: [1.6.1] 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v4 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | - name: Install poetry 39 | uses: abatilo/actions-poetry@v2 40 | with: 41 | poetry-version: ${{ matrix.poetry-version }} 42 | - name: Install project 43 | run: poetry install --only main 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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/libdvdcss"] 2 | path = submodules/libdvdcss 3 | url = https://github.com/allienx/libdvdcss-dll 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | repos: 5 | - repo: https://github.com/mtkennerly/pre-commit-hooks 6 | rev: v0.3.0 7 | hooks: 8 | - id: poetry-ruff 9 | - id: poetry-mypy 10 | - repo: https://github.com/pycqa/isort 11 | rev: 5.12.0 12 | hooks: 13 | - id: isort 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.4.0 16 | hooks: 17 | - id: trailing-whitespace 18 | args: [--markdown-linebreak-ext=md] 19 | - id: end-of-file-fixer 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "EditorConfig.EditorConfig", 6 | "streetsidesoftware.code-spell-checker", 7 | "charliermarsh.ruff", 8 | "ms-python.isort", 9 | "ms-python.mypy-type-checker" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2023-10-12 9 | 10 | New Beginnings - Initial release with a consistent and coherent project structure. 11 | Previous 0.x releases have been yanked and history has been rewritten to slim the repo commit count. 12 | 13 | [1.0.0]: https://github.com/rlaphoenix/slipstream/releases/tag/v1.0.0 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This project is managed using [Poetry](https://python-poetry.org), a fantastic Python packaging and dependency manager. 4 | Install the latest version of Poetry before continuing. Development currently requires Python 3.8+. 5 | 6 | ## Set up 7 | 8 | Starting from Zero? Not sure where to begin? Here's steps on setting up this Python project using Poetry. Note that 9 | Poetry installation instructions should be followed from the Poetry Docs: https://python-poetry.org/docs/#installation 10 | 11 | 1. While optional, It's recommended to configure Poetry to install Virtual environments within project folders: 12 | ```shell 13 | poetry config virtualenvs.in-project true 14 | ``` 15 | This makes it easier for Visual Studio Code to detect the Virtual Environment, as well as other IDEs and systems. 16 | I've also had issues with Poetry creating duplicate Virtual environments in the default folder for an unknown 17 | reason which quickly filled up my System storage. 18 | 2. Clone the Repository: 19 | ```shell 20 | git clone https://github.com/rlaphoenix/slipstream 21 | cd slipstream 22 | ``` 23 | 3. Install the Project with Poetry: 24 | ```shell 25 | poetry install 26 | ``` 27 | This creates a Virtual environment and then installs all project dependencies and executables into the Virtual 28 | environment. Your System Python environment is not affected at all. 29 | 4. Now activate the Virtual environment: 30 | ```shell 31 | poetry shell 32 | ``` 33 | Note: 34 | - You can alternatively just prefix `poetry run` to any command you wish to run under the Virtual environment. 35 | - I recommend entering the Virtual environment and all further instructions will have assumed you did. 36 | - JetBrains PyCharm has integrated support for Poetry and automatically enters Poetry Virtual environments, assuming 37 | the Python Interpreter on the bottom right is set up correctly. 38 | - For more information, see: https://python-poetry.org/docs/basic-usage/#using-your-virtual-environment 39 | 5. Install Pre-commit tooling to ensure safe and quality commits: 40 | ```shell 41 | pre-commit install 42 | ``` 43 | 44 | ## Building Source and Wheel distributions 45 | 46 | poetry build 47 | 48 | You can optionally specify `-f` to build `sdist` or `wheel` only. 49 | Built files can be found in the `/dist` directory. 50 | 51 | ## Packing with PyInstaller 52 | 53 | poetry install -E pyinstaller 54 | poetry run python pyinstaller.py 55 | 56 | You may do both `.exe` and `Folder` builds. See `ONE_FILE` bool in `pyinstaller.py`. 57 | The frozen build will be available in the `/dist` folder. 58 | 59 | ## Creating Windows Installers 60 | 61 | 1. Install the [Inno Setup Compiler](https://jrsoftware.org/isdl.php). 62 | 2. Right-click the [setup.iss](setup.iss) file in the root folder and click Compile. 63 | 3. The Windows Installer will be available in the `/dist` folder. 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](assets/banner.png) 2 | 3 | [![Linter: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 4 | [![Dependency management: Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) 5 | [![Python version](https://img.shields.io/pypi/pyversions/pslipstream)](https://pypi.python.org/pypi/pslipstream) 6 | [![Release version](https://img.shields.io/pypi/v/pslipstream)](https://pypi.python.org/pypi/pslipstream) 7 | [![DeepSource issues](https://deepsource.io/gh/rlaphoenix/slipstream.svg/?label=active+issues)](https://deepsource.io/gh/rlaphoenix/slipstream) 8 | [![Build status](https://github.com/rlaphoenix/slipstream/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/rlaphoenix/slipstream/actions/workflows/ci.yml) 9 | 10 | Slipstream's goal is to provide the user's a dead-simple process for backing up their legally owned home-media to a 11 | wide array of formats, including a full backup. Slipstream can be used with it's GUI, as CLI, or as an importable 12 | package. 13 | 14 | It's trying to be different from the other solutions out there by providing as much information about the home-media 15 | as one could need while being stupid simple to use. 16 | 17 | ![Preview](assets/preview.png) 18 | 19 | ## Installation 20 | 21 | *Windows Installers are available on the [Releases] page.* 22 | 23 | Alternatively you can download and install Slipstream from PIP/PyPI: 24 | 25 | $ pip install pslipstream 26 | 27 | > **Note** 28 | If pip gives you a warning about a path not being in your PATH environment variable then promptly add that path then 29 | close all open command prompt Windows, or running `slipstream` won't work as it will not be recognized as a program. 30 | 31 | Voilà 🎉 - You now have the `pslipstream` package installed! 32 | Launch it by typing `slipstream` in your Terminal or Windows Run. 33 | 34 | [Releases]: 35 | 36 | ## To-do 37 | 38 | - [X] Craft GUI with Qt. 39 | - [x] Create a file based settings system. 40 | - [x] Add drive selection option. 41 | - [X] Add DVD backup support, using libdvdcss. 42 | - [X] Add information window with details about the DVD ISO. 43 | - [x] Write PyInstaller spec file. 44 | - [ ] Add information window with details about the DVD-Video data, like Layer count, titles, languages, subtitles, codecs, e.t.c. 45 | - [ ] Add support for remuxing to Matroska Video (MKV) with MKVToolnix. 46 | - [ ] Add the ability to choose to remux by Title ID's. 47 | - [ ] Add the ability to choose to remux by VOB ID, and VOB CELL's. 48 | - [ ] Add the ability to choose which tracks of a title to output rather than all available. 49 | - [ ] Add Blu-ray backup support, using libaacs. 50 | 51 | ## Licensing 52 | 53 | This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE). 54 | You can find a copy of the license in the LICENSE file in the root folder. 55 | 56 | - [Music disc icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/music-disc) 57 | - [Info icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/info) 58 | - [Refresh icons created by Pixel perfect - Flaticon](https://www.flaticon.com/free-icons/refresh) 59 | 60 | * * * 61 | 62 | © rlaphoenix 2020-2023 63 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | Files used for Project metadata or other misc use cases. Nothing in here should be used within Slipstream's Python 4 | package (the actual code base). 5 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlaphoenix/Slipstream/5e412deaa58b6e4e4bdecdfde1b492b20ca9ca4b/assets/banner.png -------------------------------------------------------------------------------- /assets/banner.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlaphoenix/Slipstream/5e412deaa58b6e4e4bdecdfde1b492b20ca9ca4b/assets/banner.xcf -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlaphoenix/Slipstream/5e412deaa58b6e4e4bdecdfde1b492b20ca9ca4b/assets/preview.png -------------------------------------------------------------------------------- /make.ps1: -------------------------------------------------------------------------------- 1 | $runUic = Read-Host 'Recompile GUI from UI file? (y/n) [y]' 2 | if ($runUic -eq 'y') { 3 | pyside6-uic pslipstream/gui/main_window.ui -o pslipstream/gui/main_window_ui.py 4 | ruff check pslipstream/gui/main_window_ui.py --fix 5 | isort pslipstream/gui/main_window_ui.py 6 | # ignore type checks in the uic file 7 | $filePath = 'pslipstream/gui/main_window_ui.py' 8 | "# pylint: disable-all`n# type: ignore`n$((Get-Content -Path $filePath) -join [System.Environment]::NewLine)" | Set-Content -Path $filePath 9 | } 10 | 11 | $runPyInstaller = Read-Host 'Build to a self-contained folder via PyInstaller? (Y/n)' 12 | if ($runPyInstaller -eq 'y') { 13 | & 'poetry' run python -OO pyinstaller.py 14 | Write-Output 'Done! /dist contains the PyInstaller build.' 15 | $executePyInstaller = Read-Host 'Execute the frozen build''s executable? (Y/n)' 16 | if ($executePyInstaller -eq 'y') { 17 | & 'dist/Slipstream/Slipstream.exe' ($args | Select-Object -Skip 1) 18 | exit 19 | } 20 | } 21 | 22 | $runInnoSetup = Read-Host 'Create a Windows installer via Inno Setup? (Y/n)' 23 | if ($runInnoSetup -eq 'y') { 24 | & 'iscc' setup.iss 25 | Write-Output 'Done! /dist contains the Inno Setup installer.' 26 | } 27 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "altgraph" 5 | version = "0.17.4" 6 | description = "Python graph (network) package" 7 | optional = true 8 | python-versions = "*" 9 | files = [ 10 | {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, 11 | {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, 12 | ] 13 | 14 | [[package]] 15 | name = "appdirs" 16 | version = "1.4.4" 17 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 18 | optional = false 19 | python-versions = "*" 20 | files = [ 21 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 22 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 23 | ] 24 | 25 | [[package]] 26 | name = "cfgv" 27 | version = "3.4.0" 28 | description = "Validate configuration and produce human readable error messages." 29 | optional = false 30 | python-versions = ">=3.8" 31 | files = [ 32 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 33 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 34 | ] 35 | 36 | [[package]] 37 | name = "click" 38 | version = "8.1.7" 39 | description = "Composable command line interface toolkit" 40 | optional = false 41 | python-versions = ">=3.7" 42 | files = [ 43 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 44 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 45 | ] 46 | 47 | [package.dependencies] 48 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.6" 53 | description = "Cross-platform colored terminal text." 54 | optional = false 55 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 56 | files = [ 57 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 58 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 59 | ] 60 | 61 | [[package]] 62 | name = "coloredlogs" 63 | version = "15.0.1" 64 | description = "Colored terminal output for Python's logging module" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 67 | files = [ 68 | {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, 69 | {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, 70 | ] 71 | 72 | [package.dependencies] 73 | humanfriendly = ">=9.1" 74 | 75 | [package.extras] 76 | cron = ["capturer (>=2.4)"] 77 | 78 | [[package]] 79 | name = "distlib" 80 | version = "0.3.8" 81 | description = "Distribution utilities" 82 | optional = false 83 | python-versions = "*" 84 | files = [ 85 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 86 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 87 | ] 88 | 89 | [[package]] 90 | name = "filelock" 91 | version = "3.15.4" 92 | description = "A platform independent file lock." 93 | optional = false 94 | python-versions = ">=3.8" 95 | files = [ 96 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 97 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 98 | ] 99 | 100 | [package.extras] 101 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 102 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] 103 | typing = ["typing-extensions (>=4.8)"] 104 | 105 | [[package]] 106 | name = "humanfriendly" 107 | version = "10.0" 108 | description = "Human friendly output for text interfaces using Python" 109 | optional = false 110 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 111 | files = [ 112 | {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, 113 | {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, 114 | ] 115 | 116 | [package.dependencies] 117 | pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} 118 | 119 | [[package]] 120 | name = "identify" 121 | version = "2.6.0" 122 | description = "File identification library for Python" 123 | optional = false 124 | python-versions = ">=3.8" 125 | files = [ 126 | {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, 127 | {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, 128 | ] 129 | 130 | [package.extras] 131 | license = ["ukkonen"] 132 | 133 | [[package]] 134 | name = "importlib-metadata" 135 | version = "8.0.0" 136 | description = "Read metadata from Python packages" 137 | optional = true 138 | python-versions = ">=3.8" 139 | files = [ 140 | {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, 141 | {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, 142 | ] 143 | 144 | [package.dependencies] 145 | zipp = ">=0.5" 146 | 147 | [package.extras] 148 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 149 | perf = ["ipython"] 150 | test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] 151 | 152 | [[package]] 153 | name = "isort" 154 | version = "5.13.2" 155 | description = "A Python utility / library to sort Python imports." 156 | optional = false 157 | python-versions = ">=3.8.0" 158 | files = [ 159 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 160 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 161 | ] 162 | 163 | [package.extras] 164 | colors = ["colorama (>=0.4.6)"] 165 | 166 | [[package]] 167 | name = "jsonpickle" 168 | version = "3.2.2" 169 | description = "Python library for serializing arbitrary object graphs into JSON" 170 | optional = false 171 | python-versions = ">=3.7" 172 | files = [ 173 | {file = "jsonpickle-3.2.2-py3-none-any.whl", hash = "sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa"}, 174 | {file = "jsonpickle-3.2.2.tar.gz", hash = "sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c"}, 175 | ] 176 | 177 | [package.extras] 178 | docs = ["furo", "rst.linker (>=1.9)", "sphinx"] 179 | packaging = ["build", "twine"] 180 | testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"] 181 | 182 | [[package]] 183 | name = "macholib" 184 | version = "1.16.3" 185 | description = "Mach-O header analysis and editing" 186 | optional = true 187 | python-versions = "*" 188 | files = [ 189 | {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, 190 | {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, 191 | ] 192 | 193 | [package.dependencies] 194 | altgraph = ">=0.17" 195 | 196 | [[package]] 197 | name = "mypy" 198 | version = "1.10.1" 199 | description = "Optional static typing for Python" 200 | optional = false 201 | python-versions = ">=3.8" 202 | files = [ 203 | {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, 204 | {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, 205 | {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, 206 | {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, 207 | {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, 208 | {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, 209 | {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, 210 | {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, 211 | {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, 212 | {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, 213 | {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, 214 | {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, 215 | {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, 216 | {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, 217 | {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, 218 | {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, 219 | {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, 220 | {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, 221 | {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, 222 | {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, 223 | {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, 224 | {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, 225 | {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, 226 | {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, 227 | {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, 228 | {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, 229 | {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, 230 | ] 231 | 232 | [package.dependencies] 233 | mypy-extensions = ">=1.0.0" 234 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 235 | typing-extensions = ">=4.1.0" 236 | 237 | [package.extras] 238 | dmypy = ["psutil (>=4.0)"] 239 | install-types = ["pip"] 240 | mypyc = ["setuptools (>=50)"] 241 | reports = ["lxml"] 242 | 243 | [[package]] 244 | name = "mypy-extensions" 245 | version = "1.0.0" 246 | description = "Type system extensions for programs checked with the mypy type checker." 247 | optional = false 248 | python-versions = ">=3.5" 249 | files = [ 250 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 251 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 252 | ] 253 | 254 | [[package]] 255 | name = "nodeenv" 256 | version = "1.9.1" 257 | description = "Node.js virtual environment builder" 258 | optional = false 259 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 260 | files = [ 261 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 262 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 263 | ] 264 | 265 | [[package]] 266 | name = "packaging" 267 | version = "24.1" 268 | description = "Core utilities for Python packages" 269 | optional = true 270 | python-versions = ">=3.8" 271 | files = [ 272 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 273 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 274 | ] 275 | 276 | [[package]] 277 | name = "pefile" 278 | version = "2023.2.7" 279 | description = "Python PE parsing module" 280 | optional = true 281 | python-versions = ">=3.6.0" 282 | files = [ 283 | {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, 284 | {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, 285 | ] 286 | 287 | [[package]] 288 | name = "platformdirs" 289 | version = "4.2.2" 290 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 291 | optional = false 292 | python-versions = ">=3.8" 293 | files = [ 294 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 295 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 296 | ] 297 | 298 | [package.extras] 299 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 300 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 301 | type = ["mypy (>=1.8)"] 302 | 303 | [[package]] 304 | name = "pre-commit" 305 | version = "3.5.0" 306 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 307 | optional = false 308 | python-versions = ">=3.8" 309 | files = [ 310 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, 311 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, 312 | ] 313 | 314 | [package.dependencies] 315 | cfgv = ">=2.0.0" 316 | identify = ">=1.0.0" 317 | nodeenv = ">=0.11.1" 318 | pyyaml = ">=5.1" 319 | virtualenv = ">=20.10.0" 320 | 321 | [[package]] 322 | name = "pycdlib" 323 | version = "1.14.0" 324 | description = "Pure python ISO manipulation library" 325 | optional = false 326 | python-versions = "*" 327 | files = [ 328 | {file = "pycdlib-1.14.0-py2.py3-none-any.whl", hash = "sha256:a905827335f0066af3fd416c5cf9b1f29dffaf4d0914b714555213d1809f38d4"}, 329 | {file = "pycdlib-1.14.0.tar.gz", hash = "sha256:8ec306b31d9c850f28c5fda52438d904edd1e8fcf862c5ffd756272efac9f422"}, 330 | ] 331 | 332 | [[package]] 333 | name = "pydvdcss" 334 | version = "1.4.0" 335 | description = "Python wrapper for VideoLAN's libdvdcss." 336 | optional = false 337 | python-versions = ">=3.8,<4.0" 338 | files = [ 339 | {file = "pydvdcss-1.4.0-py3-none-any.whl", hash = "sha256:ece541509004f74da039553f8e623dc808d0c8722c0e244444d6d4e70a8c4b83"}, 340 | {file = "pydvdcss-1.4.0.tar.gz", hash = "sha256:aee224c5c68b42eff0340f12a79eb3e869aca6dd1795692466c8806e2dc40de3"}, 341 | ] 342 | 343 | [package.extras] 344 | docs = ["Sphinx (>=7.1.2,<8.0.0)", "dunamai (>=1.19.0,<2.0.0)", "furo (>=2023.9.10,<2024.0.0)", "myst-parser (>=2.0.0,<3.0.0)"] 345 | 346 | [[package]] 347 | name = "pydvdid-m" 348 | version = "1.1.1" 349 | description = "Pure Python implementation of the Windows API method IDvdInfo2::GetDiscID." 350 | optional = false 351 | python-versions = ">=3.7,<4.0" 352 | files = [ 353 | {file = "pydvdid-m-1.1.1.tar.gz", hash = "sha256:1ff51d29347301d3dd69ade43293f30f1970a38fa3f4e9114897fe1781642f72"}, 354 | {file = "pydvdid_m-1.1.1-py3-none-any.whl", hash = "sha256:0a4d2563a2228daf84f5a291178c48995fa4ed7ece6f8ac54c11895322a51808"}, 355 | ] 356 | 357 | [package.dependencies] 358 | pycdlib = ">=1.12.0,<2.0.0" 359 | python-dateutil = ">=2.8.2,<3.0.0" 360 | 361 | [package.extras] 362 | win-raw-dev = ["pywin32 (==301)"] 363 | 364 | [[package]] 365 | name = "pyinstaller" 366 | version = "6.9.0" 367 | description = "PyInstaller bundles a Python application and all its dependencies into a single package." 368 | optional = true 369 | python-versions = "<3.13,>=3.8" 370 | files = [ 371 | {file = "pyinstaller-6.9.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ced2e83acf222b936ea94abc5a5cc96588705654b39138af8fb321d9cf2b954"}, 372 | {file = "pyinstaller-6.9.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f18a3d551834ef8fb7830d48d4cc1527004d0e6b51ded7181e78374ad6111846"}, 373 | {file = "pyinstaller-6.9.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f2fc568de3d6d2a176716a3fc9f20da06d351e8bea5ddd10ecb5659fce3a05b0"}, 374 | {file = "pyinstaller-6.9.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:a0f378f64ad0655d11ade9fde7877e7573fd3d5066231608ce7dfa9040faecdd"}, 375 | {file = "pyinstaller-6.9.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bf0c13c5a8560c89540746ae742f4f4b82290e95a6b478374d9f34959fe25d6"}, 376 | {file = "pyinstaller-6.9.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:da994aba14c5686db88796684de265a8665733b4df09b939f7ebdf097d18df72"}, 377 | {file = "pyinstaller-6.9.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:4e3e50743c091a06e6d01c59bdd6d03967b453ee5384a9e790759be4129db4a4"}, 378 | {file = "pyinstaller-6.9.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b041be2fe78da47a269604d62c940d68c62f9a3913bdf64af4123f7689d47099"}, 379 | {file = "pyinstaller-6.9.0-py3-none-win32.whl", hash = "sha256:2bf4de17a1c63c0b797b38e13bfb4d03b5ee7c0a68e28b915a7eaacf6b76087f"}, 380 | {file = "pyinstaller-6.9.0-py3-none-win_amd64.whl", hash = "sha256:43709c70b1da8441a730327a8ed362bfcfdc3d42c1bf89f3e2b0a163cc4e7d33"}, 381 | {file = "pyinstaller-6.9.0-py3-none-win_arm64.whl", hash = "sha256:f15c1ef11ed5ceb32447dfbdab687017d6adbef7fc32aa359d584369bfe56eda"}, 382 | {file = "pyinstaller-6.9.0.tar.gz", hash = "sha256:f4a75c552facc2e2a370f1e422b971b5e5cdb4058ff38cea0235aa21fc0b378f"}, 383 | ] 384 | 385 | [package.dependencies] 386 | altgraph = "*" 387 | importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} 388 | macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} 389 | packaging = ">=22.0" 390 | pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} 391 | pyinstaller-hooks-contrib = ">=2024.7" 392 | pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} 393 | setuptools = ">=42.0.0" 394 | 395 | [package.extras] 396 | completion = ["argcomplete"] 397 | hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] 398 | 399 | [[package]] 400 | name = "pyinstaller-hooks-contrib" 401 | version = "2024.7" 402 | description = "Community maintained hooks for PyInstaller" 403 | optional = true 404 | python-versions = ">=3.7" 405 | files = [ 406 | {file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"}, 407 | {file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"}, 408 | ] 409 | 410 | [package.dependencies] 411 | importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} 412 | packaging = ">=22.0" 413 | setuptools = ">=42.0.0" 414 | 415 | [[package]] 416 | name = "pyreadline3" 417 | version = "3.4.1" 418 | description = "A python implementation of GNU readline." 419 | optional = false 420 | python-versions = "*" 421 | files = [ 422 | {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, 423 | {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, 424 | ] 425 | 426 | [[package]] 427 | name = "pyside6-essentials" 428 | version = "6.6.3.1" 429 | description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" 430 | optional = false 431 | python-versions = "<3.13,>=3.8" 432 | files = [ 433 | {file = "PySide6_Essentials-6.6.3.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:6c16530b63079711783796584b640cc80a347e0b2dc12651aa2877265df7a008"}, 434 | {file = "PySide6_Essentials-6.6.3.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1f41f357ce2384576581e76c9c3df1c4fa5b38e347f0bcd0cae7c5bce42a917c"}, 435 | {file = "PySide6_Essentials-6.6.3.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:27034525fdbdd21ef21f20fcd7aaf5c2ffe26f2bcf5269a69dd9492dec7e92aa"}, 436 | {file = "PySide6_Essentials-6.6.3.1-cp38-abi3-win_amd64.whl", hash = "sha256:31f7e70ada44d3cdbe6686670b3df036c720cfeb1dced0f7704e5f5a4be6a764"}, 437 | ] 438 | 439 | [package.dependencies] 440 | shiboken6 = "6.6.3.1" 441 | 442 | [[package]] 443 | name = "python-dateutil" 444 | version = "2.9.0.post0" 445 | description = "Extensions to the standard Python datetime module" 446 | optional = false 447 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 448 | files = [ 449 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 450 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 451 | ] 452 | 453 | [package.dependencies] 454 | six = ">=1.5" 455 | 456 | [[package]] 457 | name = "pywin32" 458 | version = "306" 459 | description = "Python for Window Extensions" 460 | optional = false 461 | python-versions = "*" 462 | files = [ 463 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 464 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 465 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 466 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 467 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 468 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 469 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 470 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 471 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 472 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 473 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 474 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 475 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 476 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 477 | ] 478 | 479 | [[package]] 480 | name = "pywin32-ctypes" 481 | version = "0.2.2" 482 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 483 | optional = true 484 | python-versions = ">=3.6" 485 | files = [ 486 | {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, 487 | {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, 488 | ] 489 | 490 | [[package]] 491 | name = "pyyaml" 492 | version = "6.0.1" 493 | description = "YAML parser and emitter for Python" 494 | optional = false 495 | python-versions = ">=3.6" 496 | files = [ 497 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 498 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 499 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 500 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 501 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 502 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 503 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 504 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 505 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 506 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 507 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 508 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 509 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 510 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 511 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 512 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 513 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 514 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 515 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 516 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 517 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 518 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 519 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 520 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 521 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 522 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 523 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 524 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 525 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 526 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 527 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 528 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 529 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 530 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 531 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 532 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 533 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 534 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 535 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 536 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 537 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 538 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 539 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 540 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 541 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 542 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 543 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 544 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 545 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 546 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 547 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 548 | ] 549 | 550 | [[package]] 551 | name = "ruff" 552 | version = "0.5.1" 553 | description = "An extremely fast Python linter and code formatter, written in Rust." 554 | optional = false 555 | python-versions = ">=3.7" 556 | files = [ 557 | {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, 558 | {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, 559 | {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, 560 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, 561 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, 562 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, 563 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, 564 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, 565 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, 566 | {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, 567 | {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, 568 | {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, 569 | {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, 570 | {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, 571 | {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, 572 | {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, 573 | {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, 574 | {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, 575 | ] 576 | 577 | [[package]] 578 | name = "setuptools" 579 | version = "70.3.0" 580 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 581 | optional = true 582 | python-versions = ">=3.8" 583 | files = [ 584 | {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, 585 | {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, 586 | ] 587 | 588 | [package.extras] 589 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 590 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 591 | 592 | [[package]] 593 | name = "shiboken6" 594 | version = "6.6.3.1" 595 | description = "Python/C++ bindings helper module" 596 | optional = false 597 | python-versions = "<3.13,>=3.8" 598 | files = [ 599 | {file = "shiboken6-6.6.3.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:2a8df586aa9eb629388b368d3157893083c5217ed3eb637bf182d1948c823a0f"}, 600 | {file = "shiboken6-6.6.3.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b1aeff0d79d84ddbdc9970144c1bbc3a52fcb45618d1b33d17d57f99f1246d45"}, 601 | {file = "shiboken6-6.6.3.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:902d9e126ac57cc3841cdc50ba38d53948b40cf667538172f253c4ae7b2dcb2c"}, 602 | {file = "shiboken6-6.6.3.1-cp38-abi3-win_amd64.whl", hash = "sha256:88494b5e08a1f235efddbe2b0b225a3a66e07d72b6091fcc2fc5448572453649"}, 603 | ] 604 | 605 | [[package]] 606 | name = "six" 607 | version = "1.16.0" 608 | description = "Python 2 and 3 compatibility utilities" 609 | optional = false 610 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 611 | files = [ 612 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 613 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 614 | ] 615 | 616 | [[package]] 617 | name = "tomli" 618 | version = "2.0.1" 619 | description = "A lil' TOML parser" 620 | optional = false 621 | python-versions = ">=3.7" 622 | files = [ 623 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 624 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 625 | ] 626 | 627 | [[package]] 628 | name = "tqdm" 629 | version = "4.66.4" 630 | description = "Fast, Extensible Progress Meter" 631 | optional = false 632 | python-versions = ">=3.7" 633 | files = [ 634 | {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, 635 | {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, 636 | ] 637 | 638 | [package.dependencies] 639 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 640 | 641 | [package.extras] 642 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 643 | notebook = ["ipywidgets (>=6)"] 644 | slack = ["slack-sdk"] 645 | telegram = ["requests"] 646 | 647 | [[package]] 648 | name = "types-python-dateutil" 649 | version = "2.9.0.20240316" 650 | description = "Typing stubs for python-dateutil" 651 | optional = false 652 | python-versions = ">=3.8" 653 | files = [ 654 | {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, 655 | {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, 656 | ] 657 | 658 | [[package]] 659 | name = "typing-extensions" 660 | version = "4.12.2" 661 | description = "Backported and Experimental Type Hints for Python 3.8+" 662 | optional = false 663 | python-versions = ">=3.8" 664 | files = [ 665 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 666 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 667 | ] 668 | 669 | [[package]] 670 | name = "virtualenv" 671 | version = "20.26.3" 672 | description = "Virtual Python Environment builder" 673 | optional = false 674 | python-versions = ">=3.7" 675 | files = [ 676 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 677 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 678 | ] 679 | 680 | [package.dependencies] 681 | distlib = ">=0.3.7,<1" 682 | filelock = ">=3.12.2,<4" 683 | platformdirs = ">=3.9.1,<5" 684 | 685 | [package.extras] 686 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 687 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 688 | 689 | [[package]] 690 | name = "wmi" 691 | version = "1.5.1" 692 | description = "Windows Management Instrumentation" 693 | optional = false 694 | python-versions = "*" 695 | files = [ 696 | {file = "WMI-1.5.1-py2.py3-none-any.whl", hash = "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942"}, 697 | {file = "WMI-1.5.1.tar.gz", hash = "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6"}, 698 | ] 699 | 700 | [package.dependencies] 701 | pywin32 = "*" 702 | 703 | [package.extras] 704 | all = ["pytest", "sphinx", "twine", "wheel"] 705 | dev = ["pytest", "sphinx", "twine", "wheel"] 706 | docs = ["sphinx"] 707 | package = ["twine", "wheel"] 708 | tests = ["pytest"] 709 | 710 | [[package]] 711 | name = "zipp" 712 | version = "3.19.2" 713 | description = "Backport of pathlib-compatible object wrapper for zip files" 714 | optional = true 715 | python-versions = ">=3.8" 716 | files = [ 717 | {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, 718 | {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, 719 | ] 720 | 721 | [package.extras] 722 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 723 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 724 | 725 | [extras] 726 | pyinstaller = ["pyinstaller"] 727 | 728 | [metadata] 729 | lock-version = "2.0" 730 | python-versions = ">=3.8,<3.13" 731 | content-hash = "c6dd09b7831160602f5571aefac491d0e0391de544dc622be40a48d1c8d2fea1" 732 | -------------------------------------------------------------------------------- /pslipstream/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | -------------------------------------------------------------------------------- /pslipstream/__main__.py: -------------------------------------------------------------------------------- 1 | from pslipstream.main import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /pslipstream/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import platform 4 | import struct 5 | import sys 6 | from pathlib import Path 7 | from typing import Any, List, Optional 8 | 9 | import jsonpickle 10 | from appdirs import AppDirs 11 | 12 | from pslipstream.device import Device 13 | 14 | IS_FROZEN = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") 15 | SYSTEM_INFO = ",".join(map(str, filter(None, [ 16 | sys.platform, 17 | f"{8 * struct.calcsize('P')}bit", 18 | platform.python_version(), 19 | [None, "frozen"][IS_FROZEN] 20 | ]))) 21 | 22 | 23 | class Config: 24 | def __init__(self, config_path: Path, **kwargs: Any): 25 | self.config_path = config_path 26 | 27 | self.last_opened_directory: Optional[Path] = kwargs.get("last_opened_directory") or None 28 | self.recently_opened: List[Device] = kwargs.get("recently_opened") or [] 29 | 30 | @classmethod 31 | def load(cls, path: Path) -> Config: 32 | if not path.exists(): 33 | raise FileNotFoundError(f"Config file path ({path}) was not found") 34 | if not path.is_file(): 35 | raise FileNotFoundError(f"Config file path ({path}) is not to a file.") 36 | instance: Config = jsonpickle.loads(path.read_text(encoding="utf8")) 37 | instance.config_path = path 38 | return instance 39 | 40 | def save(self) -> None: 41 | config_path_backup = self.config_path 42 | 43 | del self.config_path 44 | pickled_config = jsonpickle.dumps(self) 45 | self.config_path = config_path_backup 46 | 47 | config_path_backup.parent.mkdir(parents=True, exist_ok=True) 48 | config_path_backup.write_text(pickled_config, encoding="utf8") 49 | 50 | 51 | class Directories: 52 | _app_dirs = AppDirs("pslipstream", "rlaphoenix") 53 | user_data = Path(_app_dirs.user_data_dir) 54 | user_log = Path(_app_dirs.user_log_dir) 55 | user_config = Path(_app_dirs.user_config_dir) 56 | user_cache = Path(_app_dirs.user_cache_dir) 57 | user_state = Path(_app_dirs.user_state_dir) 58 | root = Path(__file__).resolve().parent # root of package/src 59 | static = root / "static" 60 | 61 | 62 | config_path = Directories.user_data / "config.json" 63 | if not config_path.exists(): 64 | config = Config(config_path) 65 | else: 66 | config = Config.load(config_path) 67 | -------------------------------------------------------------------------------- /pslipstream/device.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class Device: 5 | def __init__( 6 | self, 7 | target: str, 8 | medium: Optional[str] = None, 9 | make: Optional[str] = None, 10 | model: Optional[str] = None, 11 | revision: Optional[str] = None, 12 | volume_id: Optional[str] = None 13 | ): 14 | self.target = target 15 | self.medium = medium 16 | self.make = make or "Virtual" 17 | self.model = model or "FS" 18 | self.revision = revision 19 | self.volume_id = volume_id 20 | 21 | self.is_file = target.lower().endswith(".iso") or target.lower().endswith(".ifo") 22 | -------------------------------------------------------------------------------- /pslipstream/dvd.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | from pathlib import Path 6 | from typing import Any, Generator, List, Optional, Tuple 7 | 8 | from pycdlib import PyCdlib 9 | from pycdlib.pycdlibexception import PyCdlibInvalidInput 10 | from pydvdcss.dvdcss import DvdCss 11 | from pydvdid_m import DvdId 12 | from PySide6.QtCore import SignalInstance 13 | from tqdm import tqdm 14 | 15 | from pslipstream.exceptions import SlipstreamNoKeysObtained, SlipstreamReadError, SlipstreamSeekError 16 | 17 | 18 | class Dvd: 19 | def __init__(self) -> None: 20 | self.log = logging.getLogger("Dvd") 21 | self.cdlib: PyCdlib = PyCdlib() 22 | self.dvdcss: DvdCss = DvdCss() 23 | self.device: Optional[str] = None 24 | self.reader_position: int = 0 25 | self.vob_lba_offsets: List[Tuple[int, int]] = [] 26 | 27 | def __enter__(self) -> Dvd: 28 | return self 29 | 30 | def __exit__(self, *_: Any, **__: Any) -> None: 31 | self.dispose() 32 | 33 | def dispose(self) -> None: 34 | self.log.info("Disposing Dvd object...") 35 | if self.cdlib: 36 | try: 37 | self.cdlib.close() 38 | except PyCdlibInvalidInput: 39 | pass 40 | if self.dvdcss: 41 | self.dvdcss.dispose() 42 | self.device = None 43 | self.reader_position = 0 44 | self.vob_lba_offsets = [] 45 | 46 | def open(self, dev: str) -> None: 47 | """ 48 | Open the device as a DVD with pycdlib and libdvdcss. 49 | 50 | pycdlib will be used to identify and extract information. 51 | libdvdcss will be used for reading, writing, and decrypting. 52 | 53 | Raises SlipstreamDiscInUse if you try to load the same disc that's 54 | already opened. You can open a different disc without an exception as 55 | it will automatically dispose the current disc before opening. 56 | """ 57 | if self.cdlib: 58 | try: 59 | self.cdlib.close() 60 | except PyCdlibInvalidInput: 61 | pass 62 | if self.dvdcss: 63 | self.dvdcss.close() 64 | self.device = dev 65 | self.log.info("Opening '%s'...", dev) 66 | self.cdlib.open(rf"\\.\{dev}" if dev.endswith(":") else dev) 67 | self.log.info("Loaded Device in PyCdlib...") 68 | self.dvdcss.open(dev) 69 | self.log.info("Loaded Device in PyDvdCss...") 70 | self.log.info("DVD opened and ready...") 71 | 72 | def compute_crc_id(self) -> str: 73 | """ 74 | Get the CRC64 checksum known as the Media Player DVD ID. 75 | The algorithm used is the exact same one used by Microsoft's old Windows Media Center. 76 | """ 77 | crc = str(DvdId(self.cdlib).checksum) 78 | self.log.info("Got CRC64 DVD ID: %s", crc) 79 | return crc 80 | 81 | def get_files(self, path: str = "/", no_versions: bool = True) -> Generator[Tuple[str, int, int], None, None]: 82 | """ 83 | Read and list file paths directly from the disc device file system 84 | which doesn't require the device to be mounted 85 | 86 | Returns a tuple generator of the file path which will be 87 | absolute-paths relative to the root of the device, the Logical 88 | Block Address (LBA), and the Size (in sectors). 89 | """ 90 | for child in self.cdlib.list_children(iso_path=path): 91 | file_path = child.file_identifier().decode() 92 | # skip the `.` and `..` paths 93 | if file_path in [".", ".."]: 94 | continue 95 | # remove the semicolon and version number 96 | if no_versions and ";" in file_path: 97 | file_path = file_path.split(";")[0] 98 | # join it to root to be absolute 99 | file_path = os.path.join("/", path, file_path) 100 | # get lba 101 | lba = child.extent_location() 102 | # get size in sectors 103 | size = child.get_data_length() // self.cdlib.pvd.log_block_size 104 | self.log.debug("Found title file: %s, lba: %d, size: %d", file_path, lba, size) 105 | yield file_path, lba, size 106 | 107 | def get_vob_lbas(self, crack_keys: bool = False) -> List[Tuple[int, int]]: 108 | """ 109 | Get the LBA data for all VOB files in disc. 110 | Optionally seek with SEEK_KEY flag to obtain keys. 111 | 112 | Raises SlipstreamSeekError on seek failures. 113 | """ 114 | # Create an array for holding the title data 115 | lba_data: List[Tuple[int, int]] = [] 116 | # Loop all files in disc:/VIDEO_TS 117 | for vob, lba, size in self.get_files("/VIDEO_TS"): 118 | # we only want vob files 119 | if os.path.splitext(vob)[-1] != ".VOB": 120 | continue 121 | # get title key 122 | if crack_keys: 123 | if lba == self.dvdcss.seek(lba, self.dvdcss.SEEK_KEY): 124 | self.log.info("Got title key for %s", vob) 125 | else: 126 | raise SlipstreamSeekError( 127 | f"Failed to seek the disc to {lba} while attempting to " 128 | f"crack the title key for {os.path.basename(vob)}" 129 | ) 130 | # add data to title offsets 131 | lba_data.append((lba, size)) 132 | # Return lba data 133 | return lba_data 134 | 135 | def backup(self, save_path: Path, progress: Optional[SignalInstance] = None) -> None: 136 | """ 137 | Create a full untouched (but decrypted) ISO backup of a DVD with all 138 | metadata intact. 139 | 140 | Parameters: 141 | save_path: Path to store backup. 142 | progress: Signal to emit progress updates to. 143 | 144 | Raises: 145 | SlipstreamNoKeysObtained if no CSS keys were obtained when needed. 146 | SlipstreamReadError on unexpected read errors. 147 | """ 148 | self.log.info("Starting DVD backup for %s", self.device) 149 | 150 | fn = save_path.with_suffix(".ISO.!ss") 151 | first_lba = 0 # lba values are 0-indexed 152 | current_lba = first_lba 153 | last_lba = self.cdlib.pvd.space_size - 1 154 | disc_size = self.cdlib.pvd.log_block_size * self.cdlib.pvd.space_size 155 | 156 | self.log.debug( # skipcq: PYL-W1203 157 | f"Reading sectors {first_lba:,} to {last_lba:,} with sector size {self.cdlib.pvd.log_block_size:,} B." 158 | ) 159 | self.log.debug(f"Length: {last_lba + 1:,} sectors, {disc_size:,} bytes") # skipcq: PYL-W1203 160 | self.log.debug('Saving to "%s"...', fn.with_suffix("")) 161 | 162 | if self.dvdcss.is_scrambled(): 163 | self.log.debug("DVD is scrambled. Checking if all CSS keys can be cracked. This might take a while.") 164 | self.vob_lba_offsets = self.get_vob_lbas(crack_keys=True) 165 | if not self.vob_lba_offsets: 166 | raise SlipstreamNoKeysObtained("No CSS title keys were returned, unable to decrypt.") 167 | else: 168 | self.log.debug("DVD isn't scrambled. CSS title key cracking skipped.") 169 | 170 | f = fn.open("wb") 171 | t = tqdm(total=last_lba + 1, unit="sectors") 172 | 173 | while current_lba <= last_lba: 174 | data = self.read(current_lba, min(self.dvdcss.BLOCK_BUFFER, last_lba - current_lba + 1)) 175 | f.write(data) 176 | read_sectors = len(data) // self.cdlib.pvd.log_block_size 177 | current_lba += read_sectors 178 | if progress: 179 | progress.emit((current_lba / last_lba) * 100) 180 | t.update(read_sectors) 181 | 182 | f.close() 183 | t.close() 184 | 185 | fn = fn.replace(fn.with_suffix("")) 186 | self.log.info("Finished DVD Backup!") 187 | self.log.info(f"Read a total of {current_lba:,} sectors ({os.path.getsize(fn):,}) bytes)") # skipcq: PYL-W1203 188 | 189 | def read(self, first_lba: int, sectors: int) -> bytes: 190 | """ 191 | Efficiently read an amount of sectors from the disc while supporting decryption 192 | with libdvdcss (pydvdcss). 193 | 194 | Returns the amount of sectors read. 195 | Raises a SlipstreamSeekError on Seek Failures and SlipstreamReadError on Read Failures. 196 | """ 197 | # must seek to the first sector, otherwise, we get faulty data 198 | need_to_seek = first_lba != self.reader_position or first_lba == 0 199 | in_title = False 200 | entered_title = False 201 | 202 | # Make sure we never read encrypted and unencrypted data at once since libdvdcss 203 | # only decrypts the whole area of read sectors or nothing at all 204 | for title_start, title_end in self.vob_lba_offsets: 205 | title_end += title_start - 1 206 | 207 | # update key when entering a new title 208 | # FIXME: we also need this if we seek into a new title (not only the start of the title) 209 | if title_start == first_lba: 210 | entered_title = need_to_seek = in_title = True 211 | 212 | # if first_lba < title_start and first_lba + sectors > title_start: 213 | if first_lba < title_start < first_lba + sectors: 214 | # read range will read beyond or on a title, 215 | # let's read up to right before the next title start 216 | sectors = title_start - first_lba 217 | 218 | # if first_lba < title_end and first_lba + sectors > title_end: 219 | if first_lba < title_end < first_lba + sectors: 220 | # read range will read beyond or on a title, 221 | # let's read up to right before the next title start 222 | sectors = title_end - first_lba + 1 223 | 224 | # is our read range part of one title 225 | if first_lba >= title_start and first_lba + (sectors - 1) <= title_end: 226 | in_title = True 227 | 228 | if need_to_seek: 229 | if entered_title: 230 | flags = self.dvdcss.SEEK_KEY 231 | elif in_title: 232 | flags = self.dvdcss.SEEK_MPEG 233 | else: 234 | flags = self.dvdcss.NO_FLAGS 235 | 236 | # refresh the key status for this sector's data 237 | self.reader_position = self.dvdcss.seek(first_lba, flags) 238 | if self.reader_position != first_lba: 239 | raise SlipstreamSeekError(f"Failed to seek the disc to {first_lba} while doing a device read.") 240 | 241 | ret = self.dvdcss.read(sectors, [self.dvdcss.NO_FLAGS, self.dvdcss.READ_DECRYPT][in_title]) 242 | read_sectors = len(ret) // self.cdlib.pvd.log_block_size 243 | if read_sectors < 0: 244 | raise SlipstreamReadError(f"An unexpected read error occurred reading {first_lba}->{first_lba + sectors}") 245 | if read_sectors != sectors: 246 | # we do not want to just reduce the requested sector count as there's 247 | # a chance that the pvd space size is just wrong/badly mastered 248 | request_too_large = first_lba + sectors > self.cdlib.pvd.space_size 249 | if not request_too_large or (first_lba + sectors) - self.cdlib.pvd.space_size != read_sectors: 250 | raise SlipstreamReadError( 251 | f"Read {read_sectors} bytes, expected {sectors}, while reading {first_lba}->{first_lba + sectors}" 252 | ) 253 | self.reader_position += read_sectors 254 | 255 | return ret 256 | -------------------------------------------------------------------------------- /pslipstream/exceptions.py: -------------------------------------------------------------------------------- 1 | class TkinterVersionError(Exception): 2 | """Tkinter version is too outdated, update tkinter to continue.""" 3 | 4 | 5 | class WindowHandleError(Exception): 6 | """Couldn't obtain the GUI's window handle.""" 7 | 8 | 9 | class SlipstreamUiError(Exception): 10 | """Failed to load the UI.""" 11 | 12 | 13 | class SlipstreamDiscInUse(Exception): 14 | """A disc is already initialised in this instance.""" 15 | 16 | 17 | class SlipstreamNoKeysObtained(Exception): 18 | """No keys were returned, unable to decrypt.""" 19 | 20 | 21 | class SlipstreamReadError(Exception): 22 | """An unexpected read error occurred.""" 23 | 24 | 25 | class SlipstreamSeekError(Exception): 26 | """An unexpected seek error occurred.""" 27 | -------------------------------------------------------------------------------- /pslipstream/gui/__init__.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import sys 3 | 4 | from PySide6.QtCore import QDir 5 | from PySide6.QtGui import QIcon 6 | from PySide6.QtWidgets import QApplication 7 | 8 | from pslipstream.config import Directories, config 9 | from pslipstream.gui.main_window import MainWindow 10 | from pslipstream.gui.workers import WORKER_THREAD 11 | 12 | 13 | def start() -> None: 14 | """Start the GUI and Qt execution loop.""" 15 | if sys.platform == "win32": 16 | # https://stackoverflow.com/a/1552105/13183782 17 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(u"com.rlaphoenix.slipstream") 18 | 19 | app = QApplication(sys.argv) 20 | app.setStyle("fusion") 21 | app.setStyleSheet((Directories.static / "style.qss").read_text("utf8")) 22 | app.setWindowIcon(QIcon(str(Directories.static / "img/icon.ico"))) 23 | app.aboutToQuit.connect(config.save) 24 | app.aboutToQuit.connect(WORKER_THREAD.quit) 25 | QDir.setCurrent(str(Directories.root)) 26 | 27 | window = MainWindow() 28 | window.show() 29 | 30 | sys.exit(app.exec()) 31 | -------------------------------------------------------------------------------- /pslipstream/gui/main_window.py: -------------------------------------------------------------------------------- 1 | import math 2 | import traceback 3 | from datetime import datetime 4 | from functools import partial 5 | from pathlib import Path 6 | from typing import Any, Optional 7 | 8 | from pycdlib.dates import VolumeDescriptorDate 9 | from pycdlib.headervd import FileOrTextIdentifier 10 | from PySide6 import QtCore 11 | from PySide6.QtGui import QAction, QCursor 12 | from PySide6.QtWidgets import (QFileDialog, QHeaderView, QMainWindow, QMessageBox, QPushButton, QTreeWidgetItem, 13 | QVBoxLayout) 14 | 15 | from pslipstream import __version__ 16 | from pslipstream.config import SYSTEM_INFO, config 17 | from pslipstream.device import Device 18 | from pslipstream.dvd import Dvd 19 | from pslipstream.gui.main_window_ui import Ui_MainWindow # type: ignore[attr-defined] 20 | from pslipstream.gui.workers import DEVICE_LOADER, DEVICE_READER, DEVICE_SCANNER, WORKER_THREAD 21 | from pslipstream.helpers import convert_iso_descriptor_date 22 | 23 | 24 | class MainWindow(QMainWindow): 25 | def __init__(self, *args: Any, **kwargs: Any) -> None: 26 | super().__init__(*args, **kwargs) 27 | 28 | self.ui = Ui_MainWindow() 29 | self.ui.setupUi(self) 30 | 31 | self.setWindowTitle(f"Slipstream v{__version__}") 32 | self.setMinimumSize(1000, 400) 33 | 34 | self.reset_ui() 35 | self.setup_logic() 36 | 37 | def reset_ui(self) -> None: 38 | """Reset the UI to initial startup state.""" 39 | self.ui.backupButton.setEnabled(False) 40 | self.ui.backupButton.hide() 41 | self.ui.progressBar.hide() 42 | self.ui.discInfoFrame.hide() 43 | 44 | self.clear_device_list() 45 | 46 | for entry in config.recently_opened: 47 | self.add_recent_entry(entry) 48 | 49 | def setup_logic(self) -> None: 50 | """Link Signals/Slots, add startup calls.""" 51 | # menu bar actions 52 | self.ui.actionOpen.triggered.connect(self.open_file) 53 | self.ui.actionExit.triggered.connect(self.close) 54 | self.ui.actionAbout.triggered.connect(self.about) 55 | 56 | # device list 57 | DEVICE_SCANNER.started.connect(self.on_device_scan_start) 58 | DEVICE_SCANNER.finished.connect(self.on_device_scan_finish) 59 | DEVICE_SCANNER.error.connect(self.on_device_scan_error) 60 | DEVICE_SCANNER.scanned_device.connect(self.add_device_button) 61 | self.ui.refreshIcon.clicked.connect(DEVICE_SCANNER.scan) 62 | 63 | # disc info 64 | DEVICE_LOADER.started.connect(self.on_disc_load_start) 65 | DEVICE_LOADER.finished.connect(self.on_disc_load_finish) 66 | DEVICE_LOADER.error.connect(self.on_disc_load_error) 67 | DEVICE_LOADER.disc_loaded.connect(self.load_disc_info) 68 | 69 | # disc backup 70 | DEVICE_READER.started.connect(self.on_disc_read_start) 71 | DEVICE_READER.finished.connect(self.on_disc_read_finish) 72 | DEVICE_READER.error.connect(self.on_disc_read_error) 73 | DEVICE_READER.progress.connect(self.on_disc_read_progress) 74 | self.ui.backupButton.clicked.connect(lambda: ( 75 | self.start_backup(DEVICE_LOADER.disc) 76 | ) if DEVICE_LOADER.disc else ( 77 | QMessageBox.critical(self, "Error", "You somehow clicked Backup before a Disc was loaded.") 78 | )) 79 | 80 | # startup 81 | WORKER_THREAD.started.connect(DEVICE_SCANNER.scan) 82 | WORKER_THREAD.start() 83 | 84 | # Menu Bar # 85 | 86 | def open_file(self, device: Optional[Device] = None) -> None: 87 | """Open a Disc file and add a Pseudo-device Button to the Device list.""" 88 | if not device: 89 | loc = QFileDialog.getOpenFileName( 90 | self, 91 | "Backup Disc Image", 92 | str(config.last_opened_directory or ""), 93 | "ISO files (*.iso);;DVD IFO files (*.ifo)" 94 | ) 95 | if not loc[0]: 96 | return 97 | device = Device( 98 | target=loc[0], 99 | medium="DVD", # TODO: Don't presume DVD 100 | volume_id=Path(loc[0]).name 101 | ) 102 | 103 | self.add_device_button(device) 104 | DEVICE_LOADER.load_dvd(device) 105 | 106 | if not any(x.text() == device.target for x in self.ui.menuOpen_Recent.actions()): 107 | self.add_recent_entry(device) 108 | config.recently_opened.append(device) 109 | 110 | config.last_opened_directory = Path(device.target).parent 111 | 112 | def about(self) -> None: 113 | """Displays the Help->About Message Box.""" 114 | QMessageBox.about( 115 | self, 116 | "About Slipstream", 117 | f"Slipstream v{__version__} [{SYSTEM_INFO}]" + 118 | f"

Copyright (C) 2020-{datetime.now().year} rlaphoenix

" + 119 | "

The most informative Home-media backup solution.
" 120 | "" 121 | "https://github.com/rlaphoenix/Slipstream" 122 | "

" 123 | ) 124 | 125 | def add_recent_entry(self, device: Device) -> None: 126 | """Add an Entry to the File->Open Recent Menu bar list.""" 127 | recent_entry = QAction(self) 128 | recent_entry.text() 129 | recent_entry.setText(device.target) 130 | recent_entry.triggered.connect(partial(self.open_file, device)) 131 | self.ui.menuOpen_Recent.addAction(recent_entry) 132 | self.ui.menuOpen_Recent.setEnabled(True) 133 | 134 | # Device List # 135 | 136 | def clear_device_list(self) -> None: 137 | """Clear the List of Disc Reader Devices.""" 138 | for device in self.ui.deviceListDevices_2.children(): 139 | if isinstance(device, QPushButton): 140 | device.setParent(None) 141 | 142 | def add_device_button(self, device: Device) -> None: 143 | """Add a new Disc Reader Device Button to the List.""" 144 | for d in self.ui.deviceListDevices_2.children(): 145 | if isinstance(d, QPushButton) and d.objectName() == device.target: 146 | return 147 | 148 | no_disc = not bool(device.volume_id) 149 | 150 | button = QPushButton( 151 | f"{device.volume_id or 'No disc inserted...'}\n" 152 | f"{device.make} - {device.model}" 153 | ) 154 | button.setObjectName(device.target) 155 | button.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) 156 | button.clicked.connect(partial(DEVICE_LOADER.device.emit, device)) 157 | 158 | if no_disc: 159 | button.setEnabled(False) 160 | 161 | device_list: QVBoxLayout = self.ui.deviceListDevices_2.layout() 162 | device_list.insertWidget(device_list.count() - 1 if no_disc else 0, button) 163 | 164 | def on_device_scan_start(self) -> None: 165 | self.ui.refreshIcon.setEnabled(False) 166 | self.ui.statusbar.showMessage("Scanning devices...") 167 | self.clear_device_list() 168 | 169 | self.ui.progressBar.hide() 170 | self.ui.backupButton.hide() 171 | self.ui.discInfoFrame.hide() 172 | self.ui.discInfoList.clear() 173 | 174 | def on_device_scan_finish(self, device_count: int) -> None: 175 | self.ui.refreshIcon.setEnabled(True) 176 | self.ui.statusbar.showMessage(f"Found {device_count} devices") 177 | 178 | def on_device_scan_error(self, error: Exception) -> None: 179 | traceback.print_exception(error) 180 | QMessageBox.critical( 181 | self, 182 | "Error", 183 | "An unexpected error occurred while scanning for Disc Reader Devices:\n\n" + 184 | "\n".join(traceback.format_exception(error)) 185 | ) 186 | 187 | # Disc Info # 188 | 189 | def load_disc_info(self, disc: Dvd) -> None: 190 | """Load Disc Information.""" 191 | self.ui.discInfoList.clear() 192 | disc_id = disc.compute_crc_id() 193 | disc_id_tree = QTreeWidgetItem(["Disc ID", disc_id]) 194 | self.ui.discInfoList.addTopLevelItem(disc_id_tree) 195 | 196 | pvd_tree = QTreeWidgetItem(["Primary Volume Descriptor"]) 197 | for k, v in {k: disc.cdlib.pvd.__getattribute__(k) for k in disc.cdlib.pvd.__slots__}.items(): 198 | if isinstance(v, FileOrTextIdentifier): 199 | v = v.text 200 | elif isinstance(v, VolumeDescriptorDate): 201 | v = convert_iso_descriptor_date(v) 202 | pvd_tree.addChild(QTreeWidgetItem([k, repr(v)])) 203 | self.ui.discInfoList.addTopLevelItem(pvd_tree) 204 | 205 | self.ui.discInfoList.expandToDepth(0) 206 | self.ui.discInfoList.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) 207 | 208 | def on_disc_load_start(self, device: Device) -> None: 209 | self.ui.deviceListDevices_2.setEnabled(False) 210 | self.ui.refreshIcon.setEnabled(False) 211 | self.ui.progressBar.hide() 212 | self.ui.backupButton.hide() 213 | self.ui.discInfoFrame.hide() 214 | self.ui.discInfoList.clear() 215 | self.ui.statusbar.showMessage(f"Loading device {device.make} - {device.model}...") 216 | 217 | def on_disc_load_finish(self, device: Device) -> None: 218 | self.ui.deviceListDevices_2.setEnabled(True) 219 | self.ui.refreshIcon.setEnabled(True) 220 | self.ui.backupButton.setEnabled(True) 221 | self.ui.backupButton.show() 222 | self.ui.discInfoFrame.show() 223 | self.ui.statusbar.showMessage(f"Loaded device {device.make} - {device.model}...") 224 | 225 | def on_disc_load_error(self, error: Exception) -> None: 226 | traceback.print_exception(error) 227 | QMessageBox.critical( 228 | self, 229 | "Error", 230 | "An unexpected error occurred while Loading a Disc Reader Device:\n\n" + 231 | "\n".join(traceback.format_exception(error)) 232 | ) 233 | 234 | # Disc Backup # 235 | 236 | def start_backup(self, disc: Dvd) -> None: 237 | save_path, _ = QFileDialog.getSaveFileName( 238 | self, 239 | "Backup Disc Image", 240 | str(Path( 241 | config.last_opened_directory or "", 242 | disc.cdlib.pvd.volume_identifier.replace(b"\x00", b"").strip().decode() + ".ISO" 243 | )), 244 | "Disc Images (*.ISO, *.BIN);;All Files (*)" 245 | ) 246 | if not save_path: 247 | return 248 | DEVICE_READER.disc.emit(disc, Path(save_path)) 249 | 250 | def on_disc_read_progress(self, n: float) -> None: 251 | self.ui.progressBar.setValue(math.floor(n)) 252 | self.ui.backupButton.setText(f"Backing up... {math.floor(n)}%") 253 | 254 | def on_disc_read_start(self, disc: Dvd) -> None: 255 | self.ui.progressBar.show() 256 | self.ui.progressBar.setValue(0) 257 | self.ui.backupButton.setEnabled(False) 258 | self.ui.statusbar.showMessage( 259 | f"Backing up {disc.device} ({disc.cdlib.pvd.volume_identifier.decode('utf8').strip()})..." 260 | ) 261 | 262 | def on_disc_read_finish(self, disc: Dvd) -> None: 263 | self.ui.backupButton.setText("Backup") 264 | self.ui.statusbar.showMessage( 265 | f"Backed up {disc.device} ({disc.cdlib.pvd.volume_identifier.decode('utf8').strip()})..." 266 | ) 267 | self.ui.backupButton.setEnabled(True) 268 | 269 | def on_disc_read_error(self, error: Exception) -> None: 270 | traceback.print_exception(error) 271 | QMessageBox.critical( 272 | self, 273 | "Error", 274 | "An unexpected error occurred while Backing up a Disc:\n\n" + 275 | "\n".join(traceback.format_exception(error)) 276 | ) 277 | -------------------------------------------------------------------------------- /pslipstream/gui/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1000 10 | 400 11 | 12 | 13 | 14 | Slipstream 15 | 16 | 17 | 18 | static/img/music-disc-with-luster.svgstatic/img/music-disc-with-luster.svg 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 0 27 | 28 | 29 | 0 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | 40 | 0 41 | 42 | 43 | 44 | 45 | QFrame::Panel 46 | 47 | 48 | QFrame::Raised 49 | 50 | 51 | 52 | 16 53 | 54 | 55 | 20 56 | 57 | 58 | 20 59 | 60 | 61 | 20 62 | 63 | 64 | 20 65 | 66 | 67 | 68 | 69 | 16 70 | 71 | 72 | QLayout::SetMinimumSize 73 | 74 | 75 | 76 | 77 | 78 | 20 79 | 20 80 | 81 | 82 | 83 | 84 | 20 85 | 20 86 | 87 | 88 | 89 | static/img/music-disc-with-luster.svg 90 | 91 | 92 | true 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 0 101 | 28 102 | 103 | 104 | 105 | 106 | Arial 107 | 13 108 | true 109 | 110 | 111 | 112 | Device list 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 36 121 | 16777215 122 | 123 | 124 | 125 | 126 | static/img/refresh.svgstatic/img/refresh.svg 127 | 128 | 129 | 130 | 20 131 | 20 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | 148 | 200 149 | 0 150 | 151 | 152 | 153 | QFrame::NoFrame 154 | 155 | 156 | Qt::ScrollBarAlwaysOff 157 | 158 | 159 | true 160 | 161 | 162 | 163 | 164 | 0 165 | 0 166 | 200 167 | 200 168 | 169 | 170 | 171 | 172 | 16 173 | 174 | 175 | 0 176 | 177 | 178 | 0 179 | 180 | 181 | 0 182 | 183 | 184 | 0 185 | 186 | 187 | 188 | 189 | POKEMON 190 | ASUS - SDRW-08U7M-U 191 | 192 | 193 | 194 | 195 | 196 | 197 | Qt::Vertical 198 | 199 | 200 | 201 | 20 202 | 40 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Calibri 216 | 12 217 | false 218 | PreferDefault 219 | 220 | 221 | 222 | Backup 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | QFrame::Panel 233 | 234 | 235 | QFrame::Raised 236 | 237 | 238 | 239 | 16 240 | 241 | 242 | 20 243 | 244 | 245 | 20 246 | 247 | 248 | 20 249 | 250 | 251 | 20 252 | 253 | 254 | 255 | 256 | 16 257 | 258 | 259 | QLayout::SetMinimumSize 260 | 261 | 262 | 263 | 264 | 265 | 20 266 | 20 267 | 268 | 269 | 270 | 271 | 20 272 | 20 273 | 274 | 275 | 276 | static/img/info-circle.svg 277 | 278 | 279 | true 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 0 288 | 0 289 | 290 | 291 | 292 | 293 | 0 294 | 28 295 | 296 | 297 | 298 | 299 | Arial 300 | 13 301 | true 302 | 303 | 304 | 305 | Disc information 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | QFrame::NoFrame 315 | 316 | 317 | QAbstractScrollArea::AdjustToContents 318 | 319 | 320 | QAbstractItemView::SelectItems 321 | 322 | 323 | 2 324 | 325 | 326 | false 327 | 328 | 329 | true 330 | 331 | 332 | 175 333 | 334 | 335 | false 336 | 337 | 338 | 339 | Name 340 | 341 | 342 | 343 | 344 | Value 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 20 358 | 359 | 360 | false 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 0 371 | 0 372 | 1000 373 | 22 374 | 375 | 376 | 377 | 378 | File 379 | 380 | 381 | 382 | false 383 | 384 | 385 | Open Recent 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | Help 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | Open 405 | 406 | 407 | Ctrl+O 408 | 409 | 410 | 411 | 412 | Exit 413 | 414 | 415 | Ctrl+Q 416 | 417 | 418 | 419 | 420 | About 421 | 422 | 423 | 424 | 425 | 426 | 427 | -------------------------------------------------------------------------------- /pslipstream/gui/main_window_ui.py: -------------------------------------------------------------------------------- 1 | # pylint: disable-all 2 | # type: ignore 3 | # -*- coding: utf-8 -*- 4 | 5 | ################################################################################ 6 | ## Form generated from reading UI file 'main_window.ui' 7 | ## 8 | ## Created by: Qt User Interface Compiler version 6.5.3 9 | ## 10 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 11 | ################################################################################ 12 | 13 | from PySide6.QtCore import QCoreApplication, QMetaObject, QRect, QSize, Qt 14 | from PySide6.QtGui import QAction, QFont, QIcon, QPixmap 15 | from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QFrame, QHBoxLayout, QLabel, QLayout, QMenu, 16 | QMenuBar, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QStatusBar, 17 | QTreeWidget, QVBoxLayout, QWidget) 18 | 19 | 20 | class Ui_MainWindow(object): 21 | def setupUi(self, MainWindow): 22 | if not MainWindow.objectName(): 23 | MainWindow.setObjectName(u"MainWindow") 24 | MainWindow.resize(1000, 400) 25 | icon = QIcon() 26 | icon.addFile(u"static/img/music-disc-with-luster.svg", QSize(), QIcon.Normal, QIcon.Off) 27 | MainWindow.setWindowIcon(icon) 28 | self.actionOpen = QAction(MainWindow) 29 | self.actionOpen.setObjectName(u"actionOpen") 30 | self.actionExit = QAction(MainWindow) 31 | self.actionExit.setObjectName(u"actionExit") 32 | self.actionAbout = QAction(MainWindow) 33 | self.actionAbout.setObjectName(u"actionAbout") 34 | self.cw = QWidget(MainWindow) 35 | self.cw.setObjectName(u"cw") 36 | self.verticalLayout_6 = QVBoxLayout(self.cw) 37 | self.verticalLayout_6.setSpacing(0) 38 | self.verticalLayout_6.setObjectName(u"verticalLayout_6") 39 | self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) 40 | self.horizontalLayout = QHBoxLayout() 41 | self.horizontalLayout.setSpacing(0) 42 | self.horizontalLayout.setObjectName(u"horizontalLayout") 43 | self.deviceListFrame = QFrame(self.cw) 44 | self.deviceListFrame.setObjectName(u"deviceListFrame") 45 | self.deviceListFrame.setFrameShape(QFrame.Panel) 46 | self.deviceListFrame.setFrameShadow(QFrame.Raised) 47 | self.verticalLayout_2 = QVBoxLayout(self.deviceListFrame) 48 | self.verticalLayout_2.setSpacing(16) 49 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 50 | self.verticalLayout_2.setContentsMargins(20, 20, 20, 20) 51 | self.deviceListHeader = QHBoxLayout() 52 | self.deviceListHeader.setSpacing(16) 53 | self.deviceListHeader.setObjectName(u"deviceListHeader") 54 | self.deviceListHeader.setSizeConstraint(QLayout.SetMinimumSize) 55 | self.discIcon = QLabel(self.deviceListFrame) 56 | self.discIcon.setObjectName(u"discIcon") 57 | self.discIcon.setMinimumSize(QSize(20, 20)) 58 | self.discIcon.setMaximumSize(QSize(20, 20)) 59 | self.discIcon.setPixmap(QPixmap(u"static/img/music-disc-with-luster.svg")) 60 | self.discIcon.setScaledContents(True) 61 | 62 | self.deviceListHeader.addWidget(self.discIcon) 63 | 64 | self.deviceListL = QLabel(self.deviceListFrame) 65 | self.deviceListL.setObjectName(u"deviceListL") 66 | self.deviceListL.setMinimumSize(QSize(0, 28)) 67 | font = QFont() 68 | font.setFamilies([u"Arial"]) 69 | font.setPointSize(13) 70 | font.setBold(True) 71 | self.deviceListL.setFont(font) 72 | 73 | self.deviceListHeader.addWidget(self.deviceListL) 74 | 75 | self.refreshIcon = QPushButton(self.deviceListFrame) 76 | self.refreshIcon.setObjectName(u"refreshIcon") 77 | self.refreshIcon.setMaximumSize(QSize(36, 16777215)) 78 | icon1 = QIcon() 79 | icon1.addFile(u"static/img/refresh.svg", QSize(), QIcon.Normal, QIcon.Off) 80 | self.refreshIcon.setIcon(icon1) 81 | self.refreshIcon.setIconSize(QSize(20, 20)) 82 | 83 | self.deviceListHeader.addWidget(self.refreshIcon) 84 | 85 | 86 | self.verticalLayout_2.addLayout(self.deviceListHeader) 87 | 88 | self.deviceListDevices = QScrollArea(self.deviceListFrame) 89 | self.deviceListDevices.setObjectName(u"deviceListDevices") 90 | sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) 91 | sizePolicy.setHorizontalStretch(0) 92 | sizePolicy.setVerticalStretch(0) 93 | sizePolicy.setHeightForWidth(self.deviceListDevices.sizePolicy().hasHeightForWidth()) 94 | self.deviceListDevices.setSizePolicy(sizePolicy) 95 | self.deviceListDevices.setMinimumSize(QSize(200, 0)) 96 | self.deviceListDevices.setFrameShape(QFrame.NoFrame) 97 | self.deviceListDevices.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 98 | self.deviceListDevices.setWidgetResizable(True) 99 | self.deviceListDevices_2 = QWidget() 100 | self.deviceListDevices_2.setObjectName(u"deviceListDevices_2") 101 | self.deviceListDevices_2.setGeometry(QRect(0, 0, 200, 200)) 102 | self.verticalLayout_3 = QVBoxLayout(self.deviceListDevices_2) 103 | self.verticalLayout_3.setSpacing(16) 104 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 105 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 106 | self.exampleDevice = QPushButton(self.deviceListDevices_2) 107 | self.exampleDevice.setObjectName(u"exampleDevice") 108 | 109 | self.verticalLayout_3.addWidget(self.exampleDevice) 110 | 111 | self.deviceListSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 112 | 113 | self.verticalLayout_3.addItem(self.deviceListSpacer) 114 | 115 | self.deviceListDevices.setWidget(self.deviceListDevices_2) 116 | 117 | self.verticalLayout_2.addWidget(self.deviceListDevices) 118 | 119 | self.backupButton = QPushButton(self.deviceListFrame) 120 | self.backupButton.setObjectName(u"backupButton") 121 | font1 = QFont() 122 | font1.setFamilies([u"Calibri"]) 123 | font1.setPointSize(12) 124 | font1.setBold(False) 125 | font1.setStyleStrategy(QFont.PreferDefault) 126 | self.backupButton.setFont(font1) 127 | 128 | self.verticalLayout_2.addWidget(self.backupButton) 129 | 130 | 131 | self.horizontalLayout.addWidget(self.deviceListFrame) 132 | 133 | self.discInfoFrame = QFrame(self.cw) 134 | self.discInfoFrame.setObjectName(u"discInfoFrame") 135 | self.discInfoFrame.setFrameShape(QFrame.Panel) 136 | self.discInfoFrame.setFrameShadow(QFrame.Raised) 137 | self.verticalLayout = QVBoxLayout(self.discInfoFrame) 138 | self.verticalLayout.setSpacing(16) 139 | self.verticalLayout.setObjectName(u"verticalLayout") 140 | self.verticalLayout.setContentsMargins(20, 20, 20, 20) 141 | self.deviceInfoHeader = QHBoxLayout() 142 | self.deviceInfoHeader.setSpacing(16) 143 | self.deviceInfoHeader.setObjectName(u"deviceInfoHeader") 144 | self.deviceInfoHeader.setSizeConstraint(QLayout.SetMinimumSize) 145 | self.infoIcon = QLabel(self.discInfoFrame) 146 | self.infoIcon.setObjectName(u"infoIcon") 147 | self.infoIcon.setMinimumSize(QSize(20, 20)) 148 | self.infoIcon.setMaximumSize(QSize(20, 20)) 149 | self.infoIcon.setPixmap(QPixmap(u"static/img/info-circle.svg")) 150 | self.infoIcon.setScaledContents(True) 151 | 152 | self.deviceInfoHeader.addWidget(self.infoIcon) 153 | 154 | self.label_4 = QLabel(self.discInfoFrame) 155 | self.label_4.setObjectName(u"label_4") 156 | sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 157 | sizePolicy1.setHorizontalStretch(0) 158 | sizePolicy1.setVerticalStretch(0) 159 | sizePolicy1.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) 160 | self.label_4.setSizePolicy(sizePolicy1) 161 | self.label_4.setMinimumSize(QSize(0, 28)) 162 | self.label_4.setFont(font) 163 | 164 | self.deviceInfoHeader.addWidget(self.label_4) 165 | 166 | 167 | self.verticalLayout.addLayout(self.deviceInfoHeader) 168 | 169 | self.discInfoList = QTreeWidget(self.discInfoFrame) 170 | self.discInfoList.setObjectName(u"discInfoList") 171 | self.discInfoList.setFrameShape(QFrame.NoFrame) 172 | self.discInfoList.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) 173 | self.discInfoList.setSelectionBehavior(QAbstractItemView.SelectItems) 174 | self.discInfoList.setColumnCount(2) 175 | self.discInfoList.header().setVisible(False) 176 | self.discInfoList.header().setCascadingSectionResizes(True) 177 | self.discInfoList.header().setMinimumSectionSize(175) 178 | self.discInfoList.header().setStretchLastSection(False) 179 | 180 | self.verticalLayout.addWidget(self.discInfoList) 181 | 182 | 183 | self.horizontalLayout.addWidget(self.discInfoFrame) 184 | 185 | 186 | self.verticalLayout_6.addLayout(self.horizontalLayout) 187 | 188 | self.progressBar = QProgressBar(self.cw) 189 | self.progressBar.setObjectName(u"progressBar") 190 | self.progressBar.setValue(20) 191 | self.progressBar.setTextVisible(False) 192 | 193 | self.verticalLayout_6.addWidget(self.progressBar) 194 | 195 | MainWindow.setCentralWidget(self.cw) 196 | self.statusbar = QStatusBar(MainWindow) 197 | self.statusbar.setObjectName(u"statusbar") 198 | MainWindow.setStatusBar(self.statusbar) 199 | self.menubar = QMenuBar(MainWindow) 200 | self.menubar.setObjectName(u"menubar") 201 | self.menubar.setGeometry(QRect(0, 0, 1000, 22)) 202 | self.menuFile = QMenu(self.menubar) 203 | self.menuFile.setObjectName(u"menuFile") 204 | self.menuOpen_Recent = QMenu(self.menuFile) 205 | self.menuOpen_Recent.setObjectName(u"menuOpen_Recent") 206 | self.menuOpen_Recent.setEnabled(False) 207 | self.menuHelp = QMenu(self.menubar) 208 | self.menuHelp.setObjectName(u"menuHelp") 209 | MainWindow.setMenuBar(self.menubar) 210 | 211 | self.menubar.addAction(self.menuFile.menuAction()) 212 | self.menubar.addAction(self.menuHelp.menuAction()) 213 | self.menuFile.addAction(self.actionOpen) 214 | self.menuFile.addAction(self.menuOpen_Recent.menuAction()) 215 | self.menuFile.addSeparator() 216 | self.menuFile.addAction(self.actionExit) 217 | self.menuHelp.addAction(self.actionAbout) 218 | 219 | self.retranslateUi(MainWindow) 220 | 221 | QMetaObject.connectSlotsByName(MainWindow) 222 | # setupUi 223 | 224 | def retranslateUi(self, MainWindow): 225 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Slipstream", None)) 226 | self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"Open", None)) 227 | #if QT_CONFIG(shortcut) 228 | self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) 229 | #endif // QT_CONFIG(shortcut) 230 | self.actionExit.setText(QCoreApplication.translate("MainWindow", u"Exit", None)) 231 | #if QT_CONFIG(shortcut) 232 | self.actionExit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None)) 233 | #endif // QT_CONFIG(shortcut) 234 | self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About", None)) 235 | self.deviceListL.setText(QCoreApplication.translate("MainWindow", u"Device list", None)) 236 | self.exampleDevice.setText(QCoreApplication.translate("MainWindow", u"POKEMON\n" 237 | "ASUS - SDRW-08U7M-U", None)) 238 | self.backupButton.setText(QCoreApplication.translate("MainWindow", u"Backup", None)) 239 | self.label_4.setText(QCoreApplication.translate("MainWindow", u"Disc information", None)) 240 | ___qtreewidgetitem = self.discInfoList.headerItem() 241 | ___qtreewidgetitem.setText(1, QCoreApplication.translate("MainWindow", u"Value", None)) 242 | ___qtreewidgetitem.setText(0, QCoreApplication.translate("MainWindow", u"Name", None)) 243 | self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) 244 | self.menuOpen_Recent.setTitle(QCoreApplication.translate("MainWindow", u"Open Recent", None)) 245 | self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) 246 | # retranslateUi 247 | -------------------------------------------------------------------------------- /pslipstream/gui/workers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any, Optional 3 | 4 | import pythoncom 5 | from PySide6.QtCore import QObject, QThread, Signal 6 | from wmi import WMI 7 | 8 | from pslipstream.device import Device 9 | from pslipstream.dvd import Dvd 10 | 11 | 12 | class DeviceScanner(QObject): 13 | """QObject to Scan for Disc Reader Devices.""" 14 | started = Signal() 15 | finished = Signal(int) 16 | error = Signal(Exception) 17 | scanned_device = Signal(Device) 18 | 19 | def scan(self) -> None: 20 | try: 21 | self.started.emit() 22 | # noinspection PyUnresolvedReferences 23 | pythoncom.CoInitialize() # important! 24 | c = WMI() 25 | drives = c.Win32_CDROMDrive() 26 | for drive in drives: 27 | self.scanned_device.emit(Device( 28 | target=drive.drive, 29 | make=drive.name.split(" ")[0], 30 | model=drive.name.split(" ")[1], 31 | revision=drive.mfrAssignedRevisionLevel, 32 | volume_id=drive.volumeName 33 | )) 34 | self.finished.emit(len(drives)) 35 | except Exception as e: # skipcq: PYL-W0703 36 | self.error.emit(e) 37 | 38 | 39 | class DeviceLoader(QObject): 40 | """ 41 | QObject to Load Disc Information from a Disc Reader Device. 42 | 43 | Note: 44 | - Currently only DVD Discs are supported. 45 | """ 46 | started = Signal(Device) 47 | finished = Signal(Device) 48 | error = Signal(Exception) 49 | disc_loaded = Signal(Dvd) 50 | 51 | device = Signal(Device) 52 | 53 | def __init__(self, *args: Any, **kwargs: Any) -> None: 54 | super().__init__(*args, **kwargs) 55 | 56 | self.device.connect(self.load_dvd) 57 | 58 | self.disc: Optional[Dvd] = None 59 | 60 | def load_dvd(self, device: Device) -> None: 61 | try: 62 | self.started.emit(device) 63 | # noinspection PyUnresolvedReferences 64 | pythoncom.CoInitialize() 65 | # TODO: assumes disc is a DVD 66 | disc = Dvd() 67 | disc.open(device.target) 68 | self.disc = disc 69 | self.disc_loaded.emit(disc) 70 | self.finished.emit(device) 71 | except Exception as e: # skipcq: PYL-W0703 72 | self.error.emit(e) 73 | 74 | 75 | class DeviceReader(QObject): 76 | """ 77 | QObject to Read Data from a Disc Reader Device. 78 | 79 | Note: 80 | - Currently only DVD Discs are supported. 81 | - CSS (Content Scramble System) is automatically bypassed with libdvdcss. 82 | """ 83 | started = Signal(Dvd) 84 | finished = Signal(Dvd) 85 | error = Signal(Exception) 86 | progress = Signal(float) 87 | 88 | disc = Signal(Dvd, Path) 89 | 90 | def __init__(self, *args: Any, **kwargs: Any) -> None: 91 | super().__init__(*args, **kwargs) 92 | 93 | self.disc.connect(self.backup_dvd) 94 | 95 | def backup_dvd(self, disc: Dvd, save_path: Path) -> None: 96 | try: 97 | self.started.emit(disc) 98 | disc.backup(save_path, self.progress) 99 | self.finished.emit(disc) 100 | except Exception as e: # skipcq: PYL-W0703 101 | self.error.emit(e) 102 | 103 | 104 | WORKER_THREAD = QThread() 105 | 106 | DEVICE_SCANNER = DeviceScanner() 107 | DEVICE_SCANNER.moveToThread(WORKER_THREAD) 108 | 109 | DEVICE_LOADER = DeviceLoader() 110 | DEVICE_LOADER.moveToThread(WORKER_THREAD) 111 | 112 | DEVICE_READER = DeviceReader() 113 | DEVICE_READER.moveToThread(WORKER_THREAD) 114 | -------------------------------------------------------------------------------- /pslipstream/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from dateutil.tz import tzoffset 5 | from pycdlib.dates import VolumeDescriptorDate 6 | 7 | 8 | def convert_iso_descriptor_date(vdd: VolumeDescriptorDate) -> Optional[datetime]: 9 | """ 10 | Convert an ISO Descriptor Date to a DateTime object. 11 | ISO Descriptor Dates are offset from GMT in 15 minute intervals. 12 | This is offset to the user's timezone via tzoffset. 13 | Returns None if the Descriptor Date does not specify the year. 14 | It assumes a default for any other value missing from the Descriptor Date. 15 | """ 16 | if not vdd.year: 17 | return None 18 | return datetime( 19 | year=vdd.year, 20 | month=vdd.month, 21 | day=vdd.dayofmonth, 22 | hour=vdd.hour, 23 | minute=vdd.minute, 24 | second=vdd.second, 25 | microsecond=vdd.hundredthsofsecond, 26 | tzinfo=tzoffset("GMT", (15 * vdd.gmtoffset) * 60) 27 | ) 28 | -------------------------------------------------------------------------------- /pslipstream/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | import click 7 | import coloredlogs 8 | 9 | from pslipstream import __version__, gui 10 | from pslipstream.config import SYSTEM_INFO 11 | 12 | 13 | @click.command() 14 | @click.option("-v", "--version", is_flag=True, default=False, help="Print version information") 15 | @click.option("-d", "--debug", is_flag=True, default=False, help="Enable DEBUG level logs") 16 | @click.option("-l", "--license", "licence", is_flag=True, default=False, help="View license details") 17 | def main(version: bool, debug: bool, licence: bool) -> None: 18 | """Slipstream—A Home-media Backup Solution""" 19 | logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) 20 | log = logging.getLogger(__name__) 21 | coloredlogs.install( 22 | level=log.level, 23 | logger=log, 24 | fmt="{asctime} [{levelname[0]}] {name} : {message}", 25 | style="{" 26 | ) 27 | 28 | if version: 29 | print(__version__) 30 | return 31 | 32 | if licence: 33 | if not os.path.exists("LICENSE"): 34 | print( 35 | "License file was not found locally, please ensure this is a licensed distribution.\n" 36 | "The license can be found at gnu.org: https://www.gnu.org/licenses/gpl-3.0.txt" 37 | ) 38 | sys.exit(1) 39 | else: 40 | with open("LICENSE", mode="rt", encoding="utf-8") as f: 41 | print(f.read()) 42 | return 43 | 44 | log.info("Slipstream version %s [%s]", __version__, SYSTEM_INFO) 45 | log.info("Copyright (c) 2020-%d rlaphoenix", datetime.now().year) 46 | log.info("https://github.com/rlaphoenix/slipstream") 47 | 48 | gui.start() 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /pslipstream/static/README.md: -------------------------------------------------------------------------------- 1 | # static 2 | 3 | This is a folder for static files that will be copied along when installed with pip. 4 | Typical usage would be to host files that need to be read locally like the Icon for the App Window. 5 | -------------------------------------------------------------------------------- /pslipstream/static/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlaphoenix/Slipstream/5e412deaa58b6e4e4bdecdfde1b492b20ca9ca4b/pslipstream/static/img/icon.ico -------------------------------------------------------------------------------- /pslipstream/static/img/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlaphoenix/Slipstream/5e412deaa58b6e4e4bdecdfde1b492b20ca9ca4b/pslipstream/static/img/icon.xcf -------------------------------------------------------------------------------- /pslipstream/static/img/info-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /pslipstream/static/img/music-disc-with-luster.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /pslipstream/static/img/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /pslipstream/static/style.qss: -------------------------------------------------------------------------------- 1 | * { 2 | color: rgb(210, 211, 211); 3 | background-color: rgb(32, 34, 37); 4 | } 5 | 6 | QPushButton { 7 | border: none; 8 | background-color: rgb(109, 109, 109); 9 | border-radius: 2px; 10 | padding: 4px 8px; 11 | text-align: left; 12 | } 13 | QPushButton:hover:!pressed { 14 | background-color: rgb(139, 139, 139); 15 | } 16 | QPushButton:disabled { 17 | background-color: rgb(39, 39, 39); 18 | } 19 | 20 | QPushButton#backupButton { 21 | background-color: rgb(0, 105, 192); 22 | text-align: center; 23 | } 24 | QPushButton#backupButton:hover:!pressed { 25 | background-color: rgb(30, 135, 222); 26 | } 27 | QPushButton#backupButton:disabled { 28 | background-color: transparent; 29 | } 30 | 31 | QTextEdit { 32 | background-color: rgb(24, 24, 24); 33 | border-radius: 4px; 34 | } 35 | QTextEdit#log { 36 | padding: 10px; 37 | } 38 | 39 | QScrollBar { 40 | border: none; 41 | border-radius: 1px; 42 | } 43 | QScrollBar:vertical { 44 | width: 8px; 45 | margin-left: 4px; 46 | } 47 | QScrollBar:horizontal { 48 | height: 8px; 49 | margin-top: 4px; 50 | } 51 | QScrollBar:add-line, 52 | QScrollBar:sub-line { 53 | width: 0px; 54 | } 55 | QScrollBar:handle { 56 | background: white; 57 | border-radius: 2px; 58 | } 59 | 60 | QProgressBar { 61 | margin: -1px -1px -1px -1px; 62 | border: none; 63 | background-color: rgb(24, 24, 24); 64 | max-height: 5px; 65 | } 66 | 67 | QMenuBar { 68 | border-bottom: 1px solid rgb(24, 24, 24); 69 | } 70 | -------------------------------------------------------------------------------- /pyinstaller.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import shutil 3 | import struct 4 | from datetime import datetime 5 | from pathlib import Path 6 | from textwrap import dedent 7 | from typing import List 8 | 9 | import click 10 | from PyInstaller.__main__ import run 11 | 12 | from pslipstream import __version__ 13 | 14 | 15 | @click.command() 16 | @click.option("--debug", is_flag=True, help="Enable debug mode (keeps leftover build files)") 17 | @click.option("--name", default="Slipstream", help="Set the Project Name") 18 | @click.option("--author", default="rlaphoenix", help="Set the Project Author") 19 | @click.option("--version", default=__version__, help="Set the EXE Version") 20 | @click.option("--icon-file", default="pslipstream/static/img/icon.ico", 21 | help="Set the Icon file path (must be a .ICO file)") 22 | @click.option("--one-file", is_flag=True, help="Build to a singular .exe file") 23 | @click.option("--console", is_flag=True, help="Show the Console window") 24 | def main(debug: bool, name: str, author: str, version: str, icon_file: str, one_file: bool, console: bool) -> None: 25 | # Configuration options 26 | additional_data: List[List[str]] = [ 27 | # local file path, destination in build output 28 | ["pslipstream/static", "pslipstream/static"], 29 | [f"submodules/libdvdcss/1.4.3/{8 * struct.calcsize('P')}-bit/libdvdcss-2.dll", "."] 30 | ] 31 | hidden_imports: List[str] = [] 32 | extra_args: List[str] = ["-y"] 33 | 34 | # Prepare environment 35 | shutil.rmtree("build", ignore_errors=True) 36 | shutil.rmtree("dist", ignore_errors=True) 37 | Path("Slipstream.spec").unlink(missing_ok=True) 38 | version_file = Path("pyinstaller.version.txt") 39 | 40 | # Create Version file 41 | version_file.write_text( 42 | dedent(f""" 43 | VSVersionInfo( 44 | ffi=FixedFileInfo( 45 | filevers=({", ".join(version.split("."))}, 0), 46 | prodvers=({", ".join(version.split("."))}, 0), 47 | OS=0x40004, # Windows NT 48 | fileType=0x1, # Application 49 | subtype=0x0 # type is undefined 50 | ), 51 | kids=[ 52 | StringFileInfo( 53 | [ 54 | StringTable( 55 | '040904b0', 56 | [StringStruct('CompanyName', '{author}'), 57 | StringStruct('FileDescription', 'The most informative Home-media backup solution'), 58 | StringStruct('FileVersion', '{version}'), 59 | StringStruct('InternalName', '{name}'), 60 | StringStruct('LegalCopyright', '{f"Copyright (C) 2020-{datetime.now().year} {author}"}'), 61 | StringStruct('OriginalFilename', 'Slipstream.exe'), 62 | StringStruct('ProductName', '{name}'), 63 | StringStruct('ProductVersion', '{version}'), 64 | StringStruct('Comments', '{name}')]) 65 | ]), 66 | VarFileInfo([VarStruct('Translation', [1033, 1200])]) 67 | ] 68 | ) 69 | """).strip(), 70 | encoding="utf8" 71 | ) 72 | 73 | try: 74 | run([ 75 | "pslipstream/__main__.py", 76 | "-n", name, 77 | "-i", ["NONE", icon_file][bool(icon_file)], 78 | ["-D", "-F"][one_file], 79 | ["-w", "-c"][console], 80 | *itertools.chain(*[["--add-data", ":".join(x)] for x in additional_data]), 81 | *itertools.chain(*[["--hidden-import", x] for x in hidden_imports]), 82 | "--version-file", str(version_file), 83 | *extra_args 84 | ]) 85 | finally: 86 | if not debug: 87 | shutil.rmtree("build", ignore_errors=True) 88 | Path("Slipstream.spec").unlink(missing_ok=True) 89 | version_file.unlink(missing_ok=True) 90 | 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /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 = "pslipstream" 7 | version = "1.0.0" 8 | description = "The most informative Home-media backup solution." 9 | license = "GPLv3" 10 | authors = ["rlaphoenix "] 11 | readme = "README.md" 12 | repository = "https://github.com/rlaphoenix/slipstream" 13 | keywords = ["python", "dvd", "backup"] 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: End Users/Desktop", 17 | "Natural Language :: English", 18 | "Operating System :: Microsoft :: Windows", 19 | "Operating System :: Microsoft :: Windows :: Windows 7", 20 | "Operating System :: Microsoft :: Windows :: Windows 8", 21 | "Operating System :: Microsoft :: Windows :: Windows 8.1", 22 | "Operating System :: Microsoft :: Windows :: Windows 10", 23 | "Operating System :: Microsoft :: Windows :: Windows 11", 24 | "Operating System :: Microsoft :: Windows :: Windows Server 2008", 25 | "Topic :: Multimedia", 26 | "Topic :: Multimedia :: Video", 27 | "Topic :: Multimedia :: Video :: Conversion", 28 | "Topic :: Security :: Cryptography", 29 | ] 30 | include = [ 31 | { path = "CHANGELOG.md", format = "sdist" }, 32 | { path = "README.md", format = "sdist" }, 33 | { path = "LICENSE", format = "sdist" }, 34 | "static/*", 35 | ] 36 | 37 | [tool.poetry.urls] 38 | "Bug Tracker" = "https://github.com/rlaphoenix/slipstream/issues" 39 | "Forums" = "https://github.com/rlaphoenix/slipstream/discussions" 40 | "Changelog" = "https://github.com/rlaphoenix/slipstream/blob/master/CHANGELOG.md" 41 | 42 | [tool.poetry.dependencies] 43 | python = ">=3.8,<3.13" 44 | pycdlib = "^1.14.0" 45 | pydvdcss = "^1.4.0" 46 | pydvdid-m = "^1.1.1" 47 | appdirs = "^1.4.4" 48 | tqdm = "^4.66.4" 49 | PySide6-Essentials = "^6.6.3.1" 50 | click = "^8.1.7" 51 | coloredlogs = "^15.0.1" 52 | jsonpickle = "^3.2.2" 53 | pywin32 = {version = "306", platform = "win32"} 54 | WMI = {version = "^1.5.1", platform = "win32"} 55 | pyinstaller = {version = "^6.9.0", optional = true} 56 | 57 | [tool.poetry.dev-dependencies] 58 | ruff = "~0.5.1" 59 | isort = "^5.13.2" 60 | mypy = "^1.10.1" 61 | pre-commit = "^3.5.0" 62 | types-python-dateutil = "^2.9.0.20240316" 63 | 64 | [tool.poetry.extras] 65 | pyinstaller = ["pyinstaller"] 66 | 67 | [tool.poetry.scripts] 68 | slipstream = 'pslipstream.main:main' 69 | 70 | [tool.ruff] 71 | exclude = [ 72 | ".venv", 73 | "build", 74 | "dist", 75 | "*_pb2.py", 76 | "*.pyi", 77 | ] 78 | ignore = [] 79 | line-length = 120 80 | select = ["E", "F", "W"] 81 | 82 | [tool.isort] 83 | line_length = 120 84 | 85 | [tool.mypy] 86 | check_untyped_defs = true 87 | disallow_incomplete_defs = true 88 | disallow_untyped_defs = true 89 | follow_imports = 'silent' 90 | ignore_missing_imports = true 91 | no_implicit_optional = true 92 | -------------------------------------------------------------------------------- /setup.iss: -------------------------------------------------------------------------------- 1 | ; https://jrsoftware.org/ishelp/index.php 2 | 3 | #define AppName "Slipstream" 4 | #define Version "1.0.0" 5 | 6 | [Setup] 7 | AppId={#AppName} 8 | AppName={#AppName} 9 | AppPublisher=rlaphoenix 10 | AppPublisherURL=https://github.com/rlaphoenix/slipstream 11 | AppReadmeFile=https://github.com/rlaphoenix/Slipstream/blob/master/README.md 12 | AppSupportURL=https://github.com/rlaphoenix/Slipstream/discussions 13 | AppUpdatesURL=https://github.com/rlaphoenix/slipstream/releases 14 | AppVerName={#AppName} {#Version} 15 | AppVersion={#Version} 16 | ArchitecturesAllowed=x64 17 | Compression=lzma2/max 18 | DefaultDirName={autopf}\{#AppName} 19 | LicenseFile=LICENSE 20 | ; Python 3.9 has dropped support for <= Windows 7/Server 2008 R2 SP1. https://jrsoftware.org/ishelp/index.php?topic=winvernotes 21 | MinVersion=6.2 22 | OutputBaseFilename=Slipstream-Setup 23 | OutputDir=dist 24 | OutputManifestFile=Slipstream-Setup-Manifest.txt 25 | PrivilegesRequiredOverridesAllowed=dialog commandline 26 | SetupIconFile=pslipstream/static/img/icon.ico 27 | SolidCompression=yes 28 | VersionInfoVersion=0.1.0 29 | WizardStyle=modern 30 | 31 | [Languages] 32 | Name: "english"; MessagesFile: "compiler:Default.isl" 33 | 34 | [Tasks] 35 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 36 | 37 | [Files] 38 | Source: dist\Slipstream\{#AppName}.exe; DestDir: {app}; Flags: ignoreversion 39 | Source: dist\Slipstream\_internal\libdvdcss-2.dll; DestDir: {app}; Flags: onlyifdoesntexist 40 | Source: dist\Slipstream\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppName}.exe" 45 | Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppName}.exe"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#AppName}.exe"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | --------------------------------------------------------------------------------