riplex 0.9.0__tar.gz → 0.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {riplex-0.9.0 → riplex-0.9.2}/.github/workflows/release.yml +20 -0
- {riplex-0.9.0/src/riplex.egg-info → riplex-0.9.2}/PKG-INFO +2 -1
- {riplex-0.9.0 → riplex-0.9.2}/docs/changelog.md +6 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/getting-started/installation.md +3 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/troubleshooting.md +2 -0
- {riplex-0.9.0 → riplex-0.9.2}/pyproject.toml +1 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/metadata/sources/tmdb.py +35 -6
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/snapshot.py +36 -1
- {riplex-0.9.0 → riplex-0.9.2/src/riplex.egg-info}/PKG-INFO +2 -1
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex.egg-info/SOURCES.txt +4 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex.egg-info/requires.txt +1 -0
- riplex-0.9.2/src/riplex.egg-info/scm_file_list.json +136 -0
- riplex-0.9.2/src/riplex.egg-info/scm_version.json +8 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/main.py +49 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/disc_overview.py +44 -3
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/release.py +20 -3
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/selection.py +93 -18
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/welcome.py +3 -2
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/setup.py +1 -1
- riplex-0.9.2/tests/test_app_main.py +94 -0
- riplex-0.9.2/tests/test_release_screen.py +72 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_snapshot.py +37 -0
- riplex-0.9.2/tests/test_tmdb_provider.py +57 -0
- riplex-0.9.0/tests/test_release_screen.py +0 -40
- {riplex-0.9.0 → riplex-0.9.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.github/copilot-instructions.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.github/workflows/publish.yml +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.gitignore +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/.vscode/settings.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/CONTRIBUTORS.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/LICENSE +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/README.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/architecture.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/cli-guide/lookup.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/cli-guide/orchestrate.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/cli-guide/organize.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/cli-guide/workflow.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/getting-started/configuration.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/gui-guide/gui-walkthrough.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/index.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/naming-rules.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/docs/reference/cli.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/issues/planned-features.md +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/mkdocs.yml +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/1_Welcome_Screen.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/2_Disc_Detection_BTTF.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/4_Disc_Release_BTTF.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/setup.cfg +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/cache.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/config.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/dedup.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/detect.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/disc/provider.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/formatter.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/lookup.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/manifest.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/matcher.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/models.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/normalize.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/organizer.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/scanner.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/splitter.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/tagger.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/title.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/ui.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex/updater.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/crash_dump.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/keep_awake.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/src/riplex_cli/main.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/__init__.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_cache.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_cli_utils.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_config.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_dedup.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_detect.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_disc_analysis.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_disc_detection_screen.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_disc_provider.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_formatter.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_lookup.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_makemkv.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_matcher.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_normalize.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_organizer.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_planner.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_rip_guide.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_scanner.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_splitter.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_tagger.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_ui.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_updater.py +0 -0
- {riplex-0.9.0 → riplex-0.9.2}/tests/test_welcome_screen.py +0 -0
|
@@ -25,6 +25,10 @@ jobs:
|
|
|
25
25
|
pip install -e ".[gui]"
|
|
26
26
|
pip install flet-desktop pyinstaller
|
|
27
27
|
|
|
28
|
+
- name: Cache Flet desktop runtime
|
|
29
|
+
run: |
|
|
30
|
+
python -c "import flet_desktop; flet_desktop.ensure_client_cached()"
|
|
31
|
+
|
|
28
32
|
- name: Build CLI executable
|
|
29
33
|
run: |
|
|
30
34
|
pyinstaller --name riplex --onefile --console ^
|
|
@@ -33,12 +37,15 @@ jobs:
|
|
|
33
37
|
--hidden-import riplex_cli.commands.organize ^
|
|
34
38
|
--hidden-import riplex_cli.commands.lookup ^
|
|
35
39
|
--hidden-import riplex_cli.commands.setup ^
|
|
40
|
+
--hidden-import certifi ^
|
|
41
|
+
--collect-data certifi ^
|
|
36
42
|
--collect-data dvdcompare_scraper ^
|
|
37
43
|
src/riplex_cli/main.py
|
|
38
44
|
shell: cmd
|
|
39
45
|
|
|
40
46
|
- name: Build GUI executable
|
|
41
47
|
run: |
|
|
48
|
+
for /f "delims=" %%i in ('python -c "import flet_desktop; print(flet_desktop.ensure_client_cached())"') do set FLET_CLIENT_CACHE=%%i
|
|
42
49
|
pyinstaller --name riplex-ui --onefile --windowed ^
|
|
43
50
|
--hidden-import riplex_app.screens.welcome ^
|
|
44
51
|
--hidden-import riplex_app.screens.disc_detection ^
|
|
@@ -54,9 +61,12 @@ jobs:
|
|
|
54
61
|
--hidden-import riplex_app.screens.disc_overview ^
|
|
55
62
|
--hidden-import riplex_app.screens.disc_swap ^
|
|
56
63
|
--hidden-import riplex_app.screens.update ^
|
|
64
|
+
--hidden-import certifi ^
|
|
57
65
|
--collect-all flet ^
|
|
58
66
|
--collect-all flet_desktop ^
|
|
67
|
+
--collect-data certifi ^
|
|
59
68
|
--collect-data dvdcompare_scraper ^
|
|
69
|
+
--add-data "%FLET_CLIENT_CACHE%\flet;flet_client" ^
|
|
60
70
|
src/riplex_app/main.py
|
|
61
71
|
shell: cmd
|
|
62
72
|
|
|
@@ -88,6 +98,10 @@ jobs:
|
|
|
88
98
|
pip install -e ".[gui]"
|
|
89
99
|
pip install flet-desktop pyinstaller
|
|
90
100
|
|
|
101
|
+
- name: Cache Flet desktop runtime
|
|
102
|
+
run: |
|
|
103
|
+
python -c "import flet_desktop; flet_desktop.ensure_client_cached()"
|
|
104
|
+
|
|
91
105
|
- name: Build CLI executable
|
|
92
106
|
run: |
|
|
93
107
|
pyinstaller --name riplex --onefile --console \
|
|
@@ -96,11 +110,14 @@ jobs:
|
|
|
96
110
|
--hidden-import riplex_cli.commands.organize \
|
|
97
111
|
--hidden-import riplex_cli.commands.lookup \
|
|
98
112
|
--hidden-import riplex_cli.commands.setup \
|
|
113
|
+
--hidden-import certifi \
|
|
114
|
+
--collect-data certifi \
|
|
99
115
|
--collect-data dvdcompare_scraper \
|
|
100
116
|
src/riplex_cli/main.py
|
|
101
117
|
|
|
102
118
|
- name: Build GUI app bundle
|
|
103
119
|
run: |
|
|
120
|
+
FLET_CLIENT_CACHE="$(python -c 'import flet_desktop; print(flet_desktop.ensure_client_cached())')"
|
|
104
121
|
pyinstaller --name riplex-ui --onedir --windowed \
|
|
105
122
|
--hidden-import riplex_app.screens.welcome \
|
|
106
123
|
--hidden-import riplex_app.screens.disc_detection \
|
|
@@ -116,9 +133,12 @@ jobs:
|
|
|
116
133
|
--hidden-import riplex_app.screens.disc_overview \
|
|
117
134
|
--hidden-import riplex_app.screens.disc_swap \
|
|
118
135
|
--hidden-import riplex_app.screens.update \
|
|
136
|
+
--hidden-import certifi \
|
|
119
137
|
--collect-all flet \
|
|
120
138
|
--collect-all flet_desktop \
|
|
139
|
+
--collect-data certifi \
|
|
121
140
|
--collect-data dvdcompare_scraper \
|
|
141
|
+
--add-data "$FLET_CLIENT_CACHE:flet_client" \
|
|
122
142
|
src/riplex_app/main.py
|
|
123
143
|
|
|
124
144
|
- name: Package macOS artifacts
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
+
Requires-Dist: certifi>=2024.2.2
|
|
9
10
|
Requires-Dist: httpx>=0.27
|
|
10
11
|
Requires-Dist: dvdcompare-scraper>=0.1.15
|
|
11
12
|
Requires-Dist: platformdirs>=4.0
|
|
@@ -4,6 +4,12 @@ All notable changes to the riplex documentation are recorded here.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## v0.9.2 — 2026-06-24
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **TMDb credentials: accept both the API Key and the Read Access Token.** TMDb's settings page offers a v3 API Key (query parameter auth) and a v4 API Read Access Token (bearer header auth). riplex now auto-detects which credential was provided and uses the matching auth scheme, so pasting the Read Access Token no longer fails with `401 Unauthorized`. The setup wizard, GUI welcome screen, and docs now note that either credential works.
|
|
12
|
+
|
|
7
13
|
## v0.9.0 — 2026-06-13
|
|
8
14
|
|
|
9
15
|
Summary: Plex-aligned movie version and edition support for combo-disc releases, with better organize preview matching for 4K, Blu-ray, and 3D movie rips.
|
|
@@ -243,6 +243,9 @@ The setup wizard will:
|
|
|
243
243
|
> enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex`
|
|
244
244
|
> as the URL. The rest of the form can be filled with basic info -- it
|
|
245
245
|
> doesn't need to be a real business. The key is approved instantly.
|
|
246
|
+
>
|
|
247
|
+
> The settings page offers both an **API Key** (v3) and an **API Read
|
|
248
|
+
> Access Token** (v4). riplex accepts either one.
|
|
246
249
|
|
|
247
250
|
2. Ask where your Plex library and MakeMKV rip folders are
|
|
248
251
|
3. Check for required tools (MakeMKV, ffprobe, mkvmerge, mkvpropedit)
|
|
@@ -74,6 +74,8 @@ riplex setup --force
|
|
|
74
74
|
|
|
75
75
|
**Solution:** TMDb asks for an app name and URL when you request a key. You can enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex` as the URL. The rest of the form can be filled with basic info - it doesn't need to be a real business. The key is approved instantly.
|
|
76
76
|
|
|
77
|
+
The settings page shows two credentials: an **API Key** (v3) and an **API Read Access Token** (v4). riplex accepts either one - paste whichever you copied. If you get a `401 Unauthorized` error, double-check that the value was copied in full.
|
|
78
|
+
|
|
77
79
|
Sign up and request a key at: https://www.themoviedb.org/settings/api
|
|
78
80
|
|
|
79
81
|
---
|
|
@@ -28,22 +28,44 @@ TMDB_BASE_URL = "https://api.themoviedb.org/3"
|
|
|
28
28
|
_TMDB_TTL_DAYS = 7
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _looks_like_read_access_token(key: str) -> bool:
|
|
32
|
+
"""Return ``True`` when *key* looks like a TMDb v4 Read Access Token.
|
|
33
|
+
|
|
34
|
+
TMDb's settings page offers two credentials: a v3 ``API Key`` (a 32-char
|
|
35
|
+
hex string used as the ``api_key`` query parameter) and a v4 ``API Read
|
|
36
|
+
Access Token`` (a JWT used as an ``Authorization: Bearer`` header). The
|
|
37
|
+
v4 token is a JWT, so it starts with ``eyJ`` and contains exactly two
|
|
38
|
+
dots; a v3 key never does.
|
|
39
|
+
"""
|
|
40
|
+
return key.startswith("eyJ") and key.count(".") == 2
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
class TmdbProvider(MetadataProvider):
|
|
32
44
|
"""Metadata provider backed by TMDb API v3."""
|
|
33
45
|
|
|
34
46
|
def __init__(self, api_key: str | None = None) -> None:
|
|
35
|
-
self._api_key = api_key or os.environ.get("TMDB_API_KEY", "")
|
|
47
|
+
self._api_key = (api_key or os.environ.get("TMDB_API_KEY", "")).strip()
|
|
36
48
|
if not self._api_key:
|
|
37
49
|
raise ValueError(
|
|
38
50
|
"TMDb API key is required. Pass --api-key, set the "
|
|
39
51
|
"TMDB_API_KEY environment variable, or add tmdb_api_key "
|
|
40
52
|
"to your config file."
|
|
41
53
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
# TMDb accepts either a v3 API Key (query parameter) or a v4 Read
|
|
55
|
+
# Access Token (Authorization header) against the v3 endpoints. Pick
|
|
56
|
+
# the matching auth scheme so both credential types work.
|
|
57
|
+
if _looks_like_read_access_token(self._api_key):
|
|
58
|
+
self._client = httpx.AsyncClient(
|
|
59
|
+
base_url=TMDB_BASE_URL,
|
|
60
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
61
|
+
timeout=30.0,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
self._client = httpx.AsyncClient(
|
|
65
|
+
base_url=TMDB_BASE_URL,
|
|
66
|
+
params={"api_key": self._api_key},
|
|
67
|
+
timeout=30.0,
|
|
68
|
+
)
|
|
47
69
|
|
|
48
70
|
async def close(self) -> None:
|
|
49
71
|
await self._client.aclose()
|
|
@@ -61,6 +83,13 @@ class TmdbProvider(MetadataProvider):
|
|
|
61
83
|
if cached is not None:
|
|
62
84
|
return cached
|
|
63
85
|
resp = await self._client.get(path, params=params)
|
|
86
|
+
if resp.status_code == 401:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"TMDb rejected the credential (401 Unauthorized). Check that "
|
|
89
|
+
"your TMDb API Key (v3) or API Read Access Token (v4) is "
|
|
90
|
+
"correct. Both are available at "
|
|
91
|
+
"https://www.themoviedb.org/settings/api."
|
|
92
|
+
)
|
|
64
93
|
resp.raise_for_status()
|
|
65
94
|
data = resp.json()
|
|
66
95
|
if cache_ns and cache_key:
|
|
@@ -34,7 +34,7 @@ https://github.com/AnyCredit5518/riplex/issues/new?template=bug_report.yml
|
|
|
34
34
|
Files you may find here:
|
|
35
35
|
riplex-rip.snapshot.json - Disc info, metadata, and title classifications
|
|
36
36
|
riplex-scan.snapshot.json - Scanned file metadata (durations, streams, etc.)
|
|
37
|
-
|
|
37
|
+
riplex-rip.manifest.json - Map of selected/ripped title indexes to output filenames
|
|
38
38
|
riplex.log - Debug log from the most recent session
|
|
39
39
|
"""
|
|
40
40
|
|
|
@@ -121,6 +121,8 @@ def save_rip_snapshot(
|
|
|
121
121
|
discs: list | None = None,
|
|
122
122
|
ripped_titles: list[int] | None = None,
|
|
123
123
|
selected_titles: list[int] | None = None,
|
|
124
|
+
rippable_titles: list[int] | None = None,
|
|
125
|
+
classifications: dict[int, str] | None = None,
|
|
124
126
|
phase: str = "complete",
|
|
125
127
|
) -> Path | None:
|
|
126
128
|
"""Write ``riplex-rip.snapshot.json`` into *debug_dir*.
|
|
@@ -132,16 +134,31 @@ def save_rip_snapshot(
|
|
|
132
134
|
Returns the path on success, or ``None`` on failure.
|
|
133
135
|
"""
|
|
134
136
|
try:
|
|
137
|
+
selected_set = set(selected_titles or [])
|
|
138
|
+
rippable_set = set(rippable_titles or [])
|
|
139
|
+
classifications = classifications or {}
|
|
135
140
|
data = {
|
|
136
141
|
"disc_name": disc_info.disc_name,
|
|
137
142
|
"title_count": len(disc_info.titles),
|
|
138
143
|
"titles": [
|
|
139
144
|
{
|
|
140
145
|
"index": t.index,
|
|
146
|
+
"name": getattr(t, "name", ""),
|
|
141
147
|
"duration_seconds": t.duration_seconds,
|
|
142
148
|
"resolution": t.resolution,
|
|
143
149
|
"size_bytes": t.size_bytes,
|
|
144
150
|
"chapters": getattr(t, "chapter_count", t.chapters if hasattr(t, "chapters") else None),
|
|
151
|
+
"filename": getattr(t, "filename", ""),
|
|
152
|
+
"playlist": getattr(t, "playlist", ""),
|
|
153
|
+
"video_codec": getattr(t, "video_codec", ""),
|
|
154
|
+
"audio_tracks": list(getattr(t, "audio_tracks", []) or []),
|
|
155
|
+
"subtitle_tracks": list(getattr(t, "subtitle_tracks", []) or []),
|
|
156
|
+
"stream_count": getattr(t, "stream_count", 0),
|
|
157
|
+
"segment_count": getattr(t, "segment_count", 1),
|
|
158
|
+
"segment_map": getattr(t, "segment_map", ""),
|
|
159
|
+
"classification": classifications.get(t.index, ""),
|
|
160
|
+
"recommended": t.index in rippable_set,
|
|
161
|
+
"selected": t.index in selected_set,
|
|
145
162
|
}
|
|
146
163
|
for t in disc_info.titles
|
|
147
164
|
],
|
|
@@ -157,14 +174,32 @@ def save_rip_snapshot(
|
|
|
157
174
|
"discs": [
|
|
158
175
|
{
|
|
159
176
|
"number": d.number,
|
|
177
|
+
"format": getattr(d, "disc_format", ""),
|
|
178
|
+
"is_film": getattr(d, "is_film", False),
|
|
160
179
|
"episode_count": len(d.episodes),
|
|
161
180
|
"extra_count": len(d.extras),
|
|
181
|
+
"episodes": [
|
|
182
|
+
{
|
|
183
|
+
"title": e.title,
|
|
184
|
+
"runtime_seconds": e.runtime_seconds,
|
|
185
|
+
}
|
|
186
|
+
for e in getattr(d, "episodes", [])
|
|
187
|
+
],
|
|
188
|
+
"extras": [
|
|
189
|
+
{
|
|
190
|
+
"title": e.title,
|
|
191
|
+
"runtime_seconds": e.runtime_seconds,
|
|
192
|
+
"feature_type": getattr(e, "feature_type", ""),
|
|
193
|
+
}
|
|
194
|
+
for e in getattr(d, "extras", [])
|
|
195
|
+
],
|
|
162
196
|
}
|
|
163
197
|
for d in (discs or [])
|
|
164
198
|
],
|
|
165
199
|
},
|
|
166
200
|
"phase": phase,
|
|
167
201
|
"selected_titles": selected_titles or [],
|
|
202
|
+
"rippable_titles": rippable_titles or [],
|
|
168
203
|
"ripped_titles": ripped_titles or [],
|
|
169
204
|
}
|
|
170
205
|
snapshot = _v2_envelope("rip", data)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
+
Requires-Dist: certifi>=2024.2.2
|
|
9
10
|
Requires-Dist: httpx>=0.27
|
|
10
11
|
Requires-Dist: dvdcompare-scraper>=0.1.15
|
|
11
12
|
Requires-Dist: platformdirs>=4.0
|
|
@@ -59,6 +59,8 @@ src/riplex.egg-info/SOURCES.txt
|
|
|
59
59
|
src/riplex.egg-info/dependency_links.txt
|
|
60
60
|
src/riplex.egg-info/entry_points.txt
|
|
61
61
|
src/riplex.egg-info/requires.txt
|
|
62
|
+
src/riplex.egg-info/scm_file_list.json
|
|
63
|
+
src/riplex.egg-info/scm_version.json
|
|
62
64
|
src/riplex.egg-info/top_level.txt
|
|
63
65
|
src/riplex/disc/__init__.py
|
|
64
66
|
src/riplex/disc/analysis.py
|
|
@@ -99,6 +101,7 @@ src/riplex_cli/commands/organize.py
|
|
|
99
101
|
src/riplex_cli/commands/rip.py
|
|
100
102
|
src/riplex_cli/commands/setup.py
|
|
101
103
|
tests/__init__.py
|
|
104
|
+
tests/test_app_main.py
|
|
102
105
|
tests/test_cache.py
|
|
103
106
|
tests/test_cli_utils.py
|
|
104
107
|
tests/test_config.py
|
|
@@ -121,6 +124,7 @@ tests/test_scanner.py
|
|
|
121
124
|
tests/test_snapshot.py
|
|
122
125
|
tests/test_splitter.py
|
|
123
126
|
tests/test_tagger.py
|
|
127
|
+
tests/test_tmdb_provider.py
|
|
124
128
|
tests/test_ui.py
|
|
125
129
|
tests/test_updater.py
|
|
126
130
|
tests/test_welcome_screen.py
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
"mkdocs.yml",
|
|
7
|
+
"CONTRIBUTORS.md",
|
|
8
|
+
".gitignore",
|
|
9
|
+
"issues/debug-artifacts-consolidation.md",
|
|
10
|
+
"issues/orchestrate-dvdcompare-fallback.md",
|
|
11
|
+
"issues/manual-rip-and-organize-escape-hatch.md",
|
|
12
|
+
"issues/planned-features.md",
|
|
13
|
+
".vscode/settings.json",
|
|
14
|
+
"docs/naming-rules.md",
|
|
15
|
+
"docs/troubleshooting.md",
|
|
16
|
+
"docs/changelog.md",
|
|
17
|
+
"docs/index.md",
|
|
18
|
+
"docs/architecture.md",
|
|
19
|
+
"docs/cli-guide/organize.md",
|
|
20
|
+
"docs/cli-guide/lookup.md",
|
|
21
|
+
"docs/cli-guide/workflow.md",
|
|
22
|
+
"docs/cli-guide/orchestrate.md",
|
|
23
|
+
"docs/gui-guide/gui-walkthrough.md",
|
|
24
|
+
"docs/reference/cli.md",
|
|
25
|
+
"docs/getting-started/installation.md",
|
|
26
|
+
"docs/getting-started/configuration.md",
|
|
27
|
+
"src/riplex_cli/__init__.py",
|
|
28
|
+
"src/riplex_cli/formatting.py",
|
|
29
|
+
"src/riplex_cli/main.py",
|
|
30
|
+
"src/riplex_cli/commands/__init__.py",
|
|
31
|
+
"src/riplex_cli/commands/setup.py",
|
|
32
|
+
"src/riplex_cli/commands/lookup.py",
|
|
33
|
+
"src/riplex_cli/commands/organize.py",
|
|
34
|
+
"src/riplex_cli/commands/orchestrate.py",
|
|
35
|
+
"src/riplex_cli/commands/rip.py",
|
|
36
|
+
"src/riplex_app/crash_dump.py",
|
|
37
|
+
"src/riplex_app/__init__.py",
|
|
38
|
+
"src/riplex_app/bug_report.py",
|
|
39
|
+
"src/riplex_app/main.py",
|
|
40
|
+
"src/riplex_app/keep_awake.py",
|
|
41
|
+
"src/riplex_app/screens/disc_overview.py",
|
|
42
|
+
"src/riplex_app/screens/__init__.py",
|
|
43
|
+
"src/riplex_app/screens/organize_done.py",
|
|
44
|
+
"src/riplex_app/screens/folder_picker.py",
|
|
45
|
+
"src/riplex_app/screens/selection.py",
|
|
46
|
+
"src/riplex_app/screens/orchestrate_done.py",
|
|
47
|
+
"src/riplex_app/screens/progress.py",
|
|
48
|
+
"src/riplex_app/screens/metadata.py",
|
|
49
|
+
"src/riplex_app/screens/organize_preview.py",
|
|
50
|
+
"src/riplex_app/screens/release.py",
|
|
51
|
+
"src/riplex_app/screens/done.py",
|
|
52
|
+
"src/riplex_app/screens/update.py",
|
|
53
|
+
"src/riplex_app/screens/disc_swap.py",
|
|
54
|
+
"src/riplex_app/screens/disc_detection.py",
|
|
55
|
+
"src/riplex_app/screens/welcome.py",
|
|
56
|
+
"src/riplex/organizer.py",
|
|
57
|
+
"src/riplex/matcher.py",
|
|
58
|
+
"src/riplex/formatter.py",
|
|
59
|
+
"src/riplex/updater.py",
|
|
60
|
+
"src/riplex/__init__.py",
|
|
61
|
+
"src/riplex/normalize.py",
|
|
62
|
+
"src/riplex/config.py",
|
|
63
|
+
"src/riplex/models.py",
|
|
64
|
+
"src/riplex/snapshot.py",
|
|
65
|
+
"src/riplex/tagger.py",
|
|
66
|
+
"src/riplex/lookup.py",
|
|
67
|
+
"src/riplex/title.py",
|
|
68
|
+
"src/riplex/scanner.py",
|
|
69
|
+
"src/riplex/manifest.py",
|
|
70
|
+
"src/riplex/cache.py",
|
|
71
|
+
"src/riplex/dedup.py",
|
|
72
|
+
"src/riplex/ui.py",
|
|
73
|
+
"src/riplex/splitter.py",
|
|
74
|
+
"src/riplex/detect.py",
|
|
75
|
+
"src/riplex/disc/makemkv.py",
|
|
76
|
+
"src/riplex/disc/__init__.py",
|
|
77
|
+
"src/riplex/disc/analysis.py",
|
|
78
|
+
"src/riplex/disc/provider.py",
|
|
79
|
+
"src/riplex/metadata/__init__.py",
|
|
80
|
+
"src/riplex/metadata/planner.py",
|
|
81
|
+
"src/riplex/metadata/provider.py",
|
|
82
|
+
"src/riplex/metadata/sources/__init__.py",
|
|
83
|
+
"src/riplex/metadata/sources/tmdb.py",
|
|
84
|
+
"tests/test_rip_guide.py",
|
|
85
|
+
"tests/test_planner.py",
|
|
86
|
+
"tests/test_makemkv.py",
|
|
87
|
+
"tests/test_config.py",
|
|
88
|
+
"tests/__init__.py",
|
|
89
|
+
"tests/test_matcher.py",
|
|
90
|
+
"tests/test_disc_fixtures.py",
|
|
91
|
+
"tests/test_splitter.py",
|
|
92
|
+
"tests/test_detect.py",
|
|
93
|
+
"tests/test_dedup.py",
|
|
94
|
+
"tests/test_app_main.py",
|
|
95
|
+
"tests/test_scanner.py",
|
|
96
|
+
"tests/test_cli_utils.py",
|
|
97
|
+
"tests/test_disc_detection_screen.py",
|
|
98
|
+
"tests/test_lookup.py",
|
|
99
|
+
"tests/test_welcome_screen.py",
|
|
100
|
+
"tests/test_tagger.py",
|
|
101
|
+
"tests/test_snapshot.py",
|
|
102
|
+
"tests/test_formatter.py",
|
|
103
|
+
"tests/test_updater.py",
|
|
104
|
+
"tests/test_tmdb_provider.py",
|
|
105
|
+
"tests/test_normalize.py",
|
|
106
|
+
"tests/test_disc_analysis.py",
|
|
107
|
+
"tests/test_cache.py",
|
|
108
|
+
"tests/test_ui.py",
|
|
109
|
+
"tests/test_release_screen.py",
|
|
110
|
+
"tests/test_organizer.py",
|
|
111
|
+
"tests/test_disc_provider.py",
|
|
112
|
+
"tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt",
|
|
113
|
+
"tests/fixtures/makemkvcon_list.txt",
|
|
114
|
+
"tests/fixtures/chernobyl_disc1.json",
|
|
115
|
+
"tests/snapshots/The Dark Knight Rises.snapshot.json",
|
|
116
|
+
"tests/snapshots/Blade Runner The Final Cut.snapshot.json",
|
|
117
|
+
"tests/snapshots/Seven Worlds One Planet.snapshot.json",
|
|
118
|
+
"tests/snapshots/Waterworld.snapshot.json",
|
|
119
|
+
"tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json",
|
|
120
|
+
"tests/snapshots/Batman Begins.snapshot.json",
|
|
121
|
+
"tests/snapshots/The Dark Knight.snapshot.json",
|
|
122
|
+
"screenshots/0_Rip_Flow_BTTF.gif",
|
|
123
|
+
"screenshots/5_Select_Title_to_RIP_BTTF.png",
|
|
124
|
+
"screenshots/4_Disc_Release_BTTF.png",
|
|
125
|
+
"screenshots/5_Multi_Disc_Overview_BTTF.png",
|
|
126
|
+
"screenshots/3_Metadata_Lookup_BTTF.png",
|
|
127
|
+
"screenshots/1_Welcome_Screen.png",
|
|
128
|
+
"screenshots/2_Disc_Detection_BTTF.png",
|
|
129
|
+
".github/copilot-instructions.md",
|
|
130
|
+
".github/agents/riplex.agent.md",
|
|
131
|
+
".github/ISSUE_TEMPLATE/crash_report.yml",
|
|
132
|
+
".github/ISSUE_TEMPLATE/bug_report.yml",
|
|
133
|
+
".github/workflows/release.yml",
|
|
134
|
+
".github/workflows/publish.yml"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Riplex GUI - Flet-based companion app for riplex."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
import traceback as tb_module
|
|
@@ -310,7 +311,55 @@ def _parse_exception_summary(traceback_text: str) -> tuple[str, str]:
|
|
|
310
311
|
return last_line or "Exception", ""
|
|
311
312
|
|
|
312
313
|
|
|
314
|
+
def _configure_tls_certificates(env=None):
|
|
315
|
+
"""Point urllib/ssl at certifi before Flet's first-launch download."""
|
|
316
|
+
if env is None:
|
|
317
|
+
env = os.environ
|
|
318
|
+
try:
|
|
319
|
+
import certifi
|
|
320
|
+
except Exception: # pragma: no cover - certifi is an explicit dependency.
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
cert_path = certifi.where()
|
|
324
|
+
env.setdefault("SSL_CERT_FILE", cert_path)
|
|
325
|
+
env.setdefault("REQUESTS_CA_BUNDLE", cert_path)
|
|
326
|
+
return cert_path
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _configure_flet_view_path(env=None, bundle_root=None, platform=None):
|
|
330
|
+
"""Point Flet at a bundled desktop client when one is packaged."""
|
|
331
|
+
if env is None:
|
|
332
|
+
env = os.environ
|
|
333
|
+
if env.get("FLET_VIEW_PATH"):
|
|
334
|
+
return env["FLET_VIEW_PATH"]
|
|
335
|
+
|
|
336
|
+
if bundle_root is None:
|
|
337
|
+
bundle_root = getattr(sys, "_MEIPASS", None)
|
|
338
|
+
if not bundle_root:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
if platform is None:
|
|
342
|
+
platform = sys.platform
|
|
343
|
+
|
|
344
|
+
client_dir = Path(bundle_root) / "flet_client"
|
|
345
|
+
if platform.startswith("win"):
|
|
346
|
+
ready = (client_dir / "flet.exe").is_file()
|
|
347
|
+
elif platform == "darwin":
|
|
348
|
+
ready = any(client_dir.glob("*.app"))
|
|
349
|
+
else:
|
|
350
|
+
ready = (client_dir / "flet").is_file()
|
|
351
|
+
|
|
352
|
+
if not ready:
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
env["FLET_VIEW_PATH"] = str(client_dir)
|
|
356
|
+
return str(client_dir)
|
|
357
|
+
|
|
358
|
+
|
|
313
359
|
def main():
|
|
360
|
+
_configure_tls_certificates()
|
|
361
|
+
_configure_flet_view_path()
|
|
362
|
+
|
|
314
363
|
logging.basicConfig(
|
|
315
364
|
level=logging.DEBUG,
|
|
316
365
|
format="%(asctime)s %(levelname)-5s [%(name)s] %(message)s",
|
|
@@ -34,6 +34,9 @@ class DiscOverviewScreen:
|
|
|
34
34
|
year = tmdb_match.year or 0 if tmdb_match else 0
|
|
35
35
|
release_name = release.name if release else ""
|
|
36
36
|
|
|
37
|
+
if not dvdcompare_discs:
|
|
38
|
+
return self._build_empty_state(match_label=self._match_label(canonical, year))
|
|
39
|
+
|
|
37
40
|
# Detect currently inserted disc
|
|
38
41
|
from riplex.disc.provider import detect_disc_number
|
|
39
42
|
inserted_disc = None
|
|
@@ -123,9 +126,7 @@ class DiscOverviewScreen:
|
|
|
123
126
|
for cb in self.checkboxes:
|
|
124
127
|
cb.on_change = self._update_summary
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
year_str = f" ({year})" if year else ""
|
|
128
|
-
match_label = f"{canonical}{year_str}"
|
|
129
|
+
match_label = self._match_label(canonical, year)
|
|
129
130
|
|
|
130
131
|
# Resume info
|
|
131
132
|
resume_info = ft.Container()
|
|
@@ -182,6 +183,46 @@ class DiscOverviewScreen:
|
|
|
182
183
|
expand=True,
|
|
183
184
|
)
|
|
184
185
|
|
|
186
|
+
def _match_label(self, canonical: str, year: int) -> str:
|
|
187
|
+
year_str = f" ({year})" if year else ""
|
|
188
|
+
return f"{canonical}{year_str}"
|
|
189
|
+
|
|
190
|
+
def _build_empty_state(self, *, match_label: str) -> ft.Control:
|
|
191
|
+
"""Show a recoverable state when orchestrate has no disc structure."""
|
|
192
|
+
return ft.Column(
|
|
193
|
+
[
|
|
194
|
+
ft.Text("Disc Overview", size=24, weight=ft.FontWeight.BOLD),
|
|
195
|
+
ft.Text(match_label, size=14, color=ft.Colors.GREY_400),
|
|
196
|
+
ft.Text(
|
|
197
|
+
"No usable dvdcompare disc structure is selected for this title. "
|
|
198
|
+
"Pick a different dvdcompare result, or continue with duration-only "
|
|
199
|
+
"single-disc selection.",
|
|
200
|
+
size=13,
|
|
201
|
+
color=ft.Colors.ORANGE,
|
|
202
|
+
),
|
|
203
|
+
ft.Container(expand=True),
|
|
204
|
+
ft.Row([
|
|
205
|
+
ft.TextButton("Back", on_click=lambda _: self.app.navigate("release")),
|
|
206
|
+
ft.ElevatedButton(
|
|
207
|
+
"Continue without",
|
|
208
|
+
icon=ft.Icons.ARROW_FORWARD,
|
|
209
|
+
on_click=self._continue_without_dvdcompare,
|
|
210
|
+
),
|
|
211
|
+
]),
|
|
212
|
+
],
|
|
213
|
+
spacing=10,
|
|
214
|
+
expand=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _continue_without_dvdcompare(self, _e):
|
|
218
|
+
"""Proceed with a single-disc duration-only rip when no release data exists."""
|
|
219
|
+
self.app.state["dvdcompare_discs"] = []
|
|
220
|
+
self.app.state["_orchestrate_disc_number"] = 1
|
|
221
|
+
self.app.state["disc_queue"] = [1]
|
|
222
|
+
self.app.state["current_disc_idx"] = 0
|
|
223
|
+
self.app.state["all_rip_results"] = {}
|
|
224
|
+
self.app.navigate("selection")
|
|
225
|
+
|
|
185
226
|
def _update_summary(self, e):
|
|
186
227
|
"""Update summary when checkboxes change."""
|
|
187
228
|
selected_count = sum(1 for cb in self.checkboxes if cb.value)
|
|
@@ -514,12 +514,29 @@ class ReleaseScreen:
|
|
|
514
514
|
|
|
515
515
|
def _use_release(self, release):
|
|
516
516
|
"""Convert a dvdcompare release to PlannedDiscs and navigate."""
|
|
517
|
-
self.app.state["release"] = release
|
|
518
517
|
try:
|
|
519
518
|
discs = _convert_release(release)
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
self.app.state["release"] = None
|
|
521
|
+
self.app.state["dvdcompare_discs"] = []
|
|
522
|
+
self.app.state["_dvdcompare_error"] = (
|
|
523
|
+
f"Could not read disc data from the selected dvdcompare release: {exc}"
|
|
524
|
+
)
|
|
525
|
+
self.app.navigate("release")
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if not discs:
|
|
529
|
+
self.app.state["release"] = None
|
|
522
530
|
self.app.state["dvdcompare_discs"] = []
|
|
531
|
+
self.app.state["_dvdcompare_error"] = (
|
|
532
|
+
"The selected dvdcompare release did not contain usable disc data. "
|
|
533
|
+
"Try a different search title or continue without disc structure data."
|
|
534
|
+
)
|
|
535
|
+
self.app.navigate("release")
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
self.app.state["release"] = release
|
|
539
|
+
self.app.state["dvdcompare_discs"] = discs
|
|
523
540
|
self.app.navigate(self._next_screen)
|
|
524
541
|
|
|
525
542
|
def _score_releases(self, releases) -> int:
|