riplex 0.3.2__tar.gz → 0.3.4__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.
Files changed (116) hide show
  1. riplex-0.3.4/LICENSE +21 -0
  2. {riplex-0.3.2/src/riplex.egg-info → riplex-0.3.4}/PKG-INFO +3 -1
  3. {riplex-0.3.2 → riplex-0.3.4}/docs/getting-started/installation.md +8 -1
  4. riplex-0.3.4/issues/orchestrate-dvdcompare-fallback.md +45 -0
  5. riplex-0.3.4/issues/planned-features.md +77 -0
  6. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/config.py +7 -1
  7. {riplex-0.3.2 → riplex-0.3.4/src/riplex.egg-info}/PKG-INFO +3 -1
  8. {riplex-0.3.2 → riplex-0.3.4}/src/riplex.egg-info/SOURCES.txt +3 -3
  9. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/welcome.py +24 -27
  10. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/setup.py +10 -5
  11. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/main.py +8 -2
  12. {riplex-0.3.2 → riplex-0.3.4}/tests/test_updater.py +6 -6
  13. riplex-0.3.2/MONOREPO_PLAN.md +0 -115
  14. riplex-0.3.2/PLANNED_FEATURES.md +0 -416
  15. riplex-0.3.2/plex_naming_rules.md +0 -154
  16. {riplex-0.3.2 → riplex-0.3.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  17. {riplex-0.3.2 → riplex-0.3.4}/.github/agents/riplex.agent.md +0 -0
  18. {riplex-0.3.2 → riplex-0.3.4}/.github/copilot-instructions.md +0 -0
  19. {riplex-0.3.2 → riplex-0.3.4}/.github/workflows/publish.yml +0 -0
  20. {riplex-0.3.2 → riplex-0.3.4}/.github/workflows/release.yml +0 -0
  21. {riplex-0.3.2 → riplex-0.3.4}/.gitignore +0 -0
  22. {riplex-0.3.2 → riplex-0.3.4}/README.md +0 -0
  23. {riplex-0.3.2 → riplex-0.3.4}/REFACTOR_PLAN.md +0 -0
  24. {riplex-0.3.2 → riplex-0.3.4}/docs/architecture.md +0 -0
  25. {riplex-0.3.2 → riplex-0.3.4}/docs/changelog.md +0 -0
  26. {riplex-0.3.2 → riplex-0.3.4}/docs/getting-started/configuration.md +0 -0
  27. {riplex-0.3.2 → riplex-0.3.4}/docs/guide/lookup.md +0 -0
  28. {riplex-0.3.2 → riplex-0.3.4}/docs/guide/orchestrate.md +0 -0
  29. {riplex-0.3.2 → riplex-0.3.4}/docs/guide/organize.md +0 -0
  30. {riplex-0.3.2 → riplex-0.3.4}/docs/guide/workflow.md +0 -0
  31. {riplex-0.3.2 → riplex-0.3.4}/docs/index.md +0 -0
  32. {riplex-0.3.2 → riplex-0.3.4}/docs/naming-rules.md +0 -0
  33. {riplex-0.3.2 → riplex-0.3.4}/docs/reference/cli.md +0 -0
  34. {riplex-0.3.2 → riplex-0.3.4}/issues/cross-disc-dvdcompare-matching.md +0 -0
  35. {riplex-0.3.2 → riplex-0.3.4}/mkdocs.yml +0 -0
  36. {riplex-0.3.2 → riplex-0.3.4}/pyproject.toml +0 -0
  37. {riplex-0.3.2 → riplex-0.3.4}/setup.cfg +0 -0
  38. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/__init__.py +0 -0
  39. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/cache.py +0 -0
  40. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/dedup.py +0 -0
  41. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/detect.py +0 -0
  42. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/disc/__init__.py +0 -0
  43. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/disc/analysis.py +0 -0
  44. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/disc/makemkv.py +0 -0
  45. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/disc/provider.py +0 -0
  46. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/formatter.py +0 -0
  47. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/lookup.py +0 -0
  48. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/manifest.py +0 -0
  49. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/matcher.py +0 -0
  50. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/metadata/__init__.py +0 -0
  51. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/metadata/planner.py +0 -0
  52. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/metadata/provider.py +0 -0
  53. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/metadata/sources/__init__.py +0 -0
  54. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/metadata/sources/tmdb.py +0 -0
  55. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/models.py +0 -0
  56. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/normalize.py +0 -0
  57. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/organizer.py +0 -0
  58. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/scanner.py +0 -0
  59. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/snapshot.py +0 -0
  60. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/splitter.py +0 -0
  61. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/tagger.py +0 -0
  62. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/title.py +0 -0
  63. {riplex-0.3.2 → riplex-0.3.4}/src/riplex/ui.py +0 -0
  64. {riplex-0.3.2 → riplex-0.3.4}/src/riplex.egg-info/dependency_links.txt +0 -0
  65. {riplex-0.3.2 → riplex-0.3.4}/src/riplex.egg-info/entry_points.txt +0 -0
  66. {riplex-0.3.2 → riplex-0.3.4}/src/riplex.egg-info/requires.txt +0 -0
  67. {riplex-0.3.2 → riplex-0.3.4}/src/riplex.egg-info/top_level.txt +0 -0
  68. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/__init__.py +0 -0
  69. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/main.py +0 -0
  70. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/__init__.py +0 -0
  71. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/disc_detection.py +0 -0
  72. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/done.py +0 -0
  73. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/folder_picker.py +0 -0
  74. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/metadata.py +0 -0
  75. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/organize_done.py +0 -0
  76. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/organize_preview.py +0 -0
  77. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/progress.py +0 -0
  78. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/release.py +0 -0
  79. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/screens/selection.py +0 -0
  80. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_app/updater.py +0 -0
  81. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/__init__.py +0 -0
  82. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/__init__.py +0 -0
  83. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/lookup.py +0 -0
  84. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/orchestrate.py +0 -0
  85. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/organize.py +0 -0
  86. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/commands/rip.py +0 -0
  87. {riplex-0.3.2 → riplex-0.3.4}/src/riplex_cli/formatting.py +0 -0
  88. {riplex-0.3.2 → riplex-0.3.4}/tests/__init__.py +0 -0
  89. {riplex-0.3.2 → riplex-0.3.4}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  90. {riplex-0.3.2 → riplex-0.3.4}/tests/fixtures/makemkvcon_list.txt +0 -0
  91. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  92. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  93. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  94. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  95. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  96. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  97. {riplex-0.3.2 → riplex-0.3.4}/tests/snapshots/Waterworld.snapshot.json +0 -0
  98. {riplex-0.3.2 → riplex-0.3.4}/tests/test_cache.py +0 -0
  99. {riplex-0.3.2 → riplex-0.3.4}/tests/test_cli_utils.py +0 -0
  100. {riplex-0.3.2 → riplex-0.3.4}/tests/test_config.py +0 -0
  101. {riplex-0.3.2 → riplex-0.3.4}/tests/test_dedup.py +0 -0
  102. {riplex-0.3.2 → riplex-0.3.4}/tests/test_detect.py +0 -0
  103. {riplex-0.3.2 → riplex-0.3.4}/tests/test_disc_analysis.py +0 -0
  104. {riplex-0.3.2 → riplex-0.3.4}/tests/test_disc_provider.py +0 -0
  105. {riplex-0.3.2 → riplex-0.3.4}/tests/test_formatter.py +0 -0
  106. {riplex-0.3.2 → riplex-0.3.4}/tests/test_makemkv.py +0 -0
  107. {riplex-0.3.2 → riplex-0.3.4}/tests/test_matcher.py +0 -0
  108. {riplex-0.3.2 → riplex-0.3.4}/tests/test_normalize.py +0 -0
  109. {riplex-0.3.2 → riplex-0.3.4}/tests/test_organizer.py +0 -0
  110. {riplex-0.3.2 → riplex-0.3.4}/tests/test_planner.py +0 -0
  111. {riplex-0.3.2 → riplex-0.3.4}/tests/test_rip_guide.py +0 -0
  112. {riplex-0.3.2 → riplex-0.3.4}/tests/test_scanner.py +0 -0
  113. {riplex-0.3.2 → riplex-0.3.4}/tests/test_snapshot.py +0 -0
  114. {riplex-0.3.2 → riplex-0.3.4}/tests/test_splitter.py +0 -0
  115. {riplex-0.3.2 → riplex-0.3.4}/tests/test_tagger.py +0 -0
  116. {riplex-0.3.2 → riplex-0.3.4}/tests/test_ui.py +0 -0
riplex-0.3.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AnyCredit5518
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
+ License-File: LICENSE
8
9
  Requires-Dist: httpx>=0.27
9
10
  Requires-Dist: dvdcompare-scraper>=0.1.6
10
11
  Requires-Dist: platformdirs>=4.0
@@ -14,6 +15,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
15
  Requires-Dist: respx>=0.21; extra == "dev"
15
16
  Provides-Extra: gui
16
17
  Requires-Dist: flet>=0.84; extra == "gui"
18
+ Dynamic: license-file
17
19
 
18
20
  # riplex
19
21
 
@@ -75,6 +75,13 @@ riplex setup
75
75
  The setup wizard will:
76
76
 
77
77
  1. Ask for your TMDb API key (free at https://www.themoviedb.org/settings/api)
78
+
79
+ > [!TIP]
80
+ > TMDb asks for an app name and URL when you request a key. You can just
81
+ > enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex`
82
+ > as the URL. The rest of the form can be filled with basic info - it doesn't
83
+ > need to be a real business. The key is approved instantly.
84
+
78
85
  2. Ask where your Plex library and MakeMKV rip folders are
79
86
  3. Check for required tools (MakeMKV, ffprobe, mkvmerge, mkvpropedit)
80
87
  4. Offer to install any missing tools for you (via winget on Windows, Homebrew on macOS, or apt on Linux)
@@ -132,6 +139,6 @@ MakeMKV requires a registration key. A free beta key is available at https://for
132
139
 
133
140
  ### MKVToolNix (mkvmerge, mkvpropedit)
134
141
 
135
- - **Windows**: `winget install MKVToolNix.MKVToolNix` (or download from https://mkvtoolnix.download/)
142
+ - **Windows**: `winget install MoritzBunkus.MKVToolNix` (or download from https://mkvtoolnix.download/)
136
143
  - **macOS**: `brew install mkvtoolnix`
137
144
  - **Linux**: `sudo apt install mkvtoolnix`
@@ -0,0 +1,45 @@
1
+ # Orchestrate: dvdcompare fallback for missing discs
2
+
3
+ ## Problem
4
+
5
+ When `riplex orchestrate` can't find a disc on dvdcompare, it exits with a
6
+ fatal error instead of falling back to duration-based title selection. The
7
+ `rip` command handles this correctly (warns and continues), but `orchestrate`
8
+ does `return 1`.
9
+
10
+ ## Affected users
11
+
12
+ Anyone using `orchestrate` with a disc that isn't listed on dvdcompare (niche
13
+ releases, old DVDs, foreign titles, etc).
14
+
15
+ ## Workaround
16
+
17
+ Use `riplex rip` instead of `orchestrate` for single-disc titles not on
18
+ dvdcompare. It warns about the missing dvdcompare data and falls back to
19
+ selecting the main feature by TMDb runtime matching.
20
+
21
+ ## Proposed fix
22
+
23
+ Change `orchestrate.py` to warn instead of error when dvdcompare fails, then:
24
+
25
+ 1. Skip the multi-disc loop entirely (no disc data to iterate)
26
+ 2. Fall through to a single-disc rip using the same heuristics as `rip.py`:
27
+ - `build_dvd_entries([])` returns empty entries
28
+ - `select_rippable_titles()` uses TMDb runtime to pick the main feature
29
+ 3. After ripping, proceed to the organize step normally
30
+
31
+ ### Complications
32
+
33
+ The downstream code in orchestrate's disc loop (`disc_order`, disc swap
34
+ prompts, `detect_disc_number`, `_print_disc_overview`) all assume `discs` is
35
+ populated. A full fix needs to either:
36
+
37
+ - Create a synthetic single-disc `PlannedDisc` entry when dvdcompare fails
38
+ - Or branch early into a simplified single-disc rip path (essentially
39
+ delegating to `rip`-like behavior) before hitting the disc loop
40
+
41
+ ### Files involved
42
+
43
+ - `src/riplex_cli/commands/orchestrate.py` (lines ~249-258): the error/return
44
+ - Downstream: disc loop starting at ~line 285, `_print_disc_overview`,
45
+ `detect_disc_number`, disc swap prompts
@@ -0,0 +1,77 @@
1
+ # Planned Features
2
+
3
+
4
+ ## Multi-Resolution Support (4K + Standard Blu-ray)
5
+
6
+ Many 4K boxsets include standard Blu-ray discs with the same content at 1080p.
7
+ Some users may want to rip both so Plex can serve the lower-resolution version
8
+ to mobile devices without transcoding.
9
+
10
+ **Movies**: Supported via Plex "Multi-Version Movies" — multiple files with
11
+ different resolution suffixes in the same folder collapse into one item.
12
+
13
+ **TV Shows**: NOT officially supported by Plex. No documented multi-version
14
+ episode collapsing.
15
+
16
+ ### Plan
17
+
18
+ - Support ripping both 4K and standard Blu-ray discs for movies, naming with
19
+ resolution suffixes during organize.
20
+ - For TV shows, warn users and offer to skip (explain limitation), allow
21
+ override for users with separate libraries.
22
+
23
+
24
+ ## Orchestrate Flow (GUI)
25
+
26
+ The GUI has rip and organize flows. The orchestrate flow (multi-disc pipeline
27
+ with disc-swap prompts) is not yet implemented.
28
+
29
+ ### Key pieces needed
30
+
31
+ - Disc swap prompt screen
32
+ - Session state tracking across disc swaps (accumulated rip results, metadata)
33
+ - Disc number auto-detection after each swap
34
+ - Error recovery (retry/skip failed disc)
35
+ - Cancel mid-flow and organize what's been ripped so far
36
+
37
+
38
+ ## Bug Report Submission
39
+
40
+ ### Problem
41
+
42
+ Snapshot files for debugging are mixed in with media files and have
43
+ inconsistent formats. Users must hunt for debug artifacts across multiple
44
+ locations when filing bug reports.
45
+
46
+ ### Plan
47
+
48
+ 1. Move all debug artifacts into a `_riplex/` subfolder (Plex-ignored).
49
+ 2. Consistent v2 snapshot envelope format with type discriminator.
50
+ 3. GUI writes same snapshots as CLI.
51
+ 4. "Report a Bug" button in GUI: opens pre-filled GitHub issue, copies
52
+ debug folder path to clipboard.
53
+ 5. `.github/ISSUE_TEMPLATE/bug_report.yml` for structured reports.
54
+
55
+
56
+ ## Interactive Lookup Command
57
+
58
+ `riplex lookup` currently auto-picks the first TMDb match and default
59
+ dvdcompare release without confirmation. Add interactive selection (reuse
60
+ existing `_pick_best` prompt from planner). Keep `--auto` flag for scripting.
61
+
62
+
63
+ ## Multi-Language Track Selection
64
+
65
+ Users in multilingual households want to keep multiple audio and subtitle
66
+ tracks when ripping, not just the default/English track.
67
+
68
+ ### Plan
69
+
70
+ - Add config options for preferred audio and subtitle languages (e.g.
71
+ `audio_languages = ["en", "es"]`, `subtitle_languages = ["en", "es", "fr"]`)
72
+ - During rip, pass language preferences to MakeMKV/mkvmerge so all selected
73
+ tracks are retained
74
+ - During organize, preserve all selected tracks when remuxing
75
+ - GUI: add language selection to setup/config screen
76
+ - Default behavior: keep all tracks (current MakeMKV default) — only filter
77
+ if the user explicitly configures preferred languages
@@ -33,7 +33,13 @@ def load_config() -> dict[str, Any]:
33
33
  for path in _candidate_paths():
34
34
  if path.is_file():
35
35
  with open(path, "rb") as f:
36
- return tomllib.load(f)
36
+ try:
37
+ return tomllib.load(f)
38
+ except tomllib.TOMLDecodeError as exc:
39
+ raise SystemExit(
40
+ f"Error: invalid config file {path}\n {exc}\n"
41
+ f"Run 'riplex setup --force' to delete it and start fresh."
42
+ ) from None
37
43
  return {}
38
44
 
39
45
 
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
+ License-File: LICENSE
8
9
  Requires-Dist: httpx>=0.27
9
10
  Requires-Dist: dvdcompare-scraper>=0.1.6
10
11
  Requires-Dist: platformdirs>=4.0
@@ -14,6 +15,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
15
  Requires-Dist: respx>=0.21; extra == "dev"
15
16
  Provides-Extra: gui
16
17
  Requires-Dist: flet>=0.84; extra == "gui"
18
+ Dynamic: license-file
17
19
 
18
20
  # riplex
19
21
 
@@ -1,10 +1,8 @@
1
1
  .gitignore
2
- MONOREPO_PLAN.md
3
- PLANNED_FEATURES.md
2
+ LICENSE
4
3
  README.md
5
4
  REFACTOR_PLAN.md
6
5
  mkdocs.yml
7
- plex_naming_rules.md
8
6
  pyproject.toml
9
7
  .github/copilot-instructions.md
10
8
  .github/ISSUE_TEMPLATE/bug_report.yml
@@ -23,6 +21,8 @@ docs/guide/organize.md
23
21
  docs/guide/workflow.md
24
22
  docs/reference/cli.md
25
23
  issues/cross-disc-dvdcompare-matching.md
24
+ issues/orchestrate-dvdcompare-fallback.md
25
+ issues/planned-features.md
26
26
  src/riplex/__init__.py
27
27
  src/riplex/cache.py
28
28
  src/riplex/config.py
@@ -38,6 +38,7 @@ class WelcomeScreen:
38
38
  missing_tools.append("ffprobe")
39
39
  if not has_mkvmerge:
40
40
  missing_tools.append("mkvmerge")
41
+ self._missing_tools = missing_tools
41
42
 
42
43
  status_rows = []
43
44
  for label, ok in checks:
@@ -61,7 +62,7 @@ class WelcomeScreen:
61
62
  ft.ElevatedButton(
62
63
  "Install Missing Tools",
63
64
  icon=ft.Icons.DOWNLOAD,
64
- on_click=lambda _: self._install_tools(missing_tools),
65
+ on_click=self._on_install_click,
65
66
  ),
66
67
  ft.TextButton(
67
68
  "MakeMKV ↗",
@@ -286,15 +287,20 @@ class WelcomeScreen:
286
287
  url = get_download_url(self._update_info)
287
288
  webbrowser.open(url)
288
289
 
290
+ def _on_install_click(self, e):
291
+ """Handle Install Missing Tools button click."""
292
+ self._install_status.value = "Starting installation..."
293
+ self.app.page.update()
294
+ self._install_tools(self._missing_tools)
295
+
289
296
  def _install_tools(self, missing: list[str]):
290
297
  """Install missing tools via system package manager in background."""
291
298
  import platform
292
299
  import subprocess
293
- import sys
294
300
 
295
301
  system = platform.system()
296
302
  packages = {
297
- "Windows": {"makemkvcon": "MakeMKV.MakeMKV", "ffprobe": "Gyan.FFmpeg", "mkvmerge": "MKVToolNix.MKVToolNix"},
303
+ "Windows": {"makemkvcon": "GuinpinSoft.MakeMKV", "ffprobe": "Gyan.FFmpeg", "mkvmerge": "MoritzBunkus.MKVToolNix"},
298
304
  "Darwin": {"makemkvcon": "makemkv", "ffprobe": "ffmpeg", "mkvmerge": "mkvtoolnix"},
299
305
  }
300
306
 
@@ -306,34 +312,28 @@ class WelcomeScreen:
306
312
  return
307
313
 
308
314
  def _do_install():
309
- async def _update_status(msg):
310
- self._install_status.value = msg
311
- self.app.page.update()
312
-
313
315
  try:
314
316
  if system == "Windows":
315
- # Build a single winget command string to run elevated
316
317
  cmds = " && ".join(
317
318
  f'winget install --accept-source-agreements --accept-package-agreements {pkg}'
318
319
  for pkg in to_install
319
320
  )
320
- self.app.page.run_task(
321
- lambda: _update_status(f"Installing {', '.join(to_install)}... (accept the admin prompt)")
322
- )
323
- # Run elevated via powershell Start-Process
321
+ self._install_status.value = f"Installing {', '.join(to_install)}... (accept the admin prompt)"
322
+ self.app.page.update()
324
323
  result = subprocess.run(
325
324
  ["powershell", "-Command",
326
- f'Start-Process cmd -ArgumentList \'/c {cmds} & pause\' -Verb RunAs -Wait'],
327
- creationflags=subprocess.CREATE_NO_WINDOW,
325
+ f"Start-Process cmd -ArgumentList '/c {cmds} & pause' -Verb RunAs -Wait"],
326
+ capture_output=True,
327
+ text=True,
328
328
  )
329
329
  if result.returncode != 0:
330
- raise subprocess.CalledProcessError(result.returncode, "winget")
330
+ raise subprocess.CalledProcessError(result.returncode, "winget", output=result.stdout, stderr=result.stderr)
331
331
  elif system == "Darwin":
332
332
  if shutil.which("brew"):
333
- self.app.page.run_task(lambda: _update_status("Installing via brew..."))
333
+ self._install_status.value = "Installing via brew..."
334
+ self.app.page.update()
334
335
  subprocess.run(["brew", "install"] + to_install, check=True)
335
336
  else:
336
- # No Homebrew — open download pages in browser
337
337
  download_urls = {
338
338
  "makemkv": "https://www.makemkv.com/download/",
339
339
  "ffmpeg": "https://ffmpeg.org/download.html",
@@ -342,18 +342,15 @@ class WelcomeScreen:
342
342
  for pkg in to_install:
343
343
  if pkg in download_urls:
344
344
  webbrowser.open(download_urls[pkg])
345
- self.app.page.run_task(
346
- lambda: _update_status("Opened download pages in your browser. Install the tools, then restart the app.")
347
- )
345
+ self._install_status.value = "Opened download pages in your browser. Install the tools, then restart the app."
346
+ self.app.page.update()
348
347
  return
349
348
 
350
- self.app.page.run_task(
351
- lambda: _update_status("Done! Restart the app for changes to take effect.")
352
- )
353
- except (subprocess.CalledProcessError, FileNotFoundError, OSError) as exc:
354
- self.app.page.run_task(
355
- lambda: _update_status(f"Install failed: {exc}. Try the manual links above.")
356
- )
349
+ self._install_status.value = "Done! Restart the app for changes to take effect."
350
+ self.app.page.update()
351
+ except Exception as exc:
352
+ self._install_status.value = f"Install failed: {exc}. Try the manual links above."
353
+ self.app.page.update()
357
354
 
358
355
  threading.Thread(target=_do_install, daemon=True).start()
359
356
 
@@ -12,7 +12,7 @@ def _offer_install(missing: list[str]) -> None:
12
12
 
13
13
  system = platform.system()
14
14
  packages: dict[str, dict[str, str]] = {
15
- "Windows": {"makemkvcon": "MakeMKV.MakeMKV", "ffprobe": "Gyan.FFmpeg", "mkvmerge": "MKVToolNix.MKVToolNix", "mkvpropedit": "MKVToolNix.MKVToolNix"},
15
+ "Windows": {"makemkvcon": "GuinpinSoft.MakeMKV", "ffprobe": "Gyan.FFmpeg", "mkvmerge": "MoritzBunkus.MKVToolNix", "mkvpropedit": "MoritzBunkus.MKVToolNix"},
16
16
  "Darwin": {"makemkvcon": "makemkv", "ffprobe": "ffmpeg", "mkvmerge": "mkvtoolnix", "mkvpropedit": "mkvtoolnix"},
17
17
  "Linux": {"makemkvcon": "", "ffprobe": "ffmpeg", "mkvmerge": "mkvtoolnix", "mkvpropedit": "mkvtoolnix"},
18
18
  }
@@ -53,7 +53,7 @@ def _offer_install(missing: list[str]) -> None:
53
53
  print("\n Installation complete. You may need to restart your terminal for PATH changes to take effect.")
54
54
 
55
55
 
56
- def run_setup() -> int:
56
+ def run_setup(force: bool = False) -> int:
57
57
  """Interactive setup wizard to create or update the riplex config file."""
58
58
  import shutil
59
59
 
@@ -62,9 +62,14 @@ def run_setup() -> int:
62
62
  print("riplex setup")
63
63
  print(f"Config file: {config_path}\n")
64
64
 
65
- existing: dict[str, str] = load_config()
66
- if existing:
67
- print("(Existing config found. Press Enter to keep current values.)\n")
65
+ if force and config_path.is_file():
66
+ config_path.unlink()
67
+ print("(Existing config deleted. Starting fresh.)\n")
68
+ existing: dict[str, str] = {}
69
+ else:
70
+ existing = load_config()
71
+ if existing:
72
+ print("(Existing config found. Press Enter to keep current values.)\n")
68
73
 
69
74
  def prompt(key: str, label: str, hint: str = "", mask: bool = False) -> str:
70
75
  current = existing.get(key, "")
@@ -325,10 +325,16 @@ def _build_parser() -> argparse.ArgumentParser:
325
325
  )
326
326
 
327
327
  # --- setup ---
328
- subs.add_parser(
328
+ setup_parser = subs.add_parser(
329
329
  "setup",
330
330
  help="Interactive setup wizard to create or update the config file.",
331
331
  )
332
+ setup_parser.add_argument(
333
+ "--force",
334
+ action="store_true",
335
+ default=False,
336
+ help="Delete existing config and start fresh.",
337
+ )
332
338
 
333
339
  return parser
334
340
 
@@ -343,7 +349,7 @@ async def _run(args: argparse.Namespace) -> int:
343
349
  if args.command == "orchestrate":
344
350
  return await run_orchestrate(args)
345
351
  if args.command == "setup":
346
- return run_setup()
352
+ return run_setup(force=getattr(args, "force", False))
347
353
  return 1
348
354
 
349
355
 
@@ -142,13 +142,13 @@ class TestInstallToolsLogic:
142
142
  def test_windows_package_mapping(self):
143
143
  """Verify correct winget package IDs for missing tools."""
144
144
  packages = {
145
- "makemkvcon": "MakeMKV.MakeMKV",
145
+ "makemkvcon": "GuinpinSoft.MakeMKV",
146
146
  "ffprobe": "Gyan.FFmpeg",
147
- "mkvmerge": "MKVToolNix.MKVToolNix",
147
+ "mkvmerge": "MoritzBunkus.MKVToolNix",
148
148
  }
149
149
  missing = ["ffprobe", "mkvmerge"]
150
150
  to_install = sorted(set(packages[t] for t in missing if packages.get(t)))
151
- assert to_install == ["Gyan.FFmpeg", "MKVToolNix.MKVToolNix"]
151
+ assert to_install == ["Gyan.FFmpeg", "MoritzBunkus.MKVToolNix"]
152
152
 
153
153
  def test_macos_package_mapping(self):
154
154
  """Verify correct brew package names for missing tools."""
@@ -164,9 +164,9 @@ class TestInstallToolsLogic:
164
164
  def test_deduplicates_packages(self):
165
165
  """mkvmerge and mkvpropedit map to same package."""
166
166
  packages = {
167
- "mkvmerge": "MKVToolNix.MKVToolNix",
168
- "mkvpropedit": "MKVToolNix.MKVToolNix",
167
+ "mkvmerge": "MoritzBunkus.MKVToolNix",
168
+ "mkvpropedit": "MoritzBunkus.MKVToolNix",
169
169
  }
170
170
  missing = ["mkvmerge", "mkvpropedit"]
171
171
  to_install = sorted(set(packages[t] for t in missing if packages.get(t)))
172
- assert to_install == ["MKVToolNix.MKVToolNix"]
172
+ assert to_install == ["MoritzBunkus.MKVToolNix"]
@@ -1,115 +0,0 @@
1
- # Monorepo Refactor Plan
2
-
3
- ## Project Context
4
-
5
- riplex automates disc ripping and file organization for Plex. It reads physical discs via MakeMKV, identifies what's on them using TMDb and dvdcompare.net metadata, rips the right titles, then deduplicates, matches, renames, and moves files into the exact folder structure Plex expects.
6
-
7
- There are currently two repos:
8
- - **riplex** (`C:\Users\asher\Projects\anycredit5518\riplex`): the library and CLI, published to PyPI
9
- - **riplex-app** (`C:\Users\asher\Projects\anycredit5518\riplex-app`): a Flet-based GUI that depends on riplex as a pip package
10
-
11
- ## Problem
12
-
13
- The current two-repo setup has issues:
14
-
15
- 1. **Shared logic is trapped in `cli.py`**: Functions like disc format detection, release scoring, disc number inference, and title extraction are private helpers in the CLI module. The GUI needs the same logic but can't access it, so it duplicates it.
16
-
17
- 2. **Circular development friction**: Changing a shared algorithm means updating both repos, keeping them in sync, and dealing with version pinning.
18
-
19
- 3. **Distribution complexity**: Users who want the GUI must install two packages. Building the GUI .exe requires a working riplex install.
20
-
21
- ## Goal
22
-
23
- Consolidate into a single repo with a clean separation of concerns:
24
-
25
- - **Library layer**: all business logic, reusable by any frontend
26
- - **CLI layer**: thin wrapper that handles argument parsing, terminal formatting, and interactive prompts
27
- - **GUI layer**: thin wrapper that handles Flet screens and user interaction
28
-
29
- Both the CLI and GUI should be thin consumers of the library. Neither should contain business logic.
30
-
31
- ## Requirements
32
-
33
- 1. **Single repo**: all code lives in one place, one set of tests, one CI pipeline
34
- 2. **Clean library API**: shared orchestration logic must be importable as a public module (not private `_functions` buried in a CLI file)
35
- 3. **Independent installability**: `pip install riplex` gets you library + CLI (no GUI dependencies). GUI is optional via `pip install riplex[gui]` or standalone binary.
36
- 4. **Zero CLI regression**: the `riplex` command must behave identically after the refactor. All existing tests must pass at every step.
37
- 5. **Standalone GUI binary**: the GUI can be packaged as a .exe/.app for users who don't have Python
38
- 6. **Atomic commits**: each commit should be a self-contained logical change that doesn't break the build
39
-
40
- ## Target Architecture
41
-
42
- ```
43
- riplex/
44
- src/
45
- riplex/ # Shared library (all business logic)
46
- riplex_cli/ # CLI thin wrapper (argparse, terminal formatting, prompts)
47
- riplex_app/ # GUI thin wrapper (Flet screens, no business logic)
48
- tests/
49
- docs/
50
- pyproject.toml
51
- ```
52
-
53
- ### Distribution
54
-
55
- | Install method | What you get |
56
- |---|---|
57
- | `pip install riplex` | Library + CLI |
58
- | `pip install riplex[gui]` | Library + CLI + GUI (flet dependency) |
59
- | GitHub Release .exe/.app | Standalone GUI binary |
60
-
61
- ## Scope of Work
62
-
63
- The core of this refactor is extracting shared logic out of `cli.py` so both frontends can use it, then bringing the GUI source into this repo.
64
-
65
- Key areas to address:
66
- - `cli.py` is ~2800 lines mixing business logic with CLI presentation. The reusable pipeline logic (disc detection, format inference, release selection/scoring, disc number detection, title inference, folder creation, manifest handling) needs to become a proper public module.
67
- - The GUI currently duplicates some of this logic (release scoring, format detection). After extraction, remove the duplicates.
68
- - `pyproject.toml` needs to declare multiple packages, entry points for both CLI and GUI, and an optional `[gui]` dependency group.
69
- - CI needs a new workflow to build GUI binaries on tagged releases.
70
- - The standalone `riplex-app` repo should be archived with a pointer to the monorepo.
71
-
72
- ## Implementation Recommendations
73
-
74
- These are suggested approaches, not prescriptive steps. Use judgment on the exact module boundaries and function signatures.
75
-
76
- ### Extracting shared logic
77
-
78
- Create a module (e.g. `riplex.orchestrate` or similar) to house the pipeline functions currently private in `cli.py`. Candidates include:
79
- - Disc format detection from resolution metadata
80
- - Title inference from volume labels and MKV title tags
81
- - Media type inference (movie vs TV)
82
- - Release scoring by duration matching
83
- - Disc number auto-detection
84
- - Rip folder creation and manifest handling
85
-
86
- ### Splitting the CLI
87
-
88
- Move argparse setup, command dispatch, terminal-specific formatting (progress bars, dry-run banners, rip guide printing), and the interactive setup wizard into `src/riplex_cli/`. The CLI imports from `riplex.*` for all logic.
89
-
90
- ### Bringing in the GUI
91
-
92
- The GUI source at `riplex-app/src/riplex_app/` already imports from `riplex.*` so it should mostly work after copying. Replace any duplicated logic with imports from the new shared module.
93
-
94
- ### Suggested work order
95
-
96
- 1. Extract the shared module from `cli.py` first (independently testable, doesn't break anything)
97
- 2. Create `riplex_cli/` and move CLI wrapper code
98
- 3. Verify all tests pass and CLI behaves identically
99
- 4. Copy GUI source in, remove duplicates, update imports
100
- 5. Update `pyproject.toml`
101
- 6. Add GUI build workflow
102
- 7. Merge to main when stable
103
-
104
- ## Constraints
105
-
106
- - Work on the `monorepo` branch until stable
107
- - All existing tests must pass at every step
108
- - The GUI does not need its own test suite yet (manual testing is fine)
109
- - Do not change CLI behavior unless explicitly intended
110
-
111
- ## GUI Feedback (to address post-merge)
112
-
113
- - Done screen: should auto-update when ripping finishes (currently requires a click)
114
- - Done screen: add "Organize" button as a next step after ripping
115
- - Multi-disc flow: prompt to insert next disc instead of showing the final "done" screen