riplex 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/release.yml +3 -10
- {riplex-1.0.2/src/riplex.egg-info → riplex-1.0.4}/PKG-INFO +2 -2
- {riplex-1.0.2 → riplex-1.0.4}/RELEASE_NOTES_v1.0.0.md +21 -29
- {riplex-1.0.2 → riplex-1.0.4}/docs/changelog.md +3 -0
- riplex-1.0.4/issues/thediscdb-metadata-source.md +77 -0
- {riplex-1.0.2 → riplex-1.0.4}/pyproject.toml +4 -4
- riplex-1.0.4/screenshots/v1.0.0_Disc_Overview.png +0 -0
- riplex-1.0.4/screenshots/v1.0.0_In_Place_Update.png +0 -0
- riplex-1.0.4/screenshots/v1.0.0_Multi_Season_Select.png +0 -0
- riplex-1.0.4/screenshots/v1.0.0_Organize_Preview.png +0 -0
- riplex-1.0.4/scripts/gui_screenshot.py +296 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/config.py +16 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/analysis.py +266 -6
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/makemkv.py +3 -1
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/provider.py +12 -1
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/manifest.py +83 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/matcher.py +81 -1
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/title.py +3 -3
- {riplex-1.0.2 → riplex-1.0.4/src/riplex.egg-info}/PKG-INFO +2 -2
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/SOURCES.txt +14 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/requires.txt +1 -1
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/scm_file_list.json +191 -177
- riplex-1.0.4/src/riplex.egg-info/scm_version.json +8 -0
- riplex-1.0.4/src/riplex_app/assets/riplex-icon.icns +0 -0
- riplex-1.0.4/src/riplex_app/assets/riplex-icon.ico +0 -0
- riplex-1.0.4/src/riplex_app/assets/riplex-icon.png +0 -0
- riplex-1.0.4/src/riplex_app/assets/riplex-icon.svg +21 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/main.py +11 -9
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_detection.py +55 -13
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/organize_preview.py +40 -8
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/progress.py +3 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/selection.py +108 -18
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/welcome.py +2 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/orchestrate.py +35 -1
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_screen_build_smoke.py +2 -53
- riplex-1.0.4/tests/support/seed.py +69 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_app_main.py +14 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_cli_utils.py +16 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_config.py +26 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_analysis.py +11 -0
- riplex-1.0.4/tests/test_disc_assessment.py +70 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_detection_screen.py +43 -0
- riplex-1.0.4/tests/test_episode_carryover.py +137 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_matcher.py +91 -0
- riplex-1.0.4/tests/test_organize_preview.py +29 -0
- riplex-1.0.2/src/riplex.egg-info/scm_version.json +0 -8
- {riplex-1.0.2 → riplex-1.0.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/agents/riplex.agent.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/contributors-overrides.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/copilot-instructions.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/publish.yml +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/update-contributors.yml +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.gitignore +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/.vscode/settings.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/CONTRIBUTORS.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/LICENSE +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/README.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/architecture.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/lookup.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/orchestrate.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/organize.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/workflow.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/development/testing.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/getting-started/configuration.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/getting-started/installation.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/gui-guide/gui-walkthrough.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/index.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/naming-rules.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/reference/cli.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/docs/troubleshooting.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/one-season-at-a-time.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/organize-preview-match-override.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/issues/planned-features.md +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/mkdocs.yml +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/1_Welcome_Screen.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/2_Disc_Detection_BTTF.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/4_Disc_Release_BTTF.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/scripts/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/scripts/gen_gui_fixtures.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/scripts/update_contributors.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/setup.cfg +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/cache.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/dedup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/detect.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/formatter.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/lookup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/autosearch.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/planner.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/provider.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/models.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/normalize.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/organize_by_group.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/organizer.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/resume.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/scanner.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/snapshot.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/splitter.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/tagger.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/ui.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex/updater.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/bug_report.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/crash_dump.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/keep_awake.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/done.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/release.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/season_select.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/update.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/formatting.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/main.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/conftest.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/2001-a-space-odyssey-1968.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/akira-1988.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-1985.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-part-ii-1989.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-part-iii-1990.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/blade-1998.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/blade-runner-2049-2017.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/chernobyl-2019.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/d-day-normandy-1944-2014.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/final-destination-bloodlines-2025.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/flight-of-the-butterflies-2012.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/inception-2010.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/independence-day-1996.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/journey-to-space-2015.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/journey-to-the-south-pacific-2013.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/jurassic-park-1993.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/king-kong-2005.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/mad-max-fury-road-2015.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/march-of-the-penguins-2005.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/national-parks-adventure-2016.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/oceans-our-blue-planet-2018.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-01.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-03.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-04.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-the-movie-2017.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/space-station-3d-2002.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-godfather-1972.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-last-reef-cities-beneath-the-sea-2012.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-matrix-1999.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-matrix-reloaded-2003.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-1999.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-returns-2001.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-tomb-of-the-dragon-emperor-2008.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-patriot-2000.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-thing-1982.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/wonders-of-the-arctic-2014.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/conftest.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_lookup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_organize.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_parser.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_fixture_scenarios.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_errors.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_media_types.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_movie_orchestrate.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_organize.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_resume_prefill.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_tv_orchestrate.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_welcome.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/__init__.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/cli.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/driver.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/fake_page.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/fixtures.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/provider_mocks.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/support/sync_runtime.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_autosearch.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_cache.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_dedup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_detect.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_fixtures.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_provider.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_folder_picker.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_formatter.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_lookup.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_lookup_resolve_groups.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_makemkv.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_manifest_prefill_ids.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_manifest_session.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_metadata_screen.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_normalize.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_by_group.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_cli.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_session_fanout.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_organizer.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_planner.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_progress.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_release_screen.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_resume.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_rip_guide.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_scanner.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_season_select_screen.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_snapshot.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_splitter.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_tagger.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_tmdb_provider.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_ui.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_update_contributors.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_updater.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_updater_selfupdate.py +0 -0
- {riplex-1.0.2 → riplex-1.0.4}/tests/test_welcome_screen.py +0 -0
|
@@ -50,6 +50,7 @@ jobs:
|
|
|
50
50
|
run: |
|
|
51
51
|
for /f "delims=" %%i in ('python -c "import flet_desktop; print(flet_desktop.ensure_client_cached())"') do set FLET_CLIENT_CACHE=%%i
|
|
52
52
|
pyinstaller --name riplex-ui --onefile --windowed ^
|
|
53
|
+
--icon src/riplex_app/assets/riplex-icon.ico ^
|
|
53
54
|
--hidden-import riplex_app.screens.welcome ^
|
|
54
55
|
--hidden-import riplex_app.screens.disc_detection ^
|
|
55
56
|
--hidden-import riplex_app.screens.metadata ^
|
|
@@ -69,6 +70,7 @@ jobs:
|
|
|
69
70
|
--collect-all flet_desktop ^
|
|
70
71
|
--collect-data certifi ^
|
|
71
72
|
--collect-data dvdcompare_scraper ^
|
|
73
|
+
--add-data "src/riplex_app/assets/riplex-icon.ico;riplex_app/assets" ^
|
|
72
74
|
--add-data "%FLET_CLIENT_CACHE%\flet;flet_client" ^
|
|
73
75
|
src/riplex_app/main.py
|
|
74
76
|
shell: cmd
|
|
@@ -128,6 +130,7 @@ jobs:
|
|
|
128
130
|
- name: Build GUI app bundle
|
|
129
131
|
run: |
|
|
130
132
|
pyinstaller --name riplex-ui --onedir --windowed \
|
|
133
|
+
--icon src/riplex_app/assets/riplex-icon.icns \
|
|
131
134
|
--hidden-import riplex_app.screens.welcome \
|
|
132
135
|
--hidden-import riplex_app.screens.disc_detection \
|
|
133
136
|
--hidden-import riplex_app.screens.metadata \
|
|
@@ -192,12 +195,6 @@ jobs:
|
|
|
192
195
|
cp artifacts/riplex-windows/riplex-ui.exe release/riplex-ui-windows.exe
|
|
193
196
|
cp artifacts/riplex-macos/riplex-macos release/
|
|
194
197
|
cp artifacts/riplex-macos/riplex-ui-macos.zip release/
|
|
195
|
-
# Publish SHA-256 checksums so the app can verify a download before
|
|
196
|
-
# applying an in-place update (see riplex.updater.stage_update).
|
|
197
|
-
cd release
|
|
198
|
-
for f in riplex-windows.exe riplex-ui-windows.exe riplex-macos riplex-ui-macos.zip; do
|
|
199
|
-
sha256sum "$f" > "$f.sha256"
|
|
200
|
-
done
|
|
201
198
|
|
|
202
199
|
- name: Create GitHub Release
|
|
203
200
|
uses: softprops/action-gh-release@v2
|
|
@@ -210,10 +207,6 @@ jobs:
|
|
|
210
207
|
body: ${{ steps.tag_notes.outputs.body }}
|
|
211
208
|
files: |
|
|
212
209
|
release/riplex-windows.exe
|
|
213
|
-
release/riplex-windows.exe.sha256
|
|
214
210
|
release/riplex-ui-windows.exe
|
|
215
|
-
release/riplex-ui-windows.exe.sha256
|
|
216
211
|
release/riplex-macos
|
|
217
|
-
release/riplex-macos.sha256
|
|
218
212
|
release/riplex-ui-macos.zip
|
|
219
|
-
release/riplex-ui-macos.zip.sha256
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -8,7 +8,7 @@ Description-Content-Type: text/markdown
|
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: certifi>=2024.2.2
|
|
10
10
|
Requires-Dist: httpx>=0.27
|
|
11
|
-
Requires-Dist: dvdcompare-scraper>=0.1.
|
|
11
|
+
Requires-Dist: dvdcompare-scraper>=0.1.15
|
|
12
12
|
Requires-Dist: platformdirs>=4.0
|
|
13
13
|
Provides-Extra: dev
|
|
14
14
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -5,32 +5,12 @@ support, hardens episode matching, adds a comprehensive automated test suite so
|
|
|
5
5
|
the flows that matter stay working as the project grows, and makes upgrades
|
|
6
6
|
painless with one-click in-place updates on Windows.
|
|
7
7
|
|
|
8
|
-
Until now every disc was treated as a standalone rip, and updating meant
|
|
9
|
-
re-downloading the app each release. This release lets riplex understand a whole
|
|
10
|
-
box set at once — detecting the *works* inside a release, grouping the discs that
|
|
11
|
-
belong to each, and routing every work to its correct Plex destination in a
|
|
12
|
-
single guided session — and then keeps itself up to date automatically.
|
|
13
|
-
|
|
14
|
-
## Why 1.0.0?
|
|
15
|
-
|
|
16
|
-
riplex has been usable for a while; three things make this the right moment to
|
|
17
|
-
call it stable:
|
|
18
|
-
|
|
19
|
-
- **The hard workflows are done.** Multi-work box sets and complete multi-season
|
|
20
|
-
TV series — the cases that used to mean ripping disc-by-disc and sorting by
|
|
21
|
-
hand — now work end to end, in both the desktop app and the CLI.
|
|
22
|
-
- **It's protected against regressions.** A headless integration suite exercises
|
|
23
|
-
the entire GUI wizard and the CLI against real-world scenarios generated from
|
|
24
|
-
archived rips, so the flows you depend on keep working as development continues.
|
|
25
|
-
- **Updates are effortless.** In-place updates mean you install once and never
|
|
26
|
-
have to re-download and re-approve the app for each new version.
|
|
27
|
-
|
|
28
8
|
## Headline features
|
|
29
9
|
|
|
30
10
|
### Multi-work box sets
|
|
31
11
|
|
|
32
|
-
A release that contains several distinct works
|
|
33
|
-
TV series
|
|
12
|
+
A release that contains several distinct works - several films, or films plus a
|
|
13
|
+
TV series - is detected as *multiple works* instead of one. Each work gets its
|
|
34
14
|
own title selection, its own rip grouping, and its own Plex target, so a mixed
|
|
35
15
|
box set is ripped and filed correctly in one pass:
|
|
36
16
|
|
|
@@ -38,6 +18,9 @@ box set is ripped and filed correctly in one pass:
|
|
|
38
18
|
• The Select Titles screen shows each work's rip output separately, so you can see exactly which files belong to which movie or show.
|
|
39
19
|
• Full CLI parity: `riplex orchestrate` and `riplex organize` route multi-work releases the same way the GUI does, through the shared flow logic.
|
|
40
20
|
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
41
24
|
### Multi-season TV series
|
|
42
25
|
|
|
43
26
|
Complete-series sets that span several seasons are ripped season by season:
|
|
@@ -46,12 +29,15 @@ Complete-series sets that span several seasons are ripped season by season:
|
|
|
46
29
|
• TV rips nest automatically under `Season NN/`, and specials land in `Season 00`.
|
|
47
30
|
• Season labels and chips appear throughout the Disc Overview, so a 12-disc set is easy to keep track of.
|
|
48
31
|
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
49
35
|
### Guided resume across a whole set
|
|
50
36
|
|
|
51
37
|
Every session writes a `_riplex_session.json` marker capturing the full plan, so
|
|
52
38
|
you can rip a few discs, come back later, and pick up exactly where you left off:
|
|
53
39
|
|
|
54
|
-
• Resume from *any* disc of a multi-work or multi-season set
|
|
40
|
+
• Resume from *any* disc of a multi-work or multi-season set - not just the next in sequence.
|
|
55
41
|
• `riplex orchestrate` (CLI) resumes from the marker with GUI parity, via a shared `resume.py` adapter.
|
|
56
42
|
• `riplex organize` discovers the marker and fans out across every work in the set.
|
|
57
43
|
|
|
@@ -59,13 +45,13 @@ you can rip a few discs, come back later, and pick up exactly where you left off
|
|
|
59
45
|
|
|
60
46
|
Rip manifests record the exact TMDb and dvdcompare ids you confirmed at rip time.
|
|
61
47
|
When you organize (or resume) a ripped disc, riplex reuses those ids and skips
|
|
62
|
-
the metadata pickers entirely
|
|
48
|
+
the metadata pickers entirely - no re-searching, no risk of picking a different
|
|
63
49
|
release the second time.
|
|
64
50
|
|
|
65
51
|
### Smarter, more resilient TV episode matching
|
|
66
52
|
|
|
67
53
|
Matching ripped titles to episodes used to be a runtime guessing game that falls
|
|
68
|
-
apart when episodes run within seconds of each other
|
|
54
|
+
apart when episodes run within seconds of each other - or when a metadata
|
|
69
55
|
listing is simply wrong. Now:
|
|
70
56
|
|
|
71
57
|
• dvdcompare's episode listing is cross-referenced against the TMDb episode list.
|
|
@@ -73,11 +59,14 @@ listing is simply wrong. Now:
|
|
|
73
59
|
• Organize honors the rip-time season/episode classification and no longer collides identically-named files across different discs.
|
|
74
60
|
• Over-length titles are labeled **Unmatched content** and stay visible instead of being forced into the wrong slot.
|
|
75
61
|
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
76
65
|
### Seamless in-place updates (Windows)
|
|
77
66
|
|
|
78
67
|
When a new version is available, the update screen offers **Update & Restart**:
|
|
79
68
|
riplex downloads the new build, **verifies its SHA-256 checksum**, swaps the
|
|
80
|
-
running `.exe` in place, and relaunches
|
|
69
|
+
running `.exe` in place, and relaunches - no re-downloading and no repeated
|
|
81
70
|
SmartScreen approval. It only runs from a writable install folder (otherwise it
|
|
82
71
|
falls back to the browser download), and every release now publishes `.sha256`
|
|
83
72
|
checksums for its assets.
|
|
@@ -86,10 +75,13 @@ checksums for its assets.
|
|
|
86
75
|
> still uses the browser download (the old build doesn't know how to self-update);
|
|
87
76
|
> from v1.0.0 onward, updates are one click.
|
|
88
77
|
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
89
81
|
### Auto-eject after ripping
|
|
90
82
|
|
|
91
|
-
The disc ejects automatically once a rip finishes, so you can swap discs
|
|
92
|
-
just know it's done
|
|
83
|
+
The disc ejects automatically once a rip finishes, so you can swap discs - or
|
|
84
|
+
just know it's done - without reaching for the drive. On by default; set
|
|
93
85
|
`auto_eject = false` in the config to disable.
|
|
94
86
|
|
|
95
87
|
## Reliability & testing
|
|
@@ -130,7 +122,7 @@ break the flows you rely on:
|
|
|
130
122
|
|
|
131
123
|
Disc-group ids changed from the old `main_1` / `film_31` scheme to
|
|
132
124
|
`disc_1` / `discs_1_4` / `disc_31`. A session saved by an earlier version will
|
|
133
|
-
not resume
|
|
125
|
+
not resume - **start a fresh session after upgrading**.
|
|
134
126
|
|
|
135
127
|
## Full changelog
|
|
136
128
|
|
|
@@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
10
10
|
|
|
11
11
|
- **TV episodes with parenthetical titles (or filed under extras) no longer land in `Other/`.** When organizing a TV rip, the destination is now chosen from the authoritative `SxxEyy` classification recorded in each disc's rip manifest at rip time, taking priority over the re-derived dvdcompare match label. Previously an episode whose title contains a parenthetical (e.g. *Shawn and Gus in Drag (Racing)*), or one that dvdcompare files under a disc's extras (e.g. an *(Extended Version)* listing such as *Romeo and Juliet and Juliet* or *Dual Spires*), was misread as an extra and dropped into `Other/` instead of its season folder — even though orchestrate had already classified it correctly (e.g. Psych S5 E01/E05/E12).
|
|
12
12
|
|
|
13
|
+
- **makemkvcon drive-listing timeout is now configurable (default raised to 120s).** `makemkvcon -r info list` previously timed out after a fixed 60 seconds; on some setups this fired even though the MakeMKV GUI worked fine. Raise `makemkv_list_timeout` in the config if a slow drive still times out. (#23)
|
|
14
|
+
- **Disc and season detection for compact `SxDy` volume labels.** A label that glues the disc digit to the season token and trails a provider code — e.g. `EXPANSE_S3D1_UPB75` — now resolves to the correct title (*Expanse*), season (3) and disc (1). Previously the season was lost and disc detection fell through to duration matching, which could pick the wrong disc. (#18)
|
|
15
|
+
|
|
13
16
|
### Added
|
|
14
17
|
|
|
15
18
|
- **Durations on the organize preview.** Each matched file now shows its runtime next to the matched episode/target's expected runtime; a divergence of more than a minute is highlighted so an incorrect match is easy to spot before you execute.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Metadata source: thediscdb.com (per-disc file/extra data)
|
|
2
|
+
|
|
3
|
+
Tracking: GitHub issue #19.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
dvdcompare.net is riplex's only structured disc source. It lists a disc's
|
|
8
|
+
episodes and extras with runtimes, but it does **not** map those to the disc's
|
|
9
|
+
actual title files, and its extra typing is coarse. In practice that means:
|
|
10
|
+
|
|
11
|
+
- Extras frequently all collapse into `Featurettes/`, and some extras aren't
|
|
12
|
+
detected at all (reported in #19).
|
|
13
|
+
- Matching relies on runtime heuristics, which is fragile when several
|
|
14
|
+
features run within seconds of each other.
|
|
15
|
+
|
|
16
|
+
[thediscdb.com](https://thediscdb.com) publishes, per physical release, the
|
|
17
|
+
**exact source file names** on each disc mapped to their content **titles** and
|
|
18
|
+
a proper **type** (episode, deleted scene, featurette, trailer, commentary,
|
|
19
|
+
etc.). Example: the Fight Club 2026 4K steelbook Blu-ray
|
|
20
|
+
(<https://thediscdb.com/movie/fight-club-1999/releases/2026-steelbook-4k/discs/blu-ray>).
|
|
21
|
+
|
|
22
|
+
Crucially, the data is open on GitHub — <https://github.com/TheDiscDb/data> —
|
|
23
|
+
as structured files, so it can be consumed without scraping HTML.
|
|
24
|
+
|
|
25
|
+
## Why this is a good fit
|
|
26
|
+
|
|
27
|
+
riplex already has a clean provider seam: dvdcompare is wrapped behind
|
|
28
|
+
`src/riplex/disc/provider.py` producing `PlannedDisc` / `PlannedEpisode` /
|
|
29
|
+
`PlannedExtra` objects, and the organizer/matcher consume those. A second
|
|
30
|
+
source that yields the same shapes can slot in behind the same seam.
|
|
31
|
+
|
|
32
|
+
If TheDiscDb maps titles to *source file names*, it can also make matching
|
|
33
|
+
**deterministic** (map ripped MKVs by playlist / file identity) instead of
|
|
34
|
+
runtime-based, and give each extra a correct Plex folder from its declared
|
|
35
|
+
type — directly addressing the "everything becomes Featurettes" complaint.
|
|
36
|
+
|
|
37
|
+
## Proposed approach
|
|
38
|
+
|
|
39
|
+
1. **Data access.** Prefer the GitHub `TheDiscDb/data` repo over live HTML.
|
|
40
|
+
Options: (a) fetch the specific release file on demand via the GitHub raw
|
|
41
|
+
API and cache it (mirrors the existing dvdcompare cache in `cache.py`), or
|
|
42
|
+
(b) let the user point at a local clone. Start with on-demand + cache.
|
|
43
|
+
2. **New provider.** Add `src/riplex/disc/thediscdb.py` exposing the same
|
|
44
|
+
lookup surface as the dvdcompare provider, returning `PlannedDisc`/
|
|
45
|
+
`PlannedEpisode`/`PlannedExtra` (extend `PlannedExtra.feature_type` mapping
|
|
46
|
+
so TheDiscDb's types resolve to Plex extras folders via
|
|
47
|
+
`organizer._EXTRAS_FOLDER_MAP`).
|
|
48
|
+
3. **Selection / precedence.** When both sources match a release, prefer
|
|
49
|
+
TheDiscDb for extras typing + file mapping, fall back to dvdcompare for
|
|
50
|
+
runtimes it lacks. Needs a release-matching step (title + year + edition).
|
|
51
|
+
4. **Deterministic matching (stretch).** When TheDiscDb provides source file
|
|
52
|
+
names, match ripped files by name/playlist identity in the matcher before
|
|
53
|
+
the runtime pass.
|
|
54
|
+
|
|
55
|
+
## Scope / decisions to settle
|
|
56
|
+
|
|
57
|
+
- **Release identification.** dvdcompare and TheDiscDb slug releases
|
|
58
|
+
differently; we need a mapping/search step (title+year, then edition). How to
|
|
59
|
+
disambiguate multiple releases of the same film is the main design question.
|
|
60
|
+
- **Coverage.** TheDiscDb is community-maintained and sparser than dvdcompare
|
|
61
|
+
for TV. Treat it as an *augmenting* source, not a replacement — keep
|
|
62
|
+
dvdcompare as the default and layer TheDiscDb when a release is found.
|
|
63
|
+
- **Data schema.** Pin to the `TheDiscDb/data` file format; add a small parser
|
|
64
|
+
+ fixtures. Version/format drift should degrade gracefully (fall back to
|
|
65
|
+
dvdcompare), mirroring how the dvdcompare cache invalidates on scraper
|
|
66
|
+
version change.
|
|
67
|
+
- **Config.** A `thediscdb_enabled` (or source-priority) config key, off or
|
|
68
|
+
best-effort by default until coverage is proven.
|
|
69
|
+
- **Testing.** Unit-test the parser against committed TheDiscDb fixtures;
|
|
70
|
+
extend the matcher/organizer tests to cover file-name-based matching and
|
|
71
|
+
richer extra typing.
|
|
72
|
+
|
|
73
|
+
## Related
|
|
74
|
+
|
|
75
|
+
- Provider seam: `src/riplex/disc/provider.py`, `cache.py` (caching pattern).
|
|
76
|
+
- Extras typing: `organizer._EXTRAS_FOLDER_MAP` / `_extras_folder`.
|
|
77
|
+
- The "everything → Featurettes" symptom this would improve is the core of #19.
|
|
@@ -12,7 +12,7 @@ license = {text = "MIT"}
|
|
|
12
12
|
dependencies = [
|
|
13
13
|
"certifi>=2024.2.2",
|
|
14
14
|
"httpx>=0.27",
|
|
15
|
-
"dvdcompare-scraper>=0.1.
|
|
15
|
+
"dvdcompare-scraper>=0.1.15",
|
|
16
16
|
"platformdirs>=4.0",
|
|
17
17
|
]
|
|
18
18
|
|
|
@@ -53,12 +53,12 @@ riplex-ui = "riplex_app.main:main"
|
|
|
53
53
|
[tool.setuptools.packages.find]
|
|
54
54
|
where = ["src"]
|
|
55
55
|
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
riplex_app = ["assets/*"]
|
|
58
|
+
|
|
56
59
|
[tool.setuptools_scm]
|
|
57
60
|
version_scheme = "guess-next-dev"
|
|
58
61
|
local_scheme = "no-local-version"
|
|
59
62
|
|
|
60
63
|
[tool.pytest.ini_options]
|
|
61
64
|
testpaths = ["tests"]
|
|
62
|
-
markers = [
|
|
63
|
-
"integration: end-to-end GUI flow tests that drive the wizard headlessly",
|
|
64
|
-
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Headless screen-screenshot launcher for release notes / docs.
|
|
2
|
+
|
|
3
|
+
Renders any riplex-ui screen, seeded with fixture data, in Flet's *web* mode and
|
|
4
|
+
captures a PNG with headless Playwright chromium -- no window ever appears.
|
|
5
|
+
|
|
6
|
+
How it works:
|
|
7
|
+
* ``--_serve`` (internal) exports the Flet app as an ASGI app and serves it
|
|
8
|
+
with uvicorn on a port, with ``app.state`` seeded for one screen.
|
|
9
|
+
* The default (orchestrator) mode spawns that server as a subprocess, waits
|
|
10
|
+
for the port, screenshots ``http://127.0.0.1:<port>`` with Playwright, and
|
|
11
|
+
tears the server down.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python scripts/gui_screenshot.py --list
|
|
15
|
+
python scripts/gui_screenshot.py --screen organize_preview --out screenshots
|
|
16
|
+
python scripts/gui_screenshot.py --all --out screenshots
|
|
17
|
+
|
|
18
|
+
Requires the dev + gui extras plus ``playwright`` and a chromium download
|
|
19
|
+
(``python -m playwright install chromium``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import socket
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import tempfile
|
|
29
|
+
import time
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
33
|
+
if str(_REPO_ROOT) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(_REPO_ROOT))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Per-screen state overrides — the fixtures don't carry multi-work / multi-
|
|
39
|
+
# season / built-plan state, so synthesize just enough for a faithful shot.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def _override_update(app, scenario, tmp) -> None:
|
|
43
|
+
app.state["update_info"] = {
|
|
44
|
+
"tag": "v1.0.2",
|
|
45
|
+
"name": "v1.0.2",
|
|
46
|
+
"url": "https://github.com/AnyCredit5518/riplex/releases/tag/v1.0.2",
|
|
47
|
+
"releases": [
|
|
48
|
+
{
|
|
49
|
+
"tag": "v1.0.2",
|
|
50
|
+
"body": (
|
|
51
|
+
"### Fixed\n\n"
|
|
52
|
+
"- **Episodes no longer land in `Other/`.** Organize now honors the "
|
|
53
|
+
"rip-time `SxxEyy` classification recorded in each disc's manifest, so "
|
|
54
|
+
"an episode whose title has a parenthetical (*Shawn and Gus in Drag "
|
|
55
|
+
"(Racing)*) or that dvdcompare files under extras (*Dual Spires*) "
|
|
56
|
+
"routes to its season folder.\n\n"
|
|
57
|
+
"### Added\n\n"
|
|
58
|
+
"- **Durations on the organize preview** highlight a mismatched match "
|
|
59
|
+
"before you execute."
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"tag": "v1.0.1",
|
|
64
|
+
"body": (
|
|
65
|
+
"### Fixed\n\n"
|
|
66
|
+
"- **Out-of-order episode matching.** Episodes are sorted by their "
|
|
67
|
+
"canonical `SxxEyy` number before positional alignment, so a disc "
|
|
68
|
+
"whose titles are in broadcast order maps correctly."
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _override_season_select(app, scenario, tmp) -> None:
|
|
76
|
+
"""Give the show a full 8-season run so the season picker renders."""
|
|
77
|
+
from riplex.metadata.provider import EpisodeMetadata, SeasonMetadata, ShowDetail
|
|
78
|
+
|
|
79
|
+
seasons = []
|
|
80
|
+
for sn in range(1, 9):
|
|
81
|
+
eps = [
|
|
82
|
+
EpisodeMetadata(season_number=sn, episode_number=en,
|
|
83
|
+
title=f"Episode {en}", runtime_seconds=2580)
|
|
84
|
+
for en in range(1, 17)
|
|
85
|
+
]
|
|
86
|
+
seasons.append(SeasonMetadata(season_number=sn, episodes=eps, name=f"Season {sn}"))
|
|
87
|
+
app.state["show_detail"] = ShowDetail(
|
|
88
|
+
source_id="tv:1447", title="Psych", year=2006, seasons=seasons,
|
|
89
|
+
)
|
|
90
|
+
# Force the picker to render (not auto-skip).
|
|
91
|
+
app.state["season_number"] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _override_organize_preview(app, scenario, tmp) -> None:
|
|
95
|
+
"""Seed a fully-built Psych S5 organize plan (episodes + an extra)."""
|
|
96
|
+
from riplex.organizer import OrganizePlan, FileMove
|
|
97
|
+
from riplex.models import PlannedShow, PlannedSeason, PlannedEpisode
|
|
98
|
+
|
|
99
|
+
titles = {
|
|
100
|
+
1: ("Romeo and Juliet and Juliet", 2894, 2898),
|
|
101
|
+
2: ("Feet Don't Kill Me Now", 2577, 2580),
|
|
102
|
+
3: ("Not Even Close... Encounters", 2577, 2580),
|
|
103
|
+
4: ("Chivalry Is Not Dead... But Someone Is", 2581, 2585),
|
|
104
|
+
5: ("Shawn and Gus in Drag (Racket)", 2585, 2590),
|
|
105
|
+
6: ("Viagra Falls", 2499, 2520),
|
|
106
|
+
7: ("Ferry Tale", 2530, 2540),
|
|
107
|
+
8: ("Shawn 2.0", 2578, 2585),
|
|
108
|
+
}
|
|
109
|
+
base = Path("E:/Media/TV Shows/Psych (2006)")
|
|
110
|
+
moves = []
|
|
111
|
+
for ep, (name, file_dur, tgt) in titles.items():
|
|
112
|
+
disc = 1 if ep <= 4 else 2
|
|
113
|
+
idx = (ep - 1) % 4
|
|
114
|
+
delta = abs(file_dur - tgt)
|
|
115
|
+
conf = "high" if delta <= 30 else ("medium" if delta <= 120 else "low")
|
|
116
|
+
moves.append(FileMove(
|
|
117
|
+
source=str(base.parent / f"_MakeMKV/Psych/Disc {disc}/C{idx}_t0{idx}.mkv"),
|
|
118
|
+
destination=str(base / "Season 05" / f"Psych (2006) - s05e{ep:02d} - {name}.mkv"),
|
|
119
|
+
label=f"Disc {disc}: {name}",
|
|
120
|
+
confidence=conf,
|
|
121
|
+
delta_seconds=delta,
|
|
122
|
+
file_duration_seconds=file_dur,
|
|
123
|
+
target_runtime_seconds=tgt,
|
|
124
|
+
))
|
|
125
|
+
plan = OrganizePlan(
|
|
126
|
+
moves=moves,
|
|
127
|
+
splits=[],
|
|
128
|
+
unmatched=[],
|
|
129
|
+
missing=["Disc 1: No Comment: Behind the Scenes (featurette)"],
|
|
130
|
+
)
|
|
131
|
+
planned = PlannedShow(
|
|
132
|
+
canonical_title="Psych", year=2006,
|
|
133
|
+
seasons=[PlannedSeason(
|
|
134
|
+
season_number=5,
|
|
135
|
+
episodes=[PlannedEpisode(season_number=5, episode_number=ep,
|
|
136
|
+
title=name, runtime="43m")
|
|
137
|
+
for ep, (name, _, _) in titles.items()],
|
|
138
|
+
)],
|
|
139
|
+
)
|
|
140
|
+
app.state["_organize_plan"] = (plan, planned)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_OVERRIDES = {
|
|
144
|
+
"update": _override_update,
|
|
145
|
+
"season_select": _override_season_select,
|
|
146
|
+
"organize_preview": _override_organize_preview,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Which fixture scenario backs each screen (default: a Psych season).
|
|
150
|
+
_SCENARIO = {
|
|
151
|
+
"selection": "psych-season-01",
|
|
152
|
+
"season_select": "psych-season-01",
|
|
153
|
+
"disc_overview": "psych-season-01",
|
|
154
|
+
"organize_preview": "psych-season-01",
|
|
155
|
+
"update": "psych-season-01",
|
|
156
|
+
"metadata": "psych-season-01",
|
|
157
|
+
"release": "psych-season-01",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
SHOTS = ["selection", "season_select", "disc_overview", "organize_preview", "update"]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Server (internal --_serve mode)
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def _build_screen(page, scenario_name: str, screen: str) -> None:
|
|
168
|
+
from riplex_app.main import RiplexApp
|
|
169
|
+
from tests.support.fixtures import load_scenario
|
|
170
|
+
from tests.support.seed import populate_state
|
|
171
|
+
|
|
172
|
+
app = RiplexApp(page)
|
|
173
|
+
scenario = load_scenario(scenario_name)
|
|
174
|
+
tmp = Path(tempfile.mkdtemp())
|
|
175
|
+
populate_state(app.state, scenario, tmp)
|
|
176
|
+
override = _OVERRIDES.get(screen)
|
|
177
|
+
if override:
|
|
178
|
+
override(app, scenario, tmp)
|
|
179
|
+
app.navigate(screen)
|
|
180
|
+
# Drop the floating bug-report button for a clean marketing shot.
|
|
181
|
+
page.floating_action_button = None
|
|
182
|
+
try:
|
|
183
|
+
page.update()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _serve(scenario_name: str, screen: str, port: int) -> None:
|
|
189
|
+
import flet as ft
|
|
190
|
+
import uvicorn
|
|
191
|
+
|
|
192
|
+
def target(page):
|
|
193
|
+
_build_screen(page, scenario_name, screen)
|
|
194
|
+
|
|
195
|
+
asgi_app = ft.run(target, export_asgi_app=True)
|
|
196
|
+
uvicorn.Server(
|
|
197
|
+
uvicorn.Config(asgi_app, host="127.0.0.1", port=port, log_level="error")
|
|
198
|
+
).run()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Orchestrator (default mode)
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def _free_port() -> int:
|
|
206
|
+
s = socket.socket()
|
|
207
|
+
s.bind(("127.0.0.1", 0))
|
|
208
|
+
port = s.getsockname()[1]
|
|
209
|
+
s.close()
|
|
210
|
+
return port
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _wait_port(port: int, timeout: float = 40.0) -> bool:
|
|
214
|
+
end = time.time() + timeout
|
|
215
|
+
while time.time() < end:
|
|
216
|
+
try:
|
|
217
|
+
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
218
|
+
return True
|
|
219
|
+
except OSError:
|
|
220
|
+
time.sleep(0.3)
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _capture(scenario_name: str, screen: str, out_path: Path, settle_ms: int) -> bool:
|
|
225
|
+
from playwright.sync_api import sync_playwright
|
|
226
|
+
|
|
227
|
+
port = _free_port()
|
|
228
|
+
proc = subprocess.Popen(
|
|
229
|
+
[sys.executable, __file__, "--_serve",
|
|
230
|
+
"--scenario", scenario_name, "--screen", screen, "--port", str(port)],
|
|
231
|
+
)
|
|
232
|
+
try:
|
|
233
|
+
if not _wait_port(port):
|
|
234
|
+
print(f" ! server for {screen} did not start")
|
|
235
|
+
return False
|
|
236
|
+
with sync_playwright() as p:
|
|
237
|
+
browser = p.chromium.launch()
|
|
238
|
+
page = browser.new_page(
|
|
239
|
+
viewport={"width": 900, "height": 650}, device_scale_factor=2,
|
|
240
|
+
)
|
|
241
|
+
page.goto(f"http://127.0.0.1:{port}", wait_until="networkidle", timeout=60000)
|
|
242
|
+
page.wait_for_timeout(settle_ms)
|
|
243
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
page.screenshot(path=str(out_path))
|
|
245
|
+
browser.close()
|
|
246
|
+
print(f" + {screen} -> {out_path}")
|
|
247
|
+
return True
|
|
248
|
+
finally:
|
|
249
|
+
proc.terminate()
|
|
250
|
+
try:
|
|
251
|
+
proc.wait(timeout=10)
|
|
252
|
+
except subprocess.TimeoutExpired:
|
|
253
|
+
proc.kill()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def main() -> int:
|
|
257
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
258
|
+
parser.add_argument("--_serve", action="store_true", help=argparse.SUPPRESS)
|
|
259
|
+
parser.add_argument("--scenario", default=None)
|
|
260
|
+
parser.add_argument("--screen", default=None)
|
|
261
|
+
parser.add_argument("--port", type=int, default=0)
|
|
262
|
+
parser.add_argument("--out", default="screenshots", help="output directory")
|
|
263
|
+
parser.add_argument("--all", action="store_true", help="capture every mapped screen")
|
|
264
|
+
parser.add_argument("--settle-ms", type=int, default=4500,
|
|
265
|
+
help="ms to wait for Flutter to paint before capturing")
|
|
266
|
+
parser.add_argument("--list", action="store_true", help="list mapped screens")
|
|
267
|
+
args = parser.parse_args()
|
|
268
|
+
|
|
269
|
+
if args.list:
|
|
270
|
+
for s in SHOTS:
|
|
271
|
+
print(f"{s:20} <- {_SCENARIO.get(s, 'psych-season-01')}")
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
if getattr(args, "_serve"):
|
|
275
|
+
_serve(args.scenario, args.screen, args.port)
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
out_dir = Path(args.out)
|
|
279
|
+
if args.all:
|
|
280
|
+
screens = SHOTS
|
|
281
|
+
elif args.screen:
|
|
282
|
+
screens = [args.screen]
|
|
283
|
+
else:
|
|
284
|
+
parser.error("specify --screen NAME, --all, or --list")
|
|
285
|
+
|
|
286
|
+
ok = True
|
|
287
|
+
for screen in screens:
|
|
288
|
+
scenario_name = args.scenario or _SCENARIO.get(screen, "psych-season-01")
|
|
289
|
+
print(f"capturing {screen} ({scenario_name})...")
|
|
290
|
+
if not _capture(scenario_name, screen, out_dir / f"{screen}.png", args.settle_ms):
|
|
291
|
+
ok = False
|
|
292
|
+
return 0 if ok else 1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
raise SystemExit(main())
|
|
@@ -120,6 +120,22 @@ def get_auto_eject() -> bool:
|
|
|
120
120
|
return bool(cfg.get("auto_eject", True))
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def get_makemkv_list_timeout() -> int:
|
|
124
|
+
"""Seconds to wait for ``makemkvcon -r info list`` (drive enumeration).
|
|
125
|
+
|
|
126
|
+
Defaults to ``120``. Some drives are slow to spin up or enumerate and
|
|
127
|
+
time out under the old fixed 60s even though the MakeMKV GUI works;
|
|
128
|
+
raise ``makemkv_list_timeout`` in the config if you still hit
|
|
129
|
+
``makemkvcon failed: ... timed out after N seconds``.
|
|
130
|
+
"""
|
|
131
|
+
cfg = load_config()
|
|
132
|
+
try:
|
|
133
|
+
return int(cfg.get("makemkv_list_timeout", 120))
|
|
134
|
+
except (TypeError, ValueError):
|
|
135
|
+
return 120
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
123
139
|
def _config_write_path() -> Path:
|
|
124
140
|
"""Determine the writable config path (user-scoped)."""
|
|
125
141
|
appdata = os.environ.get("APPDATA")
|