riplex 0.6.1__tar.gz → 0.6.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.
Files changed (125) hide show
  1. riplex-0.6.2/.gitignore +12 -0
  2. {riplex-0.6.1/src/riplex.egg-info → riplex-0.6.2}/PKG-INFO +2 -2
  3. {riplex-0.6.1 → riplex-0.6.2}/docs/changelog.md +15 -0
  4. riplex-0.6.2/docs/getting-started/installation.md +330 -0
  5. {riplex-0.6.1 → riplex-0.6.2}/issues/planned-features.md +0 -45
  6. {riplex-0.6.1 → riplex-0.6.2}/pyproject.toml +1 -1
  7. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/provider.py +23 -3
  8. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/matcher.py +110 -11
  9. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/organizer.py +9 -1
  10. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/scanner.py +7 -0
  11. {riplex-0.6.1 → riplex-0.6.2/src/riplex.egg-info}/PKG-INFO +2 -2
  12. {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/SOURCES.txt +0 -2
  13. {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/requires.txt +1 -1
  14. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/folder_picker.py +13 -2
  15. {riplex-0.6.1 → riplex-0.6.2}/tests/test_matcher.py +227 -0
  16. {riplex-0.6.1 → riplex-0.6.2}/tests/test_organizer.py +36 -0
  17. riplex-0.6.1/.gitignore +0 -0
  18. riplex-0.6.1/REFACTOR_PLAN.md +0 -65
  19. riplex-0.6.1/docs/getting-started/installation.md +0 -217
  20. riplex-0.6.1/issues/cross-disc-dvdcompare-matching.md +0 -35
  21. {riplex-0.6.1 → riplex-0.6.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {riplex-0.6.1 → riplex-0.6.2}/.github/agents/riplex.agent.md +0 -0
  23. {riplex-0.6.1 → riplex-0.6.2}/.github/copilot-instructions.md +0 -0
  24. {riplex-0.6.1 → riplex-0.6.2}/.github/workflows/publish.yml +0 -0
  25. {riplex-0.6.1 → riplex-0.6.2}/.github/workflows/release.yml +0 -0
  26. {riplex-0.6.1 → riplex-0.6.2}/.vscode/settings.json +0 -0
  27. {riplex-0.6.1 → riplex-0.6.2}/CONTRIBUTORS.md +0 -0
  28. {riplex-0.6.1 → riplex-0.6.2}/LICENSE +0 -0
  29. {riplex-0.6.1 → riplex-0.6.2}/README.md +0 -0
  30. {riplex-0.6.1 → riplex-0.6.2}/docs/architecture.md +0 -0
  31. {riplex-0.6.1 → riplex-0.6.2}/docs/getting-started/configuration.md +0 -0
  32. {riplex-0.6.1 → riplex-0.6.2}/docs/guide/lookup.md +0 -0
  33. {riplex-0.6.1 → riplex-0.6.2}/docs/guide/orchestrate.md +0 -0
  34. {riplex-0.6.1 → riplex-0.6.2}/docs/guide/organize.md +0 -0
  35. {riplex-0.6.1 → riplex-0.6.2}/docs/guide/workflow.md +0 -0
  36. {riplex-0.6.1 → riplex-0.6.2}/docs/index.md +0 -0
  37. {riplex-0.6.1 → riplex-0.6.2}/docs/naming-rules.md +0 -0
  38. {riplex-0.6.1 → riplex-0.6.2}/docs/reference/cli.md +0 -0
  39. {riplex-0.6.1 → riplex-0.6.2}/docs/troubleshooting.md +0 -0
  40. {riplex-0.6.1 → riplex-0.6.2}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  41. {riplex-0.6.1 → riplex-0.6.2}/mkdocs.yml +0 -0
  42. {riplex-0.6.1 → riplex-0.6.2}/setup.cfg +0 -0
  43. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/__init__.py +0 -0
  44. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/cache.py +0 -0
  45. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/config.py +0 -0
  46. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/dedup.py +0 -0
  47. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/detect.py +0 -0
  48. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/__init__.py +0 -0
  49. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/analysis.py +0 -0
  50. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/makemkv.py +0 -0
  51. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/formatter.py +0 -0
  52. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/lookup.py +0 -0
  53. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/manifest.py +0 -0
  54. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/__init__.py +0 -0
  55. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/planner.py +0 -0
  56. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/provider.py +0 -0
  57. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/sources/__init__.py +0 -0
  58. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/sources/tmdb.py +0 -0
  59. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/models.py +0 -0
  60. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/normalize.py +0 -0
  61. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/snapshot.py +0 -0
  62. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/splitter.py +0 -0
  63. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/tagger.py +0 -0
  64. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/title.py +0 -0
  65. {riplex-0.6.1 → riplex-0.6.2}/src/riplex/ui.py +0 -0
  66. {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/dependency_links.txt +0 -0
  67. {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/entry_points.txt +0 -0
  68. {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/top_level.txt +0 -0
  69. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/__init__.py +0 -0
  70. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/bug_report.py +0 -0
  71. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/main.py +0 -0
  72. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/__init__.py +0 -0
  73. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_detection.py +0 -0
  74. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_overview.py +0 -0
  75. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_swap.py +0 -0
  76. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/done.py +0 -0
  77. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/metadata.py +0 -0
  78. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/orchestrate_done.py +0 -0
  79. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/organize_done.py +0 -0
  80. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/organize_preview.py +0 -0
  81. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/progress.py +0 -0
  82. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/release.py +0 -0
  83. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/selection.py +0 -0
  84. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/update.py +0 -0
  85. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/welcome.py +0 -0
  86. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/updater.py +0 -0
  87. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/__init__.py +0 -0
  88. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/__init__.py +0 -0
  89. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/lookup.py +0 -0
  90. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/orchestrate.py +0 -0
  91. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/organize.py +0 -0
  92. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/rip.py +0 -0
  93. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/setup.py +0 -0
  94. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/formatting.py +0 -0
  95. {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/main.py +0 -0
  96. {riplex-0.6.1 → riplex-0.6.2}/tests/__init__.py +0 -0
  97. {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/chernobyl_disc1.json +0 -0
  98. {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  99. {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/makemkvcon_list.txt +0 -0
  100. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  101. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  102. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  103. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  104. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  105. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  106. {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Waterworld.snapshot.json +0 -0
  107. {riplex-0.6.1 → riplex-0.6.2}/tests/test_cache.py +0 -0
  108. {riplex-0.6.1 → riplex-0.6.2}/tests/test_cli_utils.py +0 -0
  109. {riplex-0.6.1 → riplex-0.6.2}/tests/test_config.py +0 -0
  110. {riplex-0.6.1 → riplex-0.6.2}/tests/test_dedup.py +0 -0
  111. {riplex-0.6.1 → riplex-0.6.2}/tests/test_detect.py +0 -0
  112. {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_analysis.py +0 -0
  113. {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_fixtures.py +0 -0
  114. {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_provider.py +0 -0
  115. {riplex-0.6.1 → riplex-0.6.2}/tests/test_formatter.py +0 -0
  116. {riplex-0.6.1 → riplex-0.6.2}/tests/test_makemkv.py +0 -0
  117. {riplex-0.6.1 → riplex-0.6.2}/tests/test_normalize.py +0 -0
  118. {riplex-0.6.1 → riplex-0.6.2}/tests/test_planner.py +0 -0
  119. {riplex-0.6.1 → riplex-0.6.2}/tests/test_rip_guide.py +0 -0
  120. {riplex-0.6.1 → riplex-0.6.2}/tests/test_scanner.py +0 -0
  121. {riplex-0.6.1 → riplex-0.6.2}/tests/test_snapshot.py +0 -0
  122. {riplex-0.6.1 → riplex-0.6.2}/tests/test_splitter.py +0 -0
  123. {riplex-0.6.1 → riplex-0.6.2}/tests/test_tagger.py +0 -0
  124. {riplex-0.6.1 → riplex-0.6.2}/tests/test_ui.py +0 -0
  125. {riplex-0.6.1 → riplex-0.6.2}/tests/test_updater.py +0 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+
9
+ # Config file with API keys
10
+ riplex.toml
11
+ plex-planner.toml
12
+ riplex_app.log
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.6.1
3
+ Version: 0.6.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
9
  Requires-Dist: httpx>=0.27
10
- Requires-Dist: dvdcompare-scraper>=0.1.7
10
+ Requires-Dist: dvdcompare-scraper>=0.1.12
11
11
  Requires-Dist: platformdirs>=4.0
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -4,6 +4,21 @@ 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
+ ## 2026-05-09
8
+
9
+ ### Changed
10
+
11
+ - Installation guide: complete restructure for clarity. Install riplex first, then setup, then manual tool installation as a fallback. Each install option now covers all platforms (Windows, macOS, Linux, immutable Linux distros).
12
+ - Installation guide: Windows executable instructions rewritten with step-by-step PATH setup, SmartScreen guidance, and separate GUI vs CLI paths.
13
+ - Installation guide: macOS CLI now installs to `/usr/local/bin/riplex` with proper rename.
14
+ - Installation guide: Option C (from source) split into numbered steps with separate Windows and macOS/Linux commands.
15
+ - Installation guide: tkinter/folder picker note now covers both macOS and Linux.
16
+
17
+ ### Added
18
+
19
+ - Installation guide: Linux (Bazzite, Fedora Silverblue, immutable distros) sections for pipx install and manual tool installation, including Flatpak wrapper script for MKVToolNix.
20
+ - Installation guide: MakeMKV registration pulled into its own subsection.
21
+
7
22
  ## 2026-05-08
8
23
 
9
24
  ### Added
@@ -0,0 +1,330 @@
1
+ # Installation
2
+
3
+ ## Installing riplex
4
+
5
+ There are three ways to install riplex:
6
+
7
+ - **[Pre-built executables](#option-a-pre-built-executables)** - easiest, no Python needed
8
+ - **[pipx](#option-b-install-with-pipx-recommended)** - recommended for Python users
9
+ - **[From source](#option-c-install-from-source)** - for developers
10
+
11
+ ### Option A: Pre-built executables
12
+
13
+ Download the latest release for your platform from the
14
+ [GitHub Releases page](https://github.com/AnyCredit5518/riplex/releases).
15
+
16
+ #### Windows
17
+
18
+ **GUI only (easiest):**
19
+
20
+ 1. Download `riplex-ui-windows.exe`
21
+ 2. Move it wherever you keep apps (e.g. `C:\Program Files\riplex\`)
22
+ 3. Double-click to run. Windows SmartScreen may warn you because the app
23
+ isn't code-signed -- click **More info** then **Run anyway**.
24
+ 4. Optionally, right-click the `.exe` and select **Create shortcut** to add
25
+ it to your desktop or Start menu.
26
+
27
+ **CLI (for terminal users):**
28
+
29
+ 1. Download `riplex-windows.exe`
30
+ 2. Rename it to `riplex.exe`
31
+ 3. Move it to a folder of your choice (e.g. `C:\Program Files\riplex\`)
32
+ 4. Add that folder to your system PATH:
33
+ - Open **Settings** > **System** > **About** > **Advanced system settings**
34
+ - Click **Environment Variables**
35
+ - Under **User variables**, select **Path** and click **Edit**
36
+ - Click **New** and paste the folder path (e.g. `C:\Program Files\riplex\`)
37
+ - Click **OK** on all dialogs
38
+ 5. Open a **new** terminal and run `riplex setup`
39
+
40
+ > [!TIP]
41
+ > You can install both. The GUI includes built-in setup, so you don't need
42
+ > the CLI unless you prefer working in a terminal.
43
+
44
+ #### macOS (Apple Silicon only)
45
+
46
+ > **Intel Mac?** Pre-built binaries aren't available for Intel Macs. GitHub
47
+ > [deprecated their Intel macOS build runners](https://github.blog/changelog/2024-09-16-github-actions-macos-13-larger-runner-image-brownout-dates/).
48
+ > Use [Option B: Install with pipx](#option-b-install-with-pipx-recommended)
49
+ > instead, which works on any Mac.
50
+
51
+ **GUI:**
52
+
53
+ 1. Download `riplex-ui-macos.zip`
54
+ 2. Unzip it and move `riplex-ui.app` to `/Applications/`
55
+ 3. **Allow the app to open.** macOS blocks apps from unidentified developers.
56
+ The first time you open it, you'll see a warning -- do **not** click
57
+ "Move to Trash." Instead:
58
+
59
+ - **Right-click** (or Control-click) `riplex-ui.app`, choose **Open**,
60
+ then click **Open** in the dialog. macOS remembers this and won't ask
61
+ again.
62
+
63
+ - If that doesn't work, open Terminal and run:
64
+ ```
65
+ xattr -dr com.apple.quarantine /Applications/riplex-ui.app
66
+ ```
67
+
68
+ **CLI:**
69
+
70
+ 1. Download `riplex-macos`
71
+ 2. Open Terminal and run:
72
+ ```
73
+ mv ~/Downloads/riplex-macos /usr/local/bin/riplex
74
+ chmod +x /usr/local/bin/riplex
75
+ xattr -dr com.apple.quarantine /usr/local/bin/riplex
76
+ ```
77
+ 3. Run `riplex setup`
78
+
79
+ #### Linux
80
+
81
+ Pre-built executables are not currently available for Linux. Use
82
+ [Option B: Install with pipx](#option-b-install-with-pipx-recommended)
83
+ instead.
84
+
85
+ ### Option B: Install with pipx (recommended)
86
+
87
+ [pipx](https://pipx.pypa.io/) installs Python apps in isolated environments
88
+ but makes their commands available globally. No venv activation needed --
89
+ `riplex` and `riplex-ui` just work from any terminal.
90
+
91
+ #### 1. Install Python and pipx
92
+
93
+ **Windows:**
94
+
95
+ Download Python from https://www.python.org/downloads/ and run the installer.
96
+ **Check "Add Python to PATH"** at the bottom of the first screen. Then open
97
+ a terminal and run:
98
+
99
+ ```
100
+ pip install pipx
101
+ pipx ensurepath
102
+ ```
103
+
104
+ **macOS:**
105
+
106
+ ```
107
+ brew install python pipx
108
+ pipx ensurepath
109
+ ```
110
+
111
+ **Linux (Debian, Ubuntu, Mint, Pop!_OS, etc.):**
112
+
113
+ ```
114
+ sudo apt install python3 pipx
115
+ pipx ensurepath
116
+ ```
117
+
118
+ **Linux (Bazzite, Fedora Silverblue, and other immutable distros):**
119
+
120
+ Python and pipx aren't available via `apt` on immutable distros. Layer them
121
+ with `rpm-ostree`:
122
+
123
+ ```
124
+ rpm-ostree install python3 python3-pip
125
+ ```
126
+
127
+ Then reboot and run:
128
+
129
+ ```
130
+ pip install --user pipx
131
+ pipx ensurepath
132
+ ```
133
+
134
+ Restart your terminal after `ensurepath` so the new PATH takes effect.
135
+
136
+ #### 2. Install riplex
137
+
138
+ ```bash
139
+ pipx install "riplex[gui]"
140
+ ```
141
+
142
+ This installs both `riplex` (CLI) and `riplex-ui` (GUI) as globally available
143
+ commands.
144
+
145
+ > [!TIP]
146
+ > To install the CLI only (no GUI), run `pipx install riplex` instead.
147
+
148
+ #### Updating
149
+
150
+ ```bash
151
+ pipx upgrade riplex
152
+ ```
153
+
154
+ ### Option C: Install from source
155
+
156
+ For developers who want to contribute or run the latest unreleased code.
157
+
158
+ #### 1. Clone the repository
159
+
160
+ ```bash
161
+ git clone https://github.com/AnyCredit5518/riplex.git
162
+ cd riplex
163
+ ```
164
+
165
+ #### 2. Create and activate a virtual environment
166
+
167
+ **Windows:**
168
+
169
+ ```
170
+ py -m venv .venv
171
+ .venv\Scripts\activate
172
+ ```
173
+
174
+ **macOS / Linux:**
175
+
176
+ ```bash
177
+ python3.12 -m venv .venv
178
+ source .venv/bin/activate
179
+ ```
180
+
181
+ #### 3. Install in development mode
182
+
183
+ ```bash
184
+ pip install -e ".[dev,gui]"
185
+ ```
186
+
187
+ > [!NOTE]
188
+ > With a venv, `riplex` and `riplex-ui` only work while the venv is
189
+ > activated. For a global install that works from any terminal, use
190
+ > [pipx](#option-b-install-with-pipx-recommended) instead.
191
+
192
+ > [!TIP]
193
+ > The repo's `.vscode/settings.json` points VS Code at `.venv`
194
+ > automatically, so the integrated terminal activates it on open.
195
+
196
+ #### macOS SSL fix (Homebrew Python only)
197
+
198
+ If you installed Python via Homebrew and `riplex-ui` crashes on first launch
199
+ with an SSL certificate error, run this one-time fix:
200
+
201
+ ```bash
202
+ CERT=$(python3.12 -c "import certifi; print(certifi.where())")
203
+ echo "export SSL_CERT_FILE=\"$CERT\"" >> .venv/bin/activate
204
+ echo "export REQUESTS_CA_BUNDLE=\"$CERT\"" >> .venv/bin/activate
205
+ source .venv/bin/activate
206
+ ```
207
+
208
+ #### GUI folder picker (tkinter)
209
+
210
+ The browse buttons in the GUI use tkinter, which some platforms don't include
211
+ by default. Without it, the browse buttons show a hint to type the path
212
+ manually instead.
213
+
214
+ - **macOS (Homebrew):** `brew install python-tk@3.12`
215
+ - **Linux (Debian/Ubuntu):** `sudo apt install python3-tk`
216
+
217
+ ## Setup
218
+
219
+ After installing, run the setup wizard. You can do this from the CLI:
220
+
221
+ ```bash
222
+ riplex setup
223
+ ```
224
+
225
+ Or just launch the GUI -- it checks for missing tools on startup and walks you
226
+ through setup automatically.
227
+
228
+ The setup wizard will:
229
+
230
+ 1. Ask for your TMDb API key (free at https://www.themoviedb.org/settings/api)
231
+
232
+ > [!TIP]
233
+ > TMDb asks for an app name and URL when you request a key. You can just
234
+ > enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex`
235
+ > as the URL. The rest of the form can be filled with basic info -- it
236
+ > doesn't need to be a real business. The key is approved instantly.
237
+
238
+ 2. Ask where your Plex library and MakeMKV rip folders are
239
+ 3. Check for required tools (MakeMKV, ffprobe, mkvmerge, mkvpropedit)
240
+ 4. Offer to install any missing tools automatically (via winget on Windows,
241
+ Homebrew on macOS, or apt on Debian/Ubuntu-based Linux)
242
+
243
+ If you skip setup, it runs automatically the first time you use any command.
244
+
245
+ ### Verify
246
+
247
+ ```bash
248
+ riplex --help
249
+ riplex-ui
250
+ ```
251
+
252
+ Both commands should work from any terminal.
253
+
254
+ ## Manual tool installation
255
+
256
+ Most users don't need this section -- the setup wizard installs tools
257
+ automatically on Windows, macOS (with Homebrew), and Debian/Ubuntu-based
258
+ Linux. If the wizard couldn't install a tool for your platform, or you prefer
259
+ to install manually, follow the instructions below.
260
+
261
+ riplex requires these three tools:
262
+
263
+ | Tool | Purpose |
264
+ |---|---|
265
+ | [MakeMKV](https://www.makemkv.com/) | Disc reading and ripping (`makemkvcon`) |
266
+ | [FFmpeg](https://ffmpeg.org/) | MKV metadata probing (`ffprobe`) |
267
+ | [MKVToolNix](https://mkvtoolnix.download/) | MKV splitting and tagging (`mkvmerge`, `mkvpropedit`) |
268
+
269
+ ### Windows
270
+
271
+ ```
272
+ winget install GuinpinSoft.MakeMKV
273
+ winget install Gyan.FFmpeg
274
+ winget install MoritzBunkus.MKVToolNix
275
+ ```
276
+
277
+ Or download installers from the links above. These installers add the tools to
278
+ your PATH automatically.
279
+
280
+ ### macOS
281
+
282
+ ```
283
+ brew install ffmpeg mkvtoolnix
284
+ ```
285
+
286
+ MakeMKV must be downloaded from https://www.makemkv.com/ since it isn't in
287
+ Homebrew. The app bundle includes `makemkvcon` automatically.
288
+
289
+ ### Linux (Debian, Ubuntu, Pop!_OS, Mint, etc.)
290
+
291
+ ```
292
+ sudo apt install ffmpeg mkvtoolnix
293
+ ```
294
+
295
+ MakeMKV must be downloaded from https://www.makemkv.com/ or built from
296
+ source. See the [MakeMKV forum](https://forum.makemkv.com/forum/viewtopic.php?t=224)
297
+ for instructions.
298
+
299
+ ### Linux (Bazzite, Fedora Silverblue, and other immutable distros)
300
+
301
+ On immutable distros, `apt` isn't available. Use `rpm-ostree` to install
302
+ packages:
303
+
304
+ ```
305
+ rpm-ostree install ffmpeg mkvtoolnix
306
+ ```
307
+
308
+ Then reboot for the changes to take effect.
309
+
310
+ MakeMKV must be downloaded from https://www.makemkv.com/.
311
+
312
+ > [!WARNING]
313
+ > If you installed MKVToolNix as a Flatpak, `mkvmerge` won't be on your
314
+ > system PATH even though the GUI works fine. Either layer it with
315
+ > `rpm-ostree install mkvtoolnix` (recommended) or create a wrapper script:
316
+ >
317
+ > ```
318
+ > sudo tee /usr/local/bin/mkvmerge << 'EOF'
319
+ > #!/bin/sh
320
+ > exec flatpak run --command=mkvmerge org.bunkus.mkvtoolnix-gui "$@"
321
+ > EOF
322
+ > sudo chmod +x /usr/local/bin/mkvmerge
323
+ > ```
324
+
325
+ ### MakeMKV registration
326
+
327
+ MakeMKV requires a registration key. A free beta key is available at
328
+ https://forum.makemkv.com/forum/viewtopic.php?f=5&t=1053 and must be entered
329
+ in MakeMKV (Help > Register) before `makemkvcon` will work. The beta key is
330
+ updated periodically.
@@ -21,38 +21,6 @@ episode collapsing.
21
21
  override for users with separate libraries.
22
22
 
23
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
24
  ## Interactive Lookup Command
57
25
 
58
26
  `riplex lookup` currently auto-picks the first TMDb match and default
@@ -77,17 +45,4 @@ tracks when ripping, not just the default/English track.
77
45
  if the user explicitly configures preferred languages
78
46
 
79
47
 
80
- ## Drop Pre-built Intel macOS Binary
81
-
82
- The `macos-13` (Intel) CI runner is slow to queue and GitHub is phasing out
83
- Intel Mac hardware. Intel Macs are a shrinking minority of users.
84
-
85
- ### Plan
86
48
 
87
- - Remove the `macos-13` / `x86_64` matrix entry from `release.yml`
88
- - Remove `riplex-macos-x86_64` and `riplex-ui-macos-x86_64.zip` from the
89
- release step
90
- - Update `updater.py` to stop looking for arch-specific assets (only arm64)
91
- - Update installation docs: macOS section offers only the ARM build; Intel
92
- Mac users are directed to install from source (venv + `pip install -e`)
93
- - Update README download table accordingly
@@ -11,7 +11,7 @@ requires-python = ">=3.11"
11
11
  license = {text = "MIT"}
12
12
  dependencies = [
13
13
  "httpx>=0.27",
14
- "dvdcompare-scraper>=0.1.7",
14
+ "dvdcompare-scraper>=0.1.12",
15
15
  "platformdirs>=4.0",
16
16
  ]
17
17
 
@@ -21,6 +21,7 @@ from riplex.models import PlannedDisc, PlannedEpisode, PlannedExtra
21
21
  log = logging.getLogger(__name__)
22
22
 
23
23
  _DVDCOMPARE_TTL_DAYS = 30
24
+ _DVDCOMPARE_NEG_TTL_DAYS = 7
24
25
  _CACHE_NS = "dvdcompare"
25
26
 
26
27
 
@@ -39,9 +40,11 @@ class DiscProvider:
39
40
  self,
40
41
  cache_ns: str = _CACHE_NS,
41
42
  ttl_days: int = _DVDCOMPARE_TTL_DAYS,
43
+ neg_ttl_days: int = _DVDCOMPARE_NEG_TTL_DAYS,
42
44
  ) -> None:
43
45
  self.cache_ns = cache_ns
44
46
  self.ttl_days = ttl_days
47
+ self.neg_ttl_days = neg_ttl_days
45
48
 
46
49
  # -- lookup -----------------------------------------------------------
47
50
 
@@ -89,14 +92,31 @@ class DiscProvider:
89
92
  disc_format: str | None = None,
90
93
  year: int | None = None,
91
94
  ) -> FilmComparison:
92
- """Fetch a FilmComparison, returning cached data when available."""
95
+ """Fetch a FilmComparison, returning cached data when available.
96
+
97
+ Negative lookups (no results on dvdcompare) are also cached with a
98
+ shorter TTL to avoid hammering the search endpoint for titles that
99
+ don't exist.
100
+ """
93
101
  cache_key = cache.hash_key(f"film|{title}|{disc_format}|{year}")
102
+
103
+ # Check for negative cache first (shorter TTL)
104
+ neg_cached = cache.cache_get(self.cache_ns, cache_key, ttl_days=self.neg_ttl_days)
105
+ if neg_cached is not None and neg_cached.get("_negative"):
106
+ log.debug("dvdcompare negative cache hit for '%s'", title)
107
+ raise LookupError(f"No dvdcompare results for '{title}' (cached)")
108
+
94
109
  cached = cache.cache_get(self.cache_ns, cache_key, ttl_days=self.ttl_days)
95
- if cached is not None:
110
+ if cached is not None and not cached.get("_negative"):
96
111
  log.debug("dvdcompare film cache hit for '%s'", title)
97
112
  return _dict_to_film(cached)
98
113
 
99
- film = await find_film(title, disc_format, year=year)
114
+ try:
115
+ film = await find_film(title, disc_format, year=year)
116
+ except LookupError:
117
+ log.debug("dvdcompare no results for '%s', caching negative result", title)
118
+ cache.cache_set(self.cache_ns, cache_key, {"_negative": True})
119
+ raise
100
120
  log.debug("dvdcompare find_film('%s', format=%s, year=%s): %d release(s)",
101
121
  title, disc_format, year, len(film.releases) if film.releases else 0)
102
122
  cache.cache_set(self.cache_ns, cache_key, dataclasses.asdict(film))
@@ -166,6 +166,12 @@ _MAX_MATCH_DELTA = 300
166
166
 
167
167
  _PLAY_ALL_RE = re.compile(r"\bplay\s*all\b", re.IGNORECASE)
168
168
 
169
+ # Edition patterns in dvdcompare feature titles
170
+ _EDITION_RE = re.compile(
171
+ r"((?:Extended|Director'?s|Unrated|Ultimate|Special|Theatrical)\s+(?:Cut|Edition|Version))",
172
+ re.IGNORECASE,
173
+ )
174
+
169
175
 
170
176
  def collect_disc_targets(
171
177
  discs: list[PlannedDisc],
@@ -178,6 +184,7 @@ def collect_disc_targets(
178
184
  whichever disc has ``is_film=True``).
179
185
  """
180
186
  targets: list[tuple[str, int, int | None]] = []
187
+ suppress_movie_target = False
181
188
 
182
189
  # Identify the film disc number (if any) so the movie target is
183
190
  # constrained to that disc's folder.
@@ -201,22 +208,42 @@ def collect_disc_targets(
201
208
 
202
209
  # Collect non-play-all extras as targets; play-all entries are
203
210
  # redundant when individual parts are also listed on the disc.
204
- # Also skip "The Film ..." entries on film discs: they represent
205
- # the main feature which is already covered by the movie target.
211
+ # Also skip "The Film ..." entries on film discs UNLESS there are
212
+ # multiple editions (e.g. Theatrical Cut + Extended Cut).
206
213
  play_all_extras: list[PlannedExtra] = []
207
214
  regular_extras: list[PlannedExtra] = []
215
+ film_entries: list[PlannedExtra] = []
208
216
  for ex in disc.extras:
209
- if ex.runtime_seconds <= 0:
210
- continue
211
217
  if disc.is_film and ex.title.lower().startswith("the film"):
212
- log.debug("Disc %d: skipping film entry '%s' (covered by movie target)",
213
- disc.number, ex.title)
218
+ film_entries.append(ex)
219
+ continue
220
+ if ex.runtime_seconds <= 0:
214
221
  continue
215
222
  if _PLAY_ALL_RE.search(ex.title):
216
223
  play_all_extras.append(ex)
217
224
  else:
218
225
  regular_extras.append(ex)
219
226
 
227
+ # Handle "The Film ..." entries on film discs
228
+ if len(film_entries) > 1:
229
+ # Multiple editions: create edition-aware targets and suppress
230
+ # the single TMDb movie target added above. Runtimes may be
231
+ # zero when dvdcompare only lists the title/resolution; those
232
+ # are filled in during matching by pairing editions with files
233
+ # by duration order (shortest = theatrical, longest = extended).
234
+ suppress_movie_target = True
235
+ for ex in film_entries:
236
+ m = _EDITION_RE.search(ex.title)
237
+ edition = m.group(1) if m else ex.title
238
+ label = f"{prefix}: {edition} (movie)"
239
+ runtime = ex.runtime_seconds or 0
240
+ log.debug("Disc %d: multi-edition film entry '%s' -> target '%s' (%ds)",
241
+ disc.number, ex.title, label, runtime)
242
+ targets.append((label, runtime, disc.number))
243
+ elif film_entries:
244
+ log.debug("Disc %d: skipping single film entry '%s' (covered by movie target)",
245
+ disc.number, film_entries[0].title)
246
+
220
247
  # Only include play-all entries if there are no other targets on
221
248
  # this disc (episodes or regular extras) to match against.
222
249
  has_parts = bool(disc.episodes) or bool(regular_extras)
@@ -233,6 +260,12 @@ def collect_disc_targets(
233
260
  label += f" ({ex.feature_type})"
234
261
  targets.append((label, ex.runtime_seconds, disc.number))
235
262
 
263
+ # If multi-edition entries replaced the single movie target, remove it
264
+ if suppress_movie_target and targets and "(movie)" in targets[0][0]:
265
+ removed = targets.pop(0)
266
+ log.debug("Suppressed single movie target '%s' in favor of edition targets",
267
+ removed[0])
268
+
236
269
  return targets
237
270
 
238
271
 
@@ -427,16 +460,82 @@ def match_discs(
427
460
  claimed_files.add(fi)
428
461
  claimed_targets.add(ti)
429
462
 
463
+ # --- Pass 2: zero-runtime edition targets ---
464
+ # When dvdcompare lists multiple editions without runtimes (e.g.
465
+ # "Theatrical Cut" and "Extended Cut" both with runtime=0), pair
466
+ # unclaimed edition targets with unclaimed files by duration order:
467
+ # shortest file = theatrical, longest = extended/director's.
468
+ _THEATRICAL_WORDS = {"theatrical"}
469
+ _EXTENDED_WORDS = {"extended", "director", "unrated", "ultimate"}
470
+
471
+ zero_edition_targets = [
472
+ ti for ti, (label, runtime_s, _) in enumerate(targets)
473
+ if ti not in claimed_targets and runtime_s == 0 and "(movie)" in label
474
+ ]
475
+ if zero_edition_targets:
476
+ # Find unclaimed files on the same disc(s)
477
+ edition_disc = targets[zero_edition_targets[0]][2]
478
+ unclaimed_files = [
479
+ fi for fi in range(len(all_scanned))
480
+ if fi not in claimed_files
481
+ and all_scanned[fi].duration_seconds > 0
482
+ and (edition_disc is None or file_disc[fi] is None or file_disc[fi] == edition_disc)
483
+ ]
484
+ # Sort targets: theatrical first, then extended
485
+ def _edition_sort_key(ti: int) -> int:
486
+ label_lower = targets[ti][0].lower()
487
+ if any(w in label_lower for w in _THEATRICAL_WORDS):
488
+ return 0
489
+ if any(w in label_lower for w in _EXTENDED_WORDS):
490
+ return 1
491
+ return 2
492
+ zero_edition_targets.sort(key=_edition_sort_key)
493
+ # Sort files by duration (shortest first = theatrical)
494
+ unclaimed_files.sort(key=lambda fi: all_scanned[fi].duration_seconds)
495
+
496
+ for ti, fi in zip(zero_edition_targets, unclaimed_files):
497
+ sf = all_scanned[fi]
498
+ label = targets[ti][0]
499
+ log.debug("Edition match (no runtime): %s (%ds) -> '%s'",
500
+ sf.name, sf.duration_seconds, label)
501
+ matched.append(
502
+ MatchCandidate(
503
+ file_name=sf.name,
504
+ file_duration_seconds=sf.duration_seconds,
505
+ matched_label=label,
506
+ matched_runtime_seconds=sf.duration_seconds,
507
+ delta_seconds=0,
508
+ confidence="medium",
509
+ classification=sf.classification,
510
+ )
511
+ )
512
+ claimed_files.add(fi)
513
+ claimed_targets.add(ti)
514
+
430
515
  unmatched = [
431
516
  all_scanned[i]
432
517
  for i in range(len(all_scanned))
433
518
  if i not in claimed_files
434
519
  ]
435
- missing = [
436
- targets[i][0]
437
- for i in range(len(targets))
438
- if i not in claimed_targets
439
- ]
520
+
521
+ # Only report missing targets from discs the user actually has
522
+ # folders for (or targets with no disc constraint). If no folder
523
+ # mapped to any disc (e.g. single folder with no disc number),
524
+ # include all targets as missing (fallback to previous behavior).
525
+ present_discs = set(folder_map.values()) - {None}
526
+ if present_discs:
527
+ missing = [
528
+ targets[i][0]
529
+ for i in range(len(targets))
530
+ if i not in claimed_targets
531
+ and (targets[i][2] is None or targets[i][2] in present_discs)
532
+ ]
533
+ else:
534
+ missing = [
535
+ targets[i][0]
536
+ for i in range(len(targets))
537
+ if i not in claimed_targets
538
+ ]
440
539
 
441
540
  for sf in unmatched:
442
541
  log.debug("Unmatched file: %s (%ds)", sf.name, sf.duration_seconds)
@@ -461,7 +461,15 @@ def _compute_destination(
461
461
  # Movie main file
462
462
  if "(movie)" in label:
463
463
  edition = None
464
- if candidate.classification:
464
+ # Check for edition in label (from multi-edition disc targets)
465
+ # e.g. "Disc 1: Theatrical Cut (movie)"
466
+ if label.startswith("Disc ") and ": " in label:
467
+ edition_part = label.split(": ", 1)[1].replace(" (movie)", "")
468
+ m = _EDITION_RE.search(edition_part)
469
+ if m:
470
+ edition = m.group(1)
471
+ # Fallback: check rip-time classification
472
+ if not edition and candidate.classification:
465
473
  m = _EDITION_RE.search(candidate.classification)
466
474
  if m:
467
475
  edition = m.group(1)