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.
- riplex-0.6.2/.gitignore +12 -0
- {riplex-0.6.1/src/riplex.egg-info → riplex-0.6.2}/PKG-INFO +2 -2
- {riplex-0.6.1 → riplex-0.6.2}/docs/changelog.md +15 -0
- riplex-0.6.2/docs/getting-started/installation.md +330 -0
- {riplex-0.6.1 → riplex-0.6.2}/issues/planned-features.md +0 -45
- {riplex-0.6.1 → riplex-0.6.2}/pyproject.toml +1 -1
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/provider.py +23 -3
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/matcher.py +110 -11
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/organizer.py +9 -1
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/scanner.py +7 -0
- {riplex-0.6.1 → riplex-0.6.2/src/riplex.egg-info}/PKG-INFO +2 -2
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/SOURCES.txt +0 -2
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/requires.txt +1 -1
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/folder_picker.py +13 -2
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_matcher.py +227 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_organizer.py +36 -0
- riplex-0.6.1/.gitignore +0 -0
- riplex-0.6.1/REFACTOR_PLAN.md +0 -65
- riplex-0.6.1/docs/getting-started/installation.md +0 -217
- riplex-0.6.1/issues/cross-disc-dvdcompare-matching.md +0 -35
- {riplex-0.6.1 → riplex-0.6.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/.github/copilot-instructions.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/.github/workflows/publish.yml +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/.github/workflows/release.yml +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/.vscode/settings.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/CONTRIBUTORS.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/LICENSE +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/README.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/architecture.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/getting-started/configuration.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/guide/lookup.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/guide/orchestrate.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/guide/organize.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/guide/workflow.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/index.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/naming-rules.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/reference/cli.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/docs/troubleshooting.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/mkdocs.yml +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/setup.cfg +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/cache.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/config.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/dedup.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/detect.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/formatter.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/lookup.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/manifest.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/models.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/normalize.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/snapshot.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/splitter.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/tagger.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/title.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex/ui.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/main.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/release.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/screens/welcome.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_app/updater.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/src/riplex_cli/main.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/__init__.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_cache.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_cli_utils.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_config.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_dedup.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_detect.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_analysis.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_disc_provider.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_formatter.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_makemkv.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_normalize.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_planner.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_rip_guide.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_scanner.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_snapshot.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_splitter.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_tagger.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_ui.py +0 -0
- {riplex-0.6.1 → riplex-0.6.2}/tests/test_updater.py +0 -0
riplex-0.6.2/.gitignore
ADDED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.6.
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
205
|
-
#
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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)
|