riplex 0.7.3__tar.gz → 0.8.0__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.7.3/src/riplex.egg-info → riplex-0.8.0}/PKG-INFO +1 -1
- {riplex-0.7.3 → riplex-0.8.0}/docs/changelog.md +16 -0
- riplex-0.8.0/issues/manual-rip-and-organize-escape-hatch.md +106 -0
- {riplex-0.7.3 → riplex-0.8.0/src/riplex.egg-info}/PKG-INFO +1 -1
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex.egg-info/SOURCES.txt +3 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/done.py +1 -1
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/metadata.py +1 -1
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/orchestrate_done.py +5 -5
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/release.py +12 -1
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/welcome.py +59 -10
- riplex-0.8.0/tests/test_release_screen.py +40 -0
- riplex-0.8.0/tests/test_welcome_screen.py +42 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/copilot-instructions.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/workflows/publish.yml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.github/workflows/release.yml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.gitignore +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/.vscode/settings.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/CONTRIBUTORS.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/LICENSE +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/README.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/architecture.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/cli-guide/lookup.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/cli-guide/orchestrate.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/cli-guide/organize.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/cli-guide/workflow.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/getting-started/configuration.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/getting-started/installation.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/gui-guide/gui-walkthrough.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/index.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/naming-rules.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/reference/cli.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/docs/troubleshooting.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/issues/planned-features.md +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/mkdocs.yml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/pyproject.toml +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/1_Welcome_Screen.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/2_Disc_Detection_BTTF.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/4_Disc_Release_BTTF.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/setup.cfg +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/cache.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/config.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/dedup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/detect.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/disc/provider.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/formatter.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/lookup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/manifest.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/matcher.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/models.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/normalize.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/organizer.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/scanner.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/snapshot.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/splitter.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/tagger.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/title.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/ui.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex/updater.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/crash_dump.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/keep_awake.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/main.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/src/riplex_cli/main.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/__init__.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_cache.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_cli_utils.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_config.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_dedup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_detect.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_disc_analysis.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_disc_detection_screen.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_disc_provider.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_formatter.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_lookup.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_makemkv.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_matcher.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_normalize.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_organizer.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_planner.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_rip_guide.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_scanner.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_snapshot.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_splitter.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_tagger.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_ui.py +0 -0
- {riplex-0.7.3 → riplex-0.8.0}/tests/test_updater.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -4,6 +4,22 @@ 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.8.0 — 2026-06-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **GUI: editable settings after first-run setup.** The welcome screen now exposes an **Edit Settings** button after configuration is complete, so users can update the TMDb API key, media library root, MakeMKV rip output folder, and optional archive folder without re-running setup from the command line.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **GUI: less Plex-specific library wording.** User-facing destination copy now refers to a general media library while retaining **Plex-compatible naming** where the current folder/file convention is being described.
|
|
16
|
+
|
|
17
|
+
## v0.7.4 — 2026-06-10
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **GUI: empty Disc Overview after continuing without dvdcompare data.** When dvdcompare had no matching release, the orchestrate flow still navigated to the multi-disc overview, which only renders dvdcompare-provided disc rows. The no-dvdcompare fallback now treats the inserted disc as a single-disc rip and jumps directly to title selection, where TMDb runtime heuristics can pick the main feature.
|
|
22
|
+
|
|
7
23
|
## v0.7.3 — 2026-06-10
|
|
8
24
|
|
|
9
25
|
### Fixed
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# GUI: manual rip and manual organize escape hatches
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
When riplex can't classify a disc — no dvdcompare match, ambiguous TMDb
|
|
6
|
+
result, multi-feature compilation, region-coded oddity, etc. — the GUI flow
|
|
7
|
+
dead-ends at the title-selection screen. The user has to leave riplex-ui,
|
|
8
|
+
launch the MakeMKV GUI to rip the disc directly, and then either keep those
|
|
9
|
+
files unorganized or manually rename and move them into Plex layout.
|
|
10
|
+
|
|
11
|
+
We hit this in practice with a BBC Earth Blu-ray containing two unrelated
|
|
12
|
+
features (*Wild Africa* + *Tiny Giants*). The disc isn't on dvdcompare, TMDb
|
|
13
|
+
has only a combined "double feature" entry, and riplex's planner assumes one
|
|
14
|
+
disc → one TMDb item, so the rip flow couldn't proceed at all.
|
|
15
|
+
|
|
16
|
+
## Why we don't want to model compilations directly
|
|
17
|
+
|
|
18
|
+
Compilation discs that contain multiple unrelated films are rare enough that
|
|
19
|
+
building first-class support (per-title TMDb matching, per-title disc
|
|
20
|
+
metadata, multi-target organize) isn't justified. A generic escape hatch is
|
|
21
|
+
cheaper and covers other niche scenarios as a side benefit (foreign releases
|
|
22
|
+
without metadata, damaged-disc recovery, special workshop discs, etc.).
|
|
23
|
+
|
|
24
|
+
## Proposed UX
|
|
25
|
+
|
|
26
|
+
Two new entry points in the GUI, both intentionally generic:
|
|
27
|
+
|
|
28
|
+
### 1. "Manual rip" button on the title-selection screen
|
|
29
|
+
|
|
30
|
+
When riplex can't classify titles confidently (no dvdcompare match **and**
|
|
31
|
+
TMDb selection is ambiguous, or user just wants raw control), surface a
|
|
32
|
+
"Manual rip" button alongside the existing "Rip selected" button.
|
|
33
|
+
|
|
34
|
+
Clicking it drops the user into a flat MakeMKV-style title list:
|
|
35
|
+
|
|
36
|
+
- All titles, sorted by index, with duration / size / chapter count
|
|
37
|
+
- Checkbox per title (default: all titles longer than 2 minutes)
|
|
38
|
+
- Output folder picker (defaults to `<output_root>/_MakeMKV/<disc_label>/`)
|
|
39
|
+
- "Rip" button → calls `makemkvcon mkv all` (or per-title) and shows the
|
|
40
|
+
existing progress UI
|
|
41
|
+
|
|
42
|
+
No metadata lookup, no organize, no Plex naming. The user lands in the "Done"
|
|
43
|
+
screen with a folder of raw MKVs, with a follow-up button: "Organize this
|
|
44
|
+
folder now → opens the manual organize screen pre-pointed at that path."
|
|
45
|
+
|
|
46
|
+
### 2. "Organize existing folder" entry on the welcome screen
|
|
47
|
+
|
|
48
|
+
A second card on welcome (next to "Rip a disc") titled "Organize existing
|
|
49
|
+
files." Picks up an arbitrary folder of MKVs and walks the user through:
|
|
50
|
+
|
|
51
|
+
1. **Folder picker** → list MKVs with sizes and ffprobe runtimes.
|
|
52
|
+
2. **TMDb search** (reuses existing `metadata` screen) → select the canonical
|
|
53
|
+
entry.
|
|
54
|
+
3. **File assignment** → for each MKV, drop-down: `Main feature`, `Part 1
|
|
55
|
+
(-pt1)`, `Part 2 (-pt2)`, `Extra → behindthescenes`, `Extra → featurette`,
|
|
56
|
+
`Extra → trailer`, `Skip`. Auto-suggest based on duration.
|
|
57
|
+
4. **Plan preview** → show source → destination paths.
|
|
58
|
+
5. **Apply** → move + rename into Plex layout, with the existing dry-run
|
|
59
|
+
default and `Execute` toggle.
|
|
60
|
+
|
|
61
|
+
This is essentially the existing `riplex organize` CLI behavior with a GUI in
|
|
62
|
+
front of it. No new business logic — just a thin wrapper that also handles
|
|
63
|
+
the "two parts of one TMDb movie" case via the assignment step.
|
|
64
|
+
|
|
65
|
+
## Why this works for the BBC Earth case
|
|
66
|
+
|
|
67
|
+
End-to-end flow stays inside riplex-ui:
|
|
68
|
+
|
|
69
|
+
1. Insert disc → riplex tries to classify, fails to find dvdcompare match.
|
|
70
|
+
2. User clicks **Manual rip**, picks the two main titles, hits Rip.
|
|
71
|
+
3. Done screen → click **Organize this folder**.
|
|
72
|
+
4. TMDb search → "Wild Africa Tiny Giants" → pick `tmdb-1657586`.
|
|
73
|
+
5. Assign t00 → pt1, t01 → pt2.
|
|
74
|
+
6. Apply → files land in `Movies\Wild Africa - Tiny Giants (2016)\`.
|
|
75
|
+
|
|
76
|
+
No external tool, no manual `Move-Item`.
|
|
77
|
+
|
|
78
|
+
## Tradeoffs
|
|
79
|
+
|
|
80
|
+
- **Pro**: covers any disc riplex can't classify, not just compilations.
|
|
81
|
+
- **Pro**: reuses existing rip + organize plumbing; mostly UI work.
|
|
82
|
+
- **Con**: more GUI surface (two screens, file-assignment widget).
|
|
83
|
+
- **Con**: risk of users defaulting to manual mode and bypassing the smart
|
|
84
|
+
classification we built. Mitigation: gate the manual-rip button behind a
|
|
85
|
+
"we couldn't classify this disc" trigger or an "Advanced" reveal, so it's
|
|
86
|
+
not a peer to the normal "Rip selected" button on every disc.
|
|
87
|
+
|
|
88
|
+
## Out of scope
|
|
89
|
+
|
|
90
|
+
- Per-title TMDb matching (full compilation support).
|
|
91
|
+
- Multi-target organize where one folder of MKVs maps to N different TMDb
|
|
92
|
+
entries. Manual mode handles this by running the organize wizard once per
|
|
93
|
+
target with file selection.
|
|
94
|
+
- Saving manual-mode decisions for repeat use (cache).
|
|
95
|
+
|
|
96
|
+
## Files likely involved
|
|
97
|
+
|
|
98
|
+
- `src/riplex_app/screens/selection.py` — add "Manual rip" button + branch.
|
|
99
|
+
- `src/riplex_app/screens/manual_rip.py` (new) — flat title list + ripper.
|
|
100
|
+
- `src/riplex_app/screens/welcome.py` — add "Organize existing files" card.
|
|
101
|
+
- `src/riplex_app/screens/manual_organize.py` (new) — folder picker → TMDb →
|
|
102
|
+
assignment → plan preview → apply.
|
|
103
|
+
- `src/riplex/organizer.py` — already has the path-building logic; may need
|
|
104
|
+
a small helper for the assignment data structure.
|
|
105
|
+
- `src/riplex_cli/commands/organize.py` — sanity-check the CLI still works
|
|
106
|
+
after any planner refactor.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -25,6 +25,7 @@ docs/getting-started/installation.md
|
|
|
25
25
|
docs/gui-guide/gui-walkthrough.md
|
|
26
26
|
docs/reference/cli.md
|
|
27
27
|
issues/debug-artifacts-consolidation.md
|
|
28
|
+
issues/manual-rip-and-organize-escape-hatch.md
|
|
28
29
|
issues/orchestrate-dvdcompare-fallback.md
|
|
29
30
|
issues/planned-features.md
|
|
30
31
|
screenshots/0_Rip_Flow_BTTF.gif
|
|
@@ -114,6 +115,7 @@ tests/test_matcher.py
|
|
|
114
115
|
tests/test_normalize.py
|
|
115
116
|
tests/test_organizer.py
|
|
116
117
|
tests/test_planner.py
|
|
118
|
+
tests/test_release_screen.py
|
|
117
119
|
tests/test_rip_guide.py
|
|
118
120
|
tests/test_scanner.py
|
|
119
121
|
tests/test_snapshot.py
|
|
@@ -121,6 +123,7 @@ tests/test_splitter.py
|
|
|
121
123
|
tests/test_tagger.py
|
|
122
124
|
tests/test_ui.py
|
|
123
125
|
tests/test_updater.py
|
|
126
|
+
tests/test_welcome_screen.py
|
|
124
127
|
tests/fixtures/chernobyl_disc1.json
|
|
125
128
|
tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt
|
|
126
129
|
tests/fixtures/makemkvcon_list.txt
|
|
@@ -87,7 +87,7 @@ class DoneScreen:
|
|
|
87
87
|
|
|
88
88
|
no_metadata_hint = ft.Text(
|
|
89
89
|
"Ripped without metadata — use 'Organize Rips' from the home screen "
|
|
90
|
-
"when TMDb is available to move these files into your
|
|
90
|
+
"when TMDb is available to move these files into your media library.",
|
|
91
91
|
size=12,
|
|
92
92
|
color=ft.Colors.ORANGE,
|
|
93
93
|
) if tmdb_match is None and len(successful) > 0 else ft.Container()
|
|
@@ -149,7 +149,7 @@ class MetadataScreen:
|
|
|
149
149
|
[
|
|
150
150
|
ft.Text("Metadata Lookup", size=24, weight=ft.FontWeight.BOLD),
|
|
151
151
|
ft.Text(
|
|
152
|
-
"TMDb is needed for canonical titles, years, and
|
|
152
|
+
"TMDb is needed for canonical titles, years, and media-library folder "
|
|
153
153
|
"structure. Without it, you can still rip the disc and organize later.",
|
|
154
154
|
size=13,
|
|
155
155
|
color=ft.Colors.GREY_500,
|
|
@@ -17,7 +17,7 @@ class OrchestrateDoneScreen:
|
|
|
17
17
|
"""Shown after all discs in the orchestrate queue have been ripped.
|
|
18
18
|
|
|
19
19
|
Displays a summary of all disc rips and offers to organize the
|
|
20
|
-
ripped files into Plex-compatible folder structure.
|
|
20
|
+
ripped files into a Plex-compatible folder structure.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
def __init__(self, app):
|
|
@@ -108,7 +108,7 @@ class OrchestrateDoneScreen:
|
|
|
108
108
|
on_click=self._open_folder,
|
|
109
109
|
)
|
|
110
110
|
organize_btn = ft.ElevatedButton(
|
|
111
|
-
"Organize into
|
|
111
|
+
"Organize into Library",
|
|
112
112
|
icon=ft.Icons.DRIVE_FILE_MOVE,
|
|
113
113
|
on_click=self._organize,
|
|
114
114
|
style=ft.ButtonStyle(
|
|
@@ -136,8 +136,8 @@ class OrchestrateDoneScreen:
|
|
|
136
136
|
ft.Column(disc_rows, spacing=6),
|
|
137
137
|
ft.Container(height=10),
|
|
138
138
|
ft.Text(
|
|
139
|
-
"All selected discs have been ripped. Click 'Organize into
|
|
140
|
-
"to match files to metadata and move them into your
|
|
139
|
+
"All selected discs have been ripped. Click 'Organize into Library' "
|
|
140
|
+
"to match files to metadata and move them into your media library.",
|
|
141
141
|
size=13,
|
|
142
142
|
color=ft.Colors.GREY_500,
|
|
143
143
|
),
|
|
@@ -202,7 +202,7 @@ class OrchestrateDoneScreen:
|
|
|
202
202
|
self._organize_status.value = f"Error: {exc}"
|
|
203
203
|
self._organize_status.color = ft.Colors.RED
|
|
204
204
|
e.control.disabled = False
|
|
205
|
-
e.control.text = "Organize into
|
|
205
|
+
e.control.text = "Organize into Library"
|
|
206
206
|
self.app.page.update()
|
|
207
207
|
|
|
208
208
|
threading.Thread(target=_do_organize, daemon=True).start()
|
|
@@ -31,6 +31,17 @@ class ReleaseScreen:
|
|
|
31
31
|
return "disc_overview"
|
|
32
32
|
return "selection"
|
|
33
33
|
|
|
34
|
+
def _skip_next_screen(self) -> str:
|
|
35
|
+
"""Screen to use when continuing without dvdcompare data."""
|
|
36
|
+
workflow = self.app.state.get("workflow")
|
|
37
|
+
if workflow == "orchestrate":
|
|
38
|
+
self.app.state["_orchestrate_disc_number"] = 1
|
|
39
|
+
self.app.state["disc_queue"] = [1]
|
|
40
|
+
self.app.state["current_disc_idx"] = 0
|
|
41
|
+
self.app.state["all_rip_results"] = {}
|
|
42
|
+
return "selection"
|
|
43
|
+
return self._next_screen
|
|
44
|
+
|
|
34
45
|
def _current_search_title(self) -> str:
|
|
35
46
|
"""Title to use for dvdcompare lookup (user override > TMDb > raw title)."""
|
|
36
47
|
override = self.app.state.get("dvdcompare_title_override")
|
|
@@ -499,7 +510,7 @@ class ReleaseScreen:
|
|
|
499
510
|
def _skip(self, e):
|
|
500
511
|
"""Proceed without dvdcompare data."""
|
|
501
512
|
self.app.state["dvdcompare_discs"] = []
|
|
502
|
-
self.app.navigate(self.
|
|
513
|
+
self.app.navigate(self._skip_next_screen())
|
|
503
514
|
|
|
504
515
|
def _use_release(self, release):
|
|
505
516
|
"""Convert a dvdcompare release to PlannedDiscs and navigate."""
|
|
@@ -16,13 +16,30 @@ from riplex.updater import check_for_update, get_current_version
|
|
|
16
16
|
log = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def has_complete_config(config: dict) -> bool:
|
|
20
|
+
"""Return True when required GUI setup values are present."""
|
|
21
|
+
return bool(
|
|
22
|
+
config
|
|
23
|
+
and config.get("tmdb_api_key")
|
|
24
|
+
and config.get("output_root")
|
|
25
|
+
and config.get("rip_output")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def should_show_setup(config: dict, state: dict) -> bool:
|
|
30
|
+
"""Return True when the welcome screen should show setup fields."""
|
|
31
|
+
return not has_complete_config(config) or bool(state.get("_show_setup"))
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
class WelcomeScreen:
|
|
20
35
|
def __init__(self, app):
|
|
21
36
|
self.app = app
|
|
22
37
|
|
|
23
38
|
def build(self) -> ft.Control:
|
|
24
39
|
config = load_config()
|
|
25
|
-
|
|
40
|
+
has_api_key = bool(config and config.get("tmdb_api_key"))
|
|
41
|
+
has_config = has_complete_config(config)
|
|
42
|
+
show_setup = should_show_setup(config, self.app.state)
|
|
26
43
|
has_makemkv = find_makemkvcon() is not None
|
|
27
44
|
has_ffprobe = find_ffprobe() is not None
|
|
28
45
|
from riplex.splitter import find_mkvmerge
|
|
@@ -31,7 +48,7 @@ class WelcomeScreen:
|
|
|
31
48
|
# Status indicators
|
|
32
49
|
checks = [
|
|
33
50
|
("Config file", has_config),
|
|
34
|
-
("TMDb API key",
|
|
51
|
+
("TMDb API key", has_api_key),
|
|
35
52
|
("makemkvcon", has_makemkv),
|
|
36
53
|
("ffprobe", has_ffprobe),
|
|
37
54
|
("mkvmerge", has_mkvmerge),
|
|
@@ -102,7 +119,7 @@ class WelcomeScreen:
|
|
|
102
119
|
can_rip = all(ok for _, ok in checks)
|
|
103
120
|
can_organize = has_config and has_ffprobe
|
|
104
121
|
|
|
105
|
-
# Setup fields (shown if config missing)
|
|
122
|
+
# Setup fields (shown if config missing or user chooses to edit)
|
|
106
123
|
self.api_key_field = ft.TextField(
|
|
107
124
|
label="TMDb API key",
|
|
108
125
|
value=config.get("tmdb_api_key", ""),
|
|
@@ -111,7 +128,7 @@ class WelcomeScreen:
|
|
|
111
128
|
expand=True,
|
|
112
129
|
)
|
|
113
130
|
self.output_root_field = ft.TextField(
|
|
114
|
-
label="
|
|
131
|
+
label="Media library root",
|
|
115
132
|
value=config.get("output_root", ""),
|
|
116
133
|
expand=True,
|
|
117
134
|
)
|
|
@@ -155,8 +172,9 @@ class WelcomeScreen:
|
|
|
155
172
|
ft.Container(height=8),
|
|
156
173
|
_make_browse_row(self.output_root_field),
|
|
157
174
|
ft.Text(
|
|
158
|
-
"Your
|
|
159
|
-
"into Movies/ and TV Shows/ subfolders here
|
|
175
|
+
"Your media library root. Organized files will be placed "
|
|
176
|
+
"into Movies/ and TV Shows/ subfolders here using "
|
|
177
|
+
"Plex-compatible naming.",
|
|
160
178
|
size=11,
|
|
161
179
|
color=ft.Colors.GREY_600,
|
|
162
180
|
),
|
|
@@ -177,10 +195,20 @@ class WelcomeScreen:
|
|
|
177
195
|
color=ft.Colors.GREY_600,
|
|
178
196
|
),
|
|
179
197
|
ft.Container(height=8),
|
|
180
|
-
ft.
|
|
198
|
+
ft.Row(
|
|
199
|
+
[
|
|
200
|
+
ft.ElevatedButton("Save Config", on_click=self._save_config),
|
|
201
|
+
ft.TextButton(
|
|
202
|
+
"Cancel",
|
|
203
|
+
on_click=self._hide_setup,
|
|
204
|
+
visible=has_config,
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
spacing=8,
|
|
208
|
+
),
|
|
181
209
|
],
|
|
182
210
|
spacing=4,
|
|
183
|
-
visible=
|
|
211
|
+
visible=show_setup,
|
|
184
212
|
)
|
|
185
213
|
|
|
186
214
|
# Workflow buttons
|
|
@@ -190,7 +218,7 @@ class WelcomeScreen:
|
|
|
190
218
|
on_click=self._start_rip,
|
|
191
219
|
disabled=not can_rip,
|
|
192
220
|
style=ft.ButtonStyle(padding=ft.Padding(left=30, top=15, right=30, bottom=15)),
|
|
193
|
-
tooltip="Detect a disc, look up metadata, rip selected titles, and organize into
|
|
221
|
+
tooltip="Detect a disc, look up metadata, rip selected titles, and organize into your media library.",
|
|
194
222
|
)
|
|
195
223
|
organize_button = ft.ElevatedButton(
|
|
196
224
|
"Organize Rips",
|
|
@@ -228,7 +256,7 @@ class WelcomeScreen:
|
|
|
228
256
|
),
|
|
229
257
|
self.update_banner,
|
|
230
258
|
ft.Text(
|
|
231
|
-
"Rip physical discs and organize into Plex-compatible
|
|
259
|
+
"Rip physical discs and organize them into a media library with Plex-compatible naming.",
|
|
232
260
|
size=14,
|
|
233
261
|
color=ft.Colors.GREY_400,
|
|
234
262
|
),
|
|
@@ -244,6 +272,16 @@ class WelcomeScreen:
|
|
|
244
272
|
ft.Column(status_rows, spacing=4),
|
|
245
273
|
install_section,
|
|
246
274
|
ft.Container(height=10),
|
|
275
|
+
ft.Row(
|
|
276
|
+
[
|
|
277
|
+
ft.OutlinedButton(
|
|
278
|
+
"Edit Settings",
|
|
279
|
+
icon=ft.Icons.SETTINGS,
|
|
280
|
+
on_click=self._show_setup,
|
|
281
|
+
visible=has_config and not show_setup,
|
|
282
|
+
),
|
|
283
|
+
],
|
|
284
|
+
),
|
|
247
285
|
setup_section,
|
|
248
286
|
ft.Container(expand=True),
|
|
249
287
|
ft.Text("What would you like to do?", size=16, weight=ft.FontWeight.BOLD),
|
|
@@ -645,6 +683,17 @@ class WelcomeScreen:
|
|
|
645
683
|
)
|
|
646
684
|
|
|
647
685
|
# Refresh the screen
|
|
686
|
+
self.app.state.pop("_show_setup", None)
|
|
687
|
+
self.app.navigate("welcome")
|
|
688
|
+
|
|
689
|
+
def _show_setup(self, e):
|
|
690
|
+
"""Reveal config fields for editing after first-run setup."""
|
|
691
|
+
self.app.state["_show_setup"] = True
|
|
692
|
+
self.app.navigate("welcome")
|
|
693
|
+
|
|
694
|
+
def _hide_setup(self, e):
|
|
695
|
+
"""Hide config fields without saving changes."""
|
|
696
|
+
self.app.state.pop("_show_setup", None)
|
|
648
697
|
self.app.navigate("welcome")
|
|
649
698
|
|
|
650
699
|
def _start_rip(self, e):
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from riplex_app.screens.release import ReleaseScreen
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class _App:
|
|
5
|
+
def __init__(self, state):
|
|
6
|
+
self.state = state
|
|
7
|
+
self.navigated_to = None
|
|
8
|
+
|
|
9
|
+
def navigate(self, screen):
|
|
10
|
+
self.navigated_to = screen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestReleaseSkipRouting:
|
|
14
|
+
def test_rip_workflow_continues_to_selection(self):
|
|
15
|
+
app = _App({"workflow": "rip"})
|
|
16
|
+
screen = ReleaseScreen(app)
|
|
17
|
+
|
|
18
|
+
screen._skip(None)
|
|
19
|
+
|
|
20
|
+
assert app.state["dvdcompare_discs"] == []
|
|
21
|
+
assert app.navigated_to == "selection"
|
|
22
|
+
|
|
23
|
+
def test_orchestrate_with_release_uses_disc_overview(self):
|
|
24
|
+
app = _App({"workflow": "orchestrate"})
|
|
25
|
+
screen = ReleaseScreen(app)
|
|
26
|
+
|
|
27
|
+
assert screen._next_screen == "disc_overview"
|
|
28
|
+
|
|
29
|
+
def test_orchestrate_without_dvdcompare_starts_single_disc_selection(self):
|
|
30
|
+
app = _App({"workflow": "orchestrate"})
|
|
31
|
+
screen = ReleaseScreen(app)
|
|
32
|
+
|
|
33
|
+
screen._skip(None)
|
|
34
|
+
|
|
35
|
+
assert app.state["dvdcompare_discs"] == []
|
|
36
|
+
assert app.state["_orchestrate_disc_number"] == 1
|
|
37
|
+
assert app.state["disc_queue"] == [1]
|
|
38
|
+
assert app.state["current_disc_idx"] == 0
|
|
39
|
+
assert app.state["all_rip_results"] == {}
|
|
40
|
+
assert app.navigated_to == "selection"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from riplex_app.screens.welcome import has_complete_config, should_show_setup
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestHasCompleteConfig:
|
|
5
|
+
def test_requires_api_key_output_root_and_rip_output(self):
|
|
6
|
+
assert has_complete_config({}) is False
|
|
7
|
+
assert has_complete_config({"tmdb_api_key": "key"}) is False
|
|
8
|
+
assert has_complete_config({"tmdb_api_key": "key", "output_root": "E:/Media"}) is False
|
|
9
|
+
assert has_complete_config({
|
|
10
|
+
"tmdb_api_key": "key",
|
|
11
|
+
"output_root": "E:/Media",
|
|
12
|
+
"rip_output": "E:/Media/_MakeMKV",
|
|
13
|
+
}) is True
|
|
14
|
+
|
|
15
|
+
def test_archive_root_is_optional(self):
|
|
16
|
+
assert has_complete_config({
|
|
17
|
+
"tmdb_api_key": "key",
|
|
18
|
+
"output_root": "E:/Media",
|
|
19
|
+
"rip_output": "E:/Media/_MakeMKV",
|
|
20
|
+
"archive_root": "",
|
|
21
|
+
}) is True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestShouldShowSetup:
|
|
25
|
+
def test_shows_setup_for_incomplete_config(self):
|
|
26
|
+
assert should_show_setup({"tmdb_api_key": "key"}, {}) is True
|
|
27
|
+
|
|
28
|
+
def test_hides_setup_for_complete_config(self):
|
|
29
|
+
config = {
|
|
30
|
+
"tmdb_api_key": "key",
|
|
31
|
+
"output_root": "E:/Media",
|
|
32
|
+
"rip_output": "E:/Media/_MakeMKV",
|
|
33
|
+
}
|
|
34
|
+
assert should_show_setup(config, {}) is False
|
|
35
|
+
|
|
36
|
+
def test_can_force_show_setup_for_editing(self):
|
|
37
|
+
config = {
|
|
38
|
+
"tmdb_api_key": "key",
|
|
39
|
+
"output_root": "E:/Media",
|
|
40
|
+
"rip_output": "E:/Media/_MakeMKV",
|
|
41
|
+
}
|
|
42
|
+
assert should_show_setup(config, {"_show_setup": True}) is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|