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 |
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 |
69 |
--------------------------------------------------------------------------------
/pslipstream/static/img/music-disc-with-luster.svg:
--------------------------------------------------------------------------------
1 |
2 |
38 |
--------------------------------------------------------------------------------
/pslipstream/static/img/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------