riplex 0.9.4__tar.gz → 0.9.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {riplex-0.9.4 → riplex-0.9.6}/.github/agents/riplex.agent.md +105 -35
- {riplex-0.9.4 → riplex-0.9.6}/.github/copilot-instructions.md +28 -0
- {riplex-0.9.4/src/riplex.egg-info → riplex-0.9.6}/PKG-INFO +1 -1
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/disc/provider.py +31 -5
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/metadata/sources/tmdb.py +109 -9
- {riplex-0.9.4 → riplex-0.9.6/src/riplex.egg-info}/PKG-INFO +1 -1
- riplex-0.9.6/src/riplex.egg-info/scm_version.json +8 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_disc_analysis.py +59 -0
- riplex-0.9.6/tests/test_tmdb_provider.py +211 -0
- riplex-0.9.4/src/riplex.egg-info/scm_version.json +0 -8
- riplex-0.9.4/tests/test_tmdb_provider.py +0 -57
- {riplex-0.9.4 → riplex-0.9.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/.github/workflows/publish.yml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/.github/workflows/release.yml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/.gitignore +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/.vscode/settings.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/CONTRIBUTORS.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/LICENSE +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/README.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/architecture.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/changelog.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/cli-guide/lookup.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/cli-guide/orchestrate.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/cli-guide/organize.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/cli-guide/workflow.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/getting-started/configuration.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/getting-started/installation.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/gui-guide/gui-walkthrough.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/index.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/naming-rules.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/reference/cli.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/docs/troubleshooting.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/issues/planned-features.md +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/mkdocs.yml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/pyproject.toml +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/1_Welcome_Screen.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/2_Disc_Detection_BTTF.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/4_Disc_Release_BTTF.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/setup.cfg +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/cache.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/config.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/dedup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/detect.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/formatter.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/lookup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/manifest.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/matcher.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/models.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/normalize.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/organizer.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/scanner.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/snapshot.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/splitter.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/tagger.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/title.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/ui.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex/updater.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/SOURCES.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/scm_file_list.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/crash_dump.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/keep_awake.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/main.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/release.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_app/screens/welcome.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/src/riplex_cli/main.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/__init__.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_app_main.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_cache.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_cli_utils.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_config.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_dedup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_detect.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_disc_detection_screen.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_disc_provider.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_formatter.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_lookup.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_makemkv.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_matcher.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_normalize.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_organizer.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_planner.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_release_screen.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_rip_guide.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_scanner.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_snapshot.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_splitter.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_tagger.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_ui.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_updater.py +0 -0
- {riplex-0.9.4 → riplex-0.9.6}/tests/test_welcome_screen.py +0 -0
|
@@ -21,55 +21,84 @@ riplex eliminates the manual work after MakeMKV dumps raw MKV files from a physi
|
|
|
21
21
|
```
|
|
22
22
|
src/
|
|
23
23
|
riplex/ # Shared library (all business logic)
|
|
24
|
-
|
|
25
|
-
config.py # Config loading (~/.config/riplex/config.toml or platform equivalent)
|
|
24
|
+
config.py # Config loading (platform config dir via platformdirs)
|
|
26
25
|
models.py # Data models (ScannedFile, PlannedDisc, PlannedMovie, PlannedShow, etc.)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
metadata_sources/
|
|
30
|
-
tmdb.py # TMDb API implementation
|
|
31
|
-
disc_provider.py # dvdcompare.net bridge (lookup_discs, find_film, _convert_release)
|
|
32
|
-
disc_analysis.py # Live disc title classification (classify_title, is_skip_title, build_dvd_entries)
|
|
33
|
-
makemkv.py # makemkvcon wrapper (drive scanning, disc reading, ripping, progress parsing)
|
|
34
|
-
scanner.py # MKV folder scanner (ffprobe metadata extraction)
|
|
26
|
+
lookup.py # Shared lookup pipeline logic reusable by CLI and GUI
|
|
27
|
+
scanner.py # MKV folder scanner (ffprobe metadata extraction; find_ffprobe tool detection)
|
|
35
28
|
matcher.py # Runtime-based file-to-entry matching with disc constraints
|
|
36
29
|
organizer.py # Plex destination path builder and file mover
|
|
37
|
-
planner.py # TMDb metadata planning (builds PlannedMovie/PlannedShow)
|
|
38
30
|
detect.py # Format auto-detection, title grouping, incomplete file detection
|
|
39
31
|
dedup.py # Duplicate MKV detection (metadata fingerprint + perceptual hash)
|
|
40
32
|
splitter.py # Chapter-based MKV splitting via mkvmerge
|
|
41
33
|
tagger.py # MKV tagging (marks files as organized via mkvpropedit)
|
|
34
|
+
manifest.py # Disc manifest / debug artifact handling
|
|
42
35
|
cache.py # File-based JSON cache with TTL
|
|
43
36
|
normalize.py # Title normalization
|
|
37
|
+
title.py # Title model / parsing helpers
|
|
44
38
|
formatter.py # Text and JSON output formatting
|
|
45
|
-
snapshot.py # Scan result serialization
|
|
39
|
+
snapshot.py # Scan result serialization (offline replay)
|
|
40
|
+
updater.py # App self-update version check
|
|
46
41
|
ui.py # Interactive prompts (prompt_choice, prompt_confirm, prompt_text, prompt_multi_select)
|
|
42
|
+
disc/ # Live disc subpackage
|
|
43
|
+
analysis.py # Title classification (classify_title, is_skip_title, build_dvd_entries)
|
|
44
|
+
makemkv.py # makemkvcon wrapper (drive scanning, disc reading, ripping, progress parsing)
|
|
45
|
+
provider.py # dvdcompare.net bridge (lookup_discs, find_film, _convert_release)
|
|
46
|
+
metadata/ # Metadata subpackage
|
|
47
|
+
planner.py # TMDb metadata planning (builds PlannedMovie/PlannedShow)
|
|
48
|
+
provider.py # Abstract metadata provider interface
|
|
49
|
+
sources/
|
|
50
|
+
tmdb.py # TMDb API implementation
|
|
47
51
|
riplex_cli/ # CLI thin wrapper (argparse, command dispatch, terminal formatting)
|
|
48
|
-
main.py #
|
|
52
|
+
main.py # Argparse setup + command dispatch, entry point: main()
|
|
53
|
+
formatting.py # Terminal output formatting helpers
|
|
54
|
+
commands/ # One module per subcommand
|
|
55
|
+
orchestrate.py # Full pipeline command
|
|
56
|
+
rip.py # Single-disc rip command
|
|
57
|
+
organize.py # Organize existing MKV folder command
|
|
58
|
+
lookup.py # Preview disc contents command
|
|
59
|
+
setup.py # Interactive config wizard command
|
|
49
60
|
riplex_app/ # Flet GUI (wizard-style screens)
|
|
50
61
|
main.py # App entry point, screen navigation controller
|
|
62
|
+
bug_report.py # Bug report bundling helper
|
|
63
|
+
crash_dump.py # Crash dump capture
|
|
64
|
+
keep_awake.py # Prevent system sleep during long rips
|
|
51
65
|
screens/
|
|
52
|
-
welcome.py
|
|
53
|
-
disc_detection.py
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
welcome.py # Config and tool verification
|
|
67
|
+
disc_detection.py # Drive scanning, disc reading
|
|
68
|
+
disc_overview.py # Multi-disc overview
|
|
69
|
+
disc_swap.py # Disc swap prompt (multi-disc sets)
|
|
70
|
+
metadata.py # TMDb search and selection
|
|
71
|
+
release.py # dvdcompare release picker
|
|
72
|
+
selection.py # Title selection with classify_title
|
|
73
|
+
progress.py # Rip progress with makemkvcon
|
|
74
|
+
organize_preview.py # Organize plan preview
|
|
75
|
+
organize_done.py # Organize results summary
|
|
76
|
+
orchestrate_done.py # Orchestrate results summary
|
|
77
|
+
done.py # Rip results summary
|
|
78
|
+
update.py # Self-update screen
|
|
79
|
+
folder_picker.py # Folder browse dialog (tkinter)
|
|
59
80
|
tests/
|
|
60
|
-
fixtures/ # makemkvcon output samples
|
|
81
|
+
fixtures/ # makemkvcon output samples + disc JSON fixtures
|
|
61
82
|
snapshots/ # Serialized disc scan results for offline replay
|
|
62
83
|
test_*.py # One test file per module (test_matcher.py, test_disc_analysis.py, etc.)
|
|
63
84
|
docs/
|
|
64
85
|
architecture.md # System design, data flow diagrams
|
|
65
86
|
naming-rules.md # Plex naming conventions
|
|
66
87
|
changelog.md # Documentation changelog (Keep a Changelog format)
|
|
88
|
+
troubleshooting.md # Common problems and fixes
|
|
89
|
+
index.md # Docs landing page
|
|
67
90
|
getting-started/ # Installation, configuration
|
|
68
|
-
gui-guide/ # Desktop app
|
|
91
|
+
gui-guide/ # Desktop app walkthrough
|
|
69
92
|
cli-guide/ # Command-by-command workflow guides
|
|
70
93
|
reference/ # CLI reference
|
|
71
94
|
```
|
|
72
95
|
|
|
96
|
+
Note: business logic lives in the `riplex` library. `riplex/disc/` and
|
|
97
|
+
`riplex/metadata/` are proper subpackages (not flat `disc_analysis.py` /
|
|
98
|
+
`metadata_sources/`). The CLI's per-command logic lives in
|
|
99
|
+
`riplex_cli/commands/`. There is no `src/riplex/cli.py` or
|
|
100
|
+
`src/riplex/orchestrate.py`.
|
|
101
|
+
|
|
73
102
|
## Commands
|
|
74
103
|
|
|
75
104
|
| Command | Purpose |
|
|
@@ -110,8 +139,14 @@ Do NOT use `py -m riplex` (errors — riplex is a library package). Do NOT use `
|
|
|
110
139
|
|
|
111
140
|
- Python 3.11+ (use `py` command, never `python` or `python3`)
|
|
112
141
|
- Async: httpx for HTTP, asyncio.run() from sync CLI entry points
|
|
113
|
-
- Dependencies: httpx, dvdcompare-scraper, platformdirs
|
|
114
|
-
- GUI: Flet 0.
|
|
142
|
+
- Dependencies: certifi, httpx, dvdcompare-scraper, platformdirs
|
|
143
|
+
- GUI: Flet, pinned to `flet==0.85.1` in the `[gui]` extra. The pin is
|
|
144
|
+
intentional: the macOS desktop runtime in 0.85.3 ships a malformed nested
|
|
145
|
+
`objective_c.framework` that newer `codesign` rejects, which broke the macOS
|
|
146
|
+
release build and crashed the packaged GUI on launch (issue #21). Keep
|
|
147
|
+
`flet` and `flet-desktop` in lockstep at the same version.
|
|
148
|
+
- Versioning: `setuptools-scm` derives the version from the latest `v*` git
|
|
149
|
+
tag (no hardcoded version in `pyproject.toml`).
|
|
115
150
|
- External tools: makemkvcon, ffprobe, mkvmerge, mkvpropedit
|
|
116
151
|
|
|
117
152
|
## Debugging the GUI
|
|
@@ -167,12 +202,6 @@ Docs live in `docs/` and are referenced from README.md.
|
|
|
167
202
|
- `docs/cli-guide/`: Command-by-command workflow walkthroughs
|
|
168
203
|
- `docs/reference/cli.md`: Complete CLI option reference
|
|
169
204
|
|
|
170
|
-
*** Delete File: c:\Users\asher\Projects\anycredit5518\riplex\docs\guide\gui-walkthrough.md
|
|
171
|
-
*** Delete File: c:\Users\asher\Projects\anycredit5518\riplex\docs\guide\workflow.md
|
|
172
|
-
*** Delete File: c:\Users\asher\Projects\anycredit5518\riplex\docs\guide\orchestrate.md
|
|
173
|
-
*** Delete File: c:\Users\asher\Projects\anycredit5518\riplex\docs\guide\lookup.md
|
|
174
|
-
*** Delete File: c:\Users\asher\Projects\anycredit5518\riplex\docs\guide\organize.md
|
|
175
|
-
|
|
176
205
|
### Documentation rules
|
|
177
206
|
|
|
178
207
|
- When making significant changes (new features, renamed modules, changed behavior), update the relevant docs
|
|
@@ -197,11 +226,52 @@ Docs live in `docs/` and are referenced from README.md.
|
|
|
197
226
|
- Dry-run by default: never move or delete files without explicit `--execute`
|
|
198
227
|
- Config is shared between CLI and GUI (`~/.config/riplex/config.toml` or platform equivalent)
|
|
199
228
|
|
|
200
|
-
##
|
|
201
|
-
|
|
202
|
-
|
|
229
|
+
## Release workflow
|
|
230
|
+
|
|
231
|
+
Releases are built by `.github/workflows/release.yml`.
|
|
232
|
+
|
|
233
|
+
- Triggers: `push` of a `v*` tag, and manual `workflow_dispatch`.
|
|
234
|
+
- Jobs: `build-windows` (windows-latest), `build-macos` (macos-14, arm64),
|
|
235
|
+
and `release` (ubuntu-latest).
|
|
236
|
+
- The `release` job is gated on `if: startsWith(github.ref, 'refs/tags/v')`.
|
|
237
|
+
So a `workflow_dispatch` run on a branch or `main` runs ONLY the build jobs
|
|
238
|
+
and uploads artifacts — it does NOT create a user-facing release. This is
|
|
239
|
+
the safe way to validate packaging changes before tagging.
|
|
240
|
+
- To cut a real release: push an annotated `v*` tag on `main`. The tag
|
|
241
|
+
annotation becomes the release header; `setuptools-scm` picks up the
|
|
242
|
+
version from the tag.
|
|
243
|
+
- macOS GUI packaging: do NOT pass Flet's extracted `Flet.app` runtime to
|
|
244
|
+
PyInstaller. PyInstaller force-signs collected binaries on arm64 and
|
|
245
|
+
`codesign` rejects Flet's nested frameworks. Instead the workflow downloads
|
|
246
|
+
Flet's upstream `flet-macos.tar.gz` and bundles it under
|
|
247
|
+
`flet_desktop/app`; `flet_desktop.ensure_client_cached()` extracts it at
|
|
248
|
+
runtime. The macOS `.app` zip uses `zip -y` to preserve framework symlinks
|
|
249
|
+
(flattened symlinks crash the app on launch).
|
|
250
|
+
- Debugging CI: `gh run view <id> --json status,conclusion,jobs`; for logs of
|
|
251
|
+
a still-running run, fetch a finished job directly with
|
|
252
|
+
`gh api repos/<owner>/<repo>/actions/jobs/<jobId>/logs`
|
|
253
|
+
(the `--log`/`--log-failed` flags refuse until the whole run completes).
|
|
254
|
+
|
|
255
|
+
## Dependency and packaging cautions
|
|
256
|
+
|
|
257
|
+
- A Flet runtime regression can surface as a riplex GUI launch crash even
|
|
258
|
+
when riplex's own code is unchanged — check the Flet/PyInstaller packaging
|
|
259
|
+
path before assuming an app-code bug.
|
|
260
|
+
- `tkinter` (used by the GUI folder picker) is an OS-level dependency, not a
|
|
261
|
+
pip package: macOS Homebrew Python needs `brew install python-tk@3.12`.
|
|
262
|
+
- On Windows, close `riplex-ui` before upgrading deps — the entry-point
|
|
263
|
+
`.exe` shim is locked while the process runs and pip fails with `WinError 32`.
|
|
264
|
+
|
|
265
|
+
## Issue workflow
|
|
266
|
+
|
|
267
|
+
- When fixing a GitHub issue, reference it in the commit and/or tag message.
|
|
268
|
+
- After a tagged release ships, draft a user-facing comment that leads with
|
|
269
|
+
the reported symptom, gives a high-level cause and the fixed version, and
|
|
270
|
+
asks the reporter to retry (and reopen if still broken) before closing.
|
|
271
|
+
|
|
272
|
+
## Source packages
|
|
273
|
+
|
|
274
|
+
The repo contains three source packages:
|
|
203
275
|
- `riplex` — shared library (all business logic)
|
|
204
276
|
- `riplex_cli` — CLI thin wrapper
|
|
205
277
|
- `riplex_app` — Flet GUI
|
|
206
|
-
|
|
207
|
-
See `MONOREPO_PLAN.md` for background on the refactor.
|
|
@@ -105,3 +105,31 @@ All destructive commands (`rip`, `organize`, `orchestrate`) are dry-run by defau
|
|
|
105
105
|
## Testing
|
|
106
106
|
|
|
107
107
|
Run tests with `pytest` (or `python -m pytest`) from the project root with the venv active. All tests must pass before committing.
|
|
108
|
+
|
|
109
|
+
## Debugging the GUI
|
|
110
|
+
|
|
111
|
+
The GUI writes detailed logs to `riplex_app.log` in the working directory
|
|
112
|
+
(project root when launched from the repo). Check this file first when
|
|
113
|
+
debugging GUI behavior. Do NOT try to capture the GUI's terminal output with
|
|
114
|
+
a `2>&1` redirect — it breaks Flet's Flutter-Python IPC.
|
|
115
|
+
|
|
116
|
+
## Releases and packaging
|
|
117
|
+
|
|
118
|
+
Releases are produced by `.github/workflows/release.yml`.
|
|
119
|
+
|
|
120
|
+
- A GitHub Release is created ONLY when a `v*` tag is pushed. The `release`
|
|
121
|
+
job is gated on `refs/tags/v*`.
|
|
122
|
+
- `workflow_dispatch` on a branch or `main` runs only the build jobs and
|
|
123
|
+
uploads artifacts without publishing — use it to validate packaging changes
|
|
124
|
+
before tagging.
|
|
125
|
+
- To release: push an annotated `v*` tag on `main`. `setuptools-scm` derives
|
|
126
|
+
the version from that tag (there is no version string in `pyproject.toml`).
|
|
127
|
+
- macOS builds run on `macos-14` (arm64). The macOS GUI bundles Flet's
|
|
128
|
+
upstream `flet-macos.tar.gz` runtime archive (extracted at runtime) rather
|
|
129
|
+
than feeding the extracted `Flet.app` to PyInstaller, and zips the `.app`
|
|
130
|
+
with `zip -y` to preserve framework symlinks. Do not revert these without
|
|
131
|
+
understanding issue #21.
|
|
132
|
+
- Keep `flet` and `flet-desktop` pinned to the same version. A Flet runtime
|
|
133
|
+
regression can present as a GUI launch crash even when riplex's own code is
|
|
134
|
+
unchanged.
|
|
135
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
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
|
|
@@ -872,10 +872,16 @@ def detect_disc_number(
|
|
|
872
872
|
) -> int | None:
|
|
873
873
|
"""Auto-detect which dvdcompare disc number the physical disc corresponds to.
|
|
874
874
|
|
|
875
|
-
Tries three strategies:
|
|
875
|
+
Tries three strategies in order:
|
|
876
876
|
1. Parse the volume label for a disc number (e.g. "FROZEN_PLANET_II_D2" -> 2)
|
|
877
|
-
2.
|
|
878
|
-
3.
|
|
877
|
+
2. For multi-format releases, match by disc format/resolution (4K vs Blu-ray).
|
|
878
|
+
3. Match live title durations against each dvdcompare disc's episodes and extras.
|
|
879
|
+
|
|
880
|
+
Strategy 2 runs before duration matching because a movie released across
|
|
881
|
+
several format discs (4K + Blu-ray + 3D) carries the *same* main feature on
|
|
882
|
+
every disc, so its runtime cannot distinguish them — and duration matching
|
|
883
|
+
can actively pick the wrong disc when one disc lacks listed runtimes. The
|
|
884
|
+
resolution is the only reliable distinguishing signal in that case.
|
|
879
885
|
|
|
880
886
|
Returns the disc number (1-based) or None if detection fails.
|
|
881
887
|
"""
|
|
@@ -885,7 +891,13 @@ def detect_disc_number(
|
|
|
885
891
|
if match:
|
|
886
892
|
return int(match.group(1))
|
|
887
893
|
|
|
888
|
-
# Strategy 2:
|
|
894
|
+
# Strategy 2: format/resolution match (authoritative for multi-format
|
|
895
|
+
# movie releases, where every disc shares the same main feature runtime).
|
|
896
|
+
format_disc = _match_disc_by_format(disc_info, dvdcompare_discs)
|
|
897
|
+
if format_disc is not None:
|
|
898
|
+
return format_disc
|
|
899
|
+
|
|
900
|
+
# Strategy 3: duration matching against dvdcompare discs
|
|
889
901
|
if not dvdcompare_discs or not disc_info.titles:
|
|
890
902
|
return None
|
|
891
903
|
|
|
@@ -936,7 +948,20 @@ def detect_disc_number(
|
|
|
936
948
|
if best_score >= 0.5:
|
|
937
949
|
return best_disc
|
|
938
950
|
|
|
939
|
-
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _match_disc_by_format(disc_info, dvdcompare_discs: list) -> int | None:
|
|
955
|
+
"""Match the live disc to a dvdcompare disc by format/resolution.
|
|
956
|
+
|
|
957
|
+
Only returns a result when exactly one disc matches the live disc's
|
|
958
|
+
resolution, i.e. when the format is genuinely distinguishing (e.g. a single
|
|
959
|
+
4K disc among standard Blu-rays). Same-format multi-disc sets (TV box sets)
|
|
960
|
+
yield multiple candidates and fall through to duration matching.
|
|
961
|
+
"""
|
|
962
|
+
if not dvdcompare_discs:
|
|
963
|
+
return None
|
|
964
|
+
|
|
940
965
|
live_resolutions = {t.resolution for t in disc_info.titles if t.resolution}
|
|
941
966
|
has_4k = any("2160" in r for r in live_resolutions)
|
|
942
967
|
has_1080 = any("1080" in r for r in live_resolutions)
|
|
@@ -955,6 +980,7 @@ def detect_disc_number(
|
|
|
955
980
|
return None
|
|
956
981
|
|
|
957
982
|
|
|
983
|
+
|
|
958
984
|
# ---------------------------------------------------------------------------
|
|
959
985
|
# Disc content summary
|
|
960
986
|
# ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@ or passed directly to the constructor.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
|
+
import re
|
|
13
14
|
from typing import Literal
|
|
14
15
|
|
|
15
16
|
import httpx
|
|
@@ -28,6 +29,47 @@ TMDB_BASE_URL = "https://api.themoviedb.org/3"
|
|
|
28
29
|
_TMDB_TTL_DAYS = 7
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def _normalize_for_match(text: str) -> str:
|
|
33
|
+
"""Lowercase *text* and collapse runs of non-alphanumerics to spaces.
|
|
34
|
+
|
|
35
|
+
This makes punctuation-insensitive comparisons possible so that, e.g.,
|
|
36
|
+
``"TRON: Legacy"`` and ``"Tron Legacy"`` compare equal.
|
|
37
|
+
"""
|
|
38
|
+
return re.sub(r"[^a-z0-9]+", " ", text.lower()).strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _title_match_tier(query: str, title: str) -> int:
|
|
42
|
+
"""Rank how well *title* matches the search *query* (higher is better).
|
|
43
|
+
|
|
44
|
+
TMDb's ``/search/movie`` and ``/search/tv`` endpoints return loosely
|
|
45
|
+
fuzzy matches: a search for ``"Tron"`` surfaces wildly popular but
|
|
46
|
+
unrelated shows like ``House of the Dragon``. Sorting the merged list by
|
|
47
|
+
raw popularity therefore buries the title the user actually searched for.
|
|
48
|
+
This tier ranks literal title overlap ahead of popularity so genuine
|
|
49
|
+
matches win, with popularity only breaking ties within a tier.
|
|
50
|
+
|
|
51
|
+
Tiers (high to low):
|
|
52
|
+
4 - exact title match
|
|
53
|
+
3 - title starts with the query (e.g. ``"TRON: Legacy"`` for ``"tron"``)
|
|
54
|
+
2 - query appears as a whole word/phrase inside the title
|
|
55
|
+
1 - query is a substring of the title
|
|
56
|
+
0 - no literal overlap (TMDb fuzzy match)
|
|
57
|
+
"""
|
|
58
|
+
q = _normalize_for_match(query)
|
|
59
|
+
t = _normalize_for_match(title)
|
|
60
|
+
if not q or not t:
|
|
61
|
+
return 0
|
|
62
|
+
if t == q:
|
|
63
|
+
return 4
|
|
64
|
+
if t.startswith(q + " "):
|
|
65
|
+
return 3
|
|
66
|
+
if f" {q} " in f" {t} ":
|
|
67
|
+
return 2
|
|
68
|
+
if q in t:
|
|
69
|
+
return 1
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
31
73
|
def _looks_like_read_access_token(key: str) -> bool:
|
|
32
74
|
"""Return ``True`` when *key* looks like a TMDb v4 Read Access Token.
|
|
33
75
|
|
|
@@ -105,19 +147,77 @@ class TmdbProvider(MetadataProvider):
|
|
|
105
147
|
year: int | None = None,
|
|
106
148
|
media_type: Literal["movie", "tv", "auto"] = "auto",
|
|
107
149
|
) -> list[MetadataSearchResult]:
|
|
108
|
-
results: list[MetadataSearchResult]
|
|
109
|
-
|
|
110
|
-
if media_type in ("movie", "auto"):
|
|
111
|
-
results.extend(await self._search_movies(query, year))
|
|
150
|
+
results: list[MetadataSearchResult]
|
|
112
151
|
|
|
113
|
-
if media_type
|
|
114
|
-
|
|
152
|
+
if media_type == "auto" and year is None:
|
|
153
|
+
# No media-type or year constraint: use TMDb's /search/multi, the
|
|
154
|
+
# same endpoint the website's search box uses. It applies a
|
|
155
|
+
# server-side relevance score across movies and TV in one ranked
|
|
156
|
+
# list, which avoids the cross-endpoint merge problem (a single
|
|
157
|
+
# "movie" + "tv" merge has no shared score to order by).
|
|
158
|
+
results = await self._search_multi(query)
|
|
159
|
+
else:
|
|
160
|
+
# Explicit media type, or a year filter (which the dedicated
|
|
161
|
+
# endpoints support server-side but /search/multi does not).
|
|
162
|
+
results = []
|
|
163
|
+
if media_type in ("movie", "auto"):
|
|
164
|
+
results.extend(await self._search_movies(query, year))
|
|
165
|
+
if media_type in ("tv", "auto"):
|
|
166
|
+
results.extend(await self._search_tv(query, year))
|
|
115
167
|
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
168
|
+
# Rank by literal title-match quality first, then popularity. TMDb's
|
|
169
|
+
# results are fuzzy (a "Tron" search returns unrelated high-popularity
|
|
170
|
+
# shows), so this keeps the title the user searched for on top while
|
|
171
|
+
# still using popularity to order within a tier. Applying it uniformly
|
|
172
|
+
# also gives a deterministic, offline-testable final ordering.
|
|
173
|
+
results.sort(
|
|
174
|
+
key=lambda r: (_title_match_tier(query, r.title), r.popularity),
|
|
175
|
+
reverse=True,
|
|
176
|
+
)
|
|
119
177
|
return results
|
|
120
178
|
|
|
179
|
+
async def _search_multi(self, query: str) -> list[MetadataSearchResult]:
|
|
180
|
+
"""Search movies and TV in one ranked list via /search/multi.
|
|
181
|
+
|
|
182
|
+
/search/multi also returns ``person`` and ``collection`` entities,
|
|
183
|
+
which have no single runtime or release year to match a ripped MKV
|
|
184
|
+
against. Those are filtered out; only ``movie`` and ``tv`` survive.
|
|
185
|
+
"""
|
|
186
|
+
ck = cache.hash_key(f"multi|{query}")
|
|
187
|
+
data = await self._get_json("/search/multi", params={"query": query},
|
|
188
|
+
cache_ns="tmdb/search", cache_key=ck)
|
|
189
|
+
out: list[MetadataSearchResult] = []
|
|
190
|
+
for item in data.get("results", []):
|
|
191
|
+
mt = item.get("media_type")
|
|
192
|
+
if mt == "movie":
|
|
193
|
+
release = item.get("release_date", "") or ""
|
|
194
|
+
item_year = int(release[:4]) if len(release) >= 4 else None
|
|
195
|
+
out.append(
|
|
196
|
+
MetadataSearchResult(
|
|
197
|
+
source_id=f"movie:{item['id']}",
|
|
198
|
+
title=item.get("title", ""),
|
|
199
|
+
year=item_year,
|
|
200
|
+
media_type="movie",
|
|
201
|
+
overview=item.get("overview", ""),
|
|
202
|
+
popularity=item.get("popularity", 0.0),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
elif mt == "tv":
|
|
206
|
+
air_date = item.get("first_air_date", "") or ""
|
|
207
|
+
item_year = int(air_date[:4]) if len(air_date) >= 4 else None
|
|
208
|
+
out.append(
|
|
209
|
+
MetadataSearchResult(
|
|
210
|
+
source_id=f"tv:{item['id']}",
|
|
211
|
+
title=item.get("name", ""),
|
|
212
|
+
year=item_year,
|
|
213
|
+
media_type="tv",
|
|
214
|
+
overview=item.get("overview", ""),
|
|
215
|
+
popularity=item.get("popularity", 0.0),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
# person / collection entities are intentionally skipped.
|
|
219
|
+
return out
|
|
220
|
+
|
|
121
221
|
async def _search_movies(
|
|
122
222
|
self, query: str, year: int | None
|
|
123
223
|
) -> list[MetadataSearchResult]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
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
|
|
@@ -412,6 +412,65 @@ class TestDetectDiscNumber:
|
|
|
412
412
|
info = DiscInfo(disc_name="DISC", disc_type="Blu-ray disc", titles=titles)
|
|
413
413
|
assert _detect_disc_number(info, [disc1]) is None
|
|
414
414
|
|
|
415
|
+
def test_4k_disc_matched_by_format_not_duration(self):
|
|
416
|
+
"""A 4K movie disc whose feature has no listed runtime must still be
|
|
417
|
+
detected as the 4K disc via resolution — not mis-detected as the
|
|
418
|
+
Blu-ray disc that happens to list the same film's runtime.
|
|
419
|
+
|
|
420
|
+
Regression for the multi-format movie release case (issue: a 4K disc
|
|
421
|
+
was detected as disc 2 because disc 1's 4K feature had no runtime and
|
|
422
|
+
the inserted disc's film matched disc 2's listed 2D/3D runtimes).
|
|
423
|
+
"""
|
|
424
|
+
class FakeEp:
|
|
425
|
+
def __init__(self, runtime):
|
|
426
|
+
self.title = "The Film"
|
|
427
|
+
self.runtime_seconds = runtime
|
|
428
|
+
|
|
429
|
+
class FakeDisc:
|
|
430
|
+
def __init__(self, number, fmt, ep_runtimes):
|
|
431
|
+
self.number = number
|
|
432
|
+
self.disc_format = fmt
|
|
433
|
+
self.episodes = [FakeEp(rt) for rt in ep_runtimes]
|
|
434
|
+
self.extras = []
|
|
435
|
+
|
|
436
|
+
# Disc 1 = 4K disc, but dvdcompare lists no runtime for its feature.
|
|
437
|
+
disc1 = FakeDisc(1, "Blu-ray 4K", [])
|
|
438
|
+
# Disc 2 = 3D Blu-ray, lists the 2D + 3D film runtimes (~ same length).
|
|
439
|
+
disc2 = FakeDisc(2, "3D Blu-ray", [2880, 2880])
|
|
440
|
+
|
|
441
|
+
# Inserted disc is the 4K disc: its single film title is 2160p and its
|
|
442
|
+
# runtime matches disc 2's listed film runtime.
|
|
443
|
+
titles = [_make_title(0, 2885, resolution="3840x2160")]
|
|
444
|
+
info = DiscInfo(disc_name="THE_LAST_REEF", disc_type="Blu-ray disc", titles=titles)
|
|
445
|
+
assert _detect_disc_number(info, [disc1, disc2]) == 1
|
|
446
|
+
|
|
447
|
+
def test_format_match_skipped_for_same_format_set(self):
|
|
448
|
+
"""A multi-disc set where every disc is the same format must not be
|
|
449
|
+
resolved by format — it falls through to duration matching."""
|
|
450
|
+
class FakeEp:
|
|
451
|
+
def __init__(self, runtime):
|
|
452
|
+
self.title = "Ep"
|
|
453
|
+
self.runtime_seconds = runtime
|
|
454
|
+
|
|
455
|
+
class FakeDisc:
|
|
456
|
+
def __init__(self, number, runtimes):
|
|
457
|
+
self.number = number
|
|
458
|
+
self.disc_format = "Blu-ray 4K"
|
|
459
|
+
self.episodes = [FakeEp(rt) for rt in runtimes]
|
|
460
|
+
self.extras = []
|
|
461
|
+
|
|
462
|
+
# Both discs are 4K, so resolution can't distinguish them.
|
|
463
|
+
disc1 = FakeDisc(1, [3000, 3100, 3050])
|
|
464
|
+
disc2 = FakeDisc(2, [2800, 2900, 2950])
|
|
465
|
+
titles = [
|
|
466
|
+
_make_title(0, 2810, resolution="3840x2160"),
|
|
467
|
+
_make_title(1, 2910, resolution="3840x2160"),
|
|
468
|
+
_make_title(2, 2960, resolution="3840x2160"),
|
|
469
|
+
]
|
|
470
|
+
info = DiscInfo(disc_name="MYSTERY_DISC", disc_type="Blu-ray disc", titles=titles)
|
|
471
|
+
assert _detect_disc_number(info, [disc1, disc2]) == 2
|
|
472
|
+
|
|
473
|
+
|
|
415
474
|
|
|
416
475
|
class TestAnalyzeDisc:
|
|
417
476
|
"""Tests for the analyze_disc() shared entry point."""
|