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.
Files changed (242) hide show
  1. {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/release.yml +3 -10
  2. {riplex-1.0.2/src/riplex.egg-info → riplex-1.0.4}/PKG-INFO +2 -2
  3. {riplex-1.0.2 → riplex-1.0.4}/RELEASE_NOTES_v1.0.0.md +21 -29
  4. {riplex-1.0.2 → riplex-1.0.4}/docs/changelog.md +3 -0
  5. riplex-1.0.4/issues/thediscdb-metadata-source.md +77 -0
  6. {riplex-1.0.2 → riplex-1.0.4}/pyproject.toml +4 -4
  7. riplex-1.0.4/screenshots/v1.0.0_Disc_Overview.png +0 -0
  8. riplex-1.0.4/screenshots/v1.0.0_In_Place_Update.png +0 -0
  9. riplex-1.0.4/screenshots/v1.0.0_Multi_Season_Select.png +0 -0
  10. riplex-1.0.4/screenshots/v1.0.0_Organize_Preview.png +0 -0
  11. riplex-1.0.4/scripts/gui_screenshot.py +296 -0
  12. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/config.py +16 -0
  13. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/analysis.py +266 -6
  14. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/makemkv.py +3 -1
  15. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/provider.py +12 -1
  16. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/manifest.py +83 -0
  17. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/matcher.py +81 -1
  18. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/title.py +3 -3
  19. {riplex-1.0.2 → riplex-1.0.4/src/riplex.egg-info}/PKG-INFO +2 -2
  20. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/SOURCES.txt +14 -0
  21. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/requires.txt +1 -1
  22. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/scm_file_list.json +191 -177
  23. riplex-1.0.4/src/riplex.egg-info/scm_version.json +8 -0
  24. riplex-1.0.4/src/riplex_app/assets/riplex-icon.icns +0 -0
  25. riplex-1.0.4/src/riplex_app/assets/riplex-icon.ico +0 -0
  26. riplex-1.0.4/src/riplex_app/assets/riplex-icon.png +0 -0
  27. riplex-1.0.4/src/riplex_app/assets/riplex-icon.svg +21 -0
  28. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/main.py +11 -9
  29. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_detection.py +55 -13
  30. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/organize_preview.py +40 -8
  31. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/progress.py +3 -0
  32. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/selection.py +108 -18
  33. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/welcome.py +2 -0
  34. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/orchestrate.py +35 -1
  35. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_screen_build_smoke.py +2 -53
  36. riplex-1.0.4/tests/support/seed.py +69 -0
  37. {riplex-1.0.2 → riplex-1.0.4}/tests/test_app_main.py +14 -0
  38. {riplex-1.0.2 → riplex-1.0.4}/tests/test_cli_utils.py +16 -0
  39. {riplex-1.0.2 → riplex-1.0.4}/tests/test_config.py +26 -0
  40. {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_analysis.py +11 -0
  41. riplex-1.0.4/tests/test_disc_assessment.py +70 -0
  42. {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_detection_screen.py +43 -0
  43. riplex-1.0.4/tests/test_episode_carryover.py +137 -0
  44. {riplex-1.0.2 → riplex-1.0.4}/tests/test_matcher.py +91 -0
  45. riplex-1.0.4/tests/test_organize_preview.py +29 -0
  46. riplex-1.0.2/src/riplex.egg-info/scm_version.json +0 -8
  47. {riplex-1.0.2 → riplex-1.0.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  48. {riplex-1.0.2 → riplex-1.0.4}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
  49. {riplex-1.0.2 → riplex-1.0.4}/.github/agents/riplex.agent.md +0 -0
  50. {riplex-1.0.2 → riplex-1.0.4}/.github/contributors-overrides.json +0 -0
  51. {riplex-1.0.2 → riplex-1.0.4}/.github/copilot-instructions.md +0 -0
  52. {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/publish.yml +0 -0
  53. {riplex-1.0.2 → riplex-1.0.4}/.github/workflows/update-contributors.yml +0 -0
  54. {riplex-1.0.2 → riplex-1.0.4}/.gitignore +0 -0
  55. {riplex-1.0.2 → riplex-1.0.4}/.vscode/settings.json +0 -0
  56. {riplex-1.0.2 → riplex-1.0.4}/CONTRIBUTORS.md +0 -0
  57. {riplex-1.0.2 → riplex-1.0.4}/LICENSE +0 -0
  58. {riplex-1.0.2 → riplex-1.0.4}/README.md +0 -0
  59. {riplex-1.0.2 → riplex-1.0.4}/docs/architecture.md +0 -0
  60. {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/lookup.md +0 -0
  61. {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/orchestrate.md +0 -0
  62. {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/organize.md +0 -0
  63. {riplex-1.0.2 → riplex-1.0.4}/docs/cli-guide/workflow.md +0 -0
  64. {riplex-1.0.2 → riplex-1.0.4}/docs/development/testing.md +0 -0
  65. {riplex-1.0.2 → riplex-1.0.4}/docs/getting-started/configuration.md +0 -0
  66. {riplex-1.0.2 → riplex-1.0.4}/docs/getting-started/installation.md +0 -0
  67. {riplex-1.0.2 → riplex-1.0.4}/docs/gui-guide/gui-walkthrough.md +0 -0
  68. {riplex-1.0.2 → riplex-1.0.4}/docs/index.md +0 -0
  69. {riplex-1.0.2 → riplex-1.0.4}/docs/naming-rules.md +0 -0
  70. {riplex-1.0.2 → riplex-1.0.4}/docs/reference/cli.md +0 -0
  71. {riplex-1.0.2 → riplex-1.0.4}/docs/troubleshooting.md +0 -0
  72. {riplex-1.0.2 → riplex-1.0.4}/issues/debug-artifacts-consolidation.md +0 -0
  73. {riplex-1.0.2 → riplex-1.0.4}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
  74. {riplex-1.0.2 → riplex-1.0.4}/issues/one-season-at-a-time.md +0 -0
  75. {riplex-1.0.2 → riplex-1.0.4}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  76. {riplex-1.0.2 → riplex-1.0.4}/issues/organize-preview-match-override.md +0 -0
  77. {riplex-1.0.2 → riplex-1.0.4}/issues/planned-features.md +0 -0
  78. {riplex-1.0.2 → riplex-1.0.4}/mkdocs.yml +0 -0
  79. {riplex-1.0.2 → riplex-1.0.4}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
  80. {riplex-1.0.2 → riplex-1.0.4}/screenshots/1_Welcome_Screen.png +0 -0
  81. {riplex-1.0.2 → riplex-1.0.4}/screenshots/2_Disc_Detection_BTTF.png +0 -0
  82. {riplex-1.0.2 → riplex-1.0.4}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
  83. {riplex-1.0.2 → riplex-1.0.4}/screenshots/4_Disc_Release_BTTF.png +0 -0
  84. {riplex-1.0.2 → riplex-1.0.4}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
  85. {riplex-1.0.2 → riplex-1.0.4}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
  86. {riplex-1.0.2 → riplex-1.0.4}/scripts/__init__.py +0 -0
  87. {riplex-1.0.2 → riplex-1.0.4}/scripts/gen_gui_fixtures.py +0 -0
  88. {riplex-1.0.2 → riplex-1.0.4}/scripts/update_contributors.py +0 -0
  89. {riplex-1.0.2 → riplex-1.0.4}/setup.cfg +0 -0
  90. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/__init__.py +0 -0
  91. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/cache.py +0 -0
  92. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/dedup.py +0 -0
  93. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/detect.py +0 -0
  94. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/disc/__init__.py +0 -0
  95. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/formatter.py +0 -0
  96. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/lookup.py +0 -0
  97. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/__init__.py +0 -0
  98. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/autosearch.py +0 -0
  99. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/planner.py +0 -0
  100. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/provider.py +0 -0
  101. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/sources/__init__.py +0 -0
  102. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/metadata/sources/tmdb.py +0 -0
  103. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/models.py +0 -0
  104. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/normalize.py +0 -0
  105. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/organize_by_group.py +0 -0
  106. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/organizer.py +0 -0
  107. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/resume.py +0 -0
  108. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/scanner.py +0 -0
  109. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/snapshot.py +0 -0
  110. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/splitter.py +0 -0
  111. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/tagger.py +0 -0
  112. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/ui.py +0 -0
  113. {riplex-1.0.2 → riplex-1.0.4}/src/riplex/updater.py +0 -0
  114. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/dependency_links.txt +0 -0
  115. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/entry_points.txt +0 -0
  116. {riplex-1.0.2 → riplex-1.0.4}/src/riplex.egg-info/top_level.txt +0 -0
  117. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/__init__.py +0 -0
  118. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/bug_report.py +0 -0
  119. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/crash_dump.py +0 -0
  120. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/keep_awake.py +0 -0
  121. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/__init__.py +0 -0
  122. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_overview.py +0 -0
  123. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/disc_swap.py +0 -0
  124. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/done.py +0 -0
  125. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/folder_picker.py +0 -0
  126. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/metadata.py +0 -0
  127. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/orchestrate_done.py +0 -0
  128. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/organize_done.py +0 -0
  129. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/release.py +0 -0
  130. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/season_select.py +0 -0
  131. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_app/screens/update.py +0 -0
  132. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/__init__.py +0 -0
  133. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/__init__.py +0 -0
  134. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/lookup.py +0 -0
  135. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/organize.py +0 -0
  136. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/rip.py +0 -0
  137. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/commands/setup.py +0 -0
  138. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/formatting.py +0 -0
  139. {riplex-1.0.2 → riplex-1.0.4}/src/riplex_cli/main.py +0 -0
  140. {riplex-1.0.2 → riplex-1.0.4}/tests/__init__.py +0 -0
  141. {riplex-1.0.2 → riplex-1.0.4}/tests/conftest.py +0 -0
  142. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/chernobyl_disc1.json +0 -0
  143. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/2001-a-space-odyssey-1968.json +0 -0
  144. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/akira-1988.json +0 -0
  145. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-1985.json +0 -0
  146. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-part-ii-1989.json +0 -0
  147. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/back-to-the-future-part-iii-1990.json +0 -0
  148. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/blade-1998.json +0 -0
  149. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/blade-runner-2049-2017.json +0 -0
  150. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/chernobyl-2019.json +0 -0
  151. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/d-day-normandy-1944-2014.json +0 -0
  152. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/final-destination-bloodlines-2025.json +0 -0
  153. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/flight-of-the-butterflies-2012.json +0 -0
  154. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/inception-2010.json +0 -0
  155. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/independence-day-1996.json +0 -0
  156. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/journey-to-space-2015.json +0 -0
  157. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/journey-to-the-south-pacific-2013.json +0 -0
  158. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/jurassic-park-1993.json +0 -0
  159. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/king-kong-2005.json +0 -0
  160. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/mad-max-fury-road-2015.json +0 -0
  161. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/march-of-the-penguins-2005.json +0 -0
  162. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/national-parks-adventure-2016.json +0 -0
  163. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/oceans-our-blue-planet-2018.json +0 -0
  164. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-01.json +0 -0
  165. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-03.json +0 -0
  166. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-season-04.json +0 -0
  167. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/psych-the-movie-2017.json +0 -0
  168. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/space-station-3d-2002.json +0 -0
  169. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-godfather-1972.json +0 -0
  170. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-last-reef-cities-beneath-the-sea-2012.json +0 -0
  171. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-matrix-1999.json +0 -0
  172. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-matrix-reloaded-2003.json +0 -0
  173. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-1999.json +0 -0
  174. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-returns-2001.json +0 -0
  175. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-mummy-tomb-of-the-dragon-emperor-2008.json +0 -0
  176. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-patriot-2000.json +0 -0
  177. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/the-thing-1982.json +0 -0
  178. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/gui/scenarios/wonders-of-the-arctic-2014.json +0 -0
  179. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  180. {riplex-1.0.2 → riplex-1.0.4}/tests/fixtures/makemkvcon_list.txt +0 -0
  181. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/__init__.py +0 -0
  182. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/conftest.py +0 -0
  183. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_lookup.py +0 -0
  184. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_organize.py +0 -0
  185. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_cli_parser.py +0 -0
  186. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_fixture_scenarios.py +0 -0
  187. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_errors.py +0 -0
  188. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_media_types.py +0 -0
  189. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_movie_orchestrate.py +0 -0
  190. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_organize.py +0 -0
  191. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_resume_prefill.py +0 -0
  192. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_tv_orchestrate.py +0 -0
  193. {riplex-1.0.2 → riplex-1.0.4}/tests/integration/test_flow_welcome.py +0 -0
  194. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  195. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  196. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  197. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  198. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  199. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  200. {riplex-1.0.2 → riplex-1.0.4}/tests/snapshots/Waterworld.snapshot.json +0 -0
  201. {riplex-1.0.2 → riplex-1.0.4}/tests/support/__init__.py +0 -0
  202. {riplex-1.0.2 → riplex-1.0.4}/tests/support/cli.py +0 -0
  203. {riplex-1.0.2 → riplex-1.0.4}/tests/support/driver.py +0 -0
  204. {riplex-1.0.2 → riplex-1.0.4}/tests/support/fake_page.py +0 -0
  205. {riplex-1.0.2 → riplex-1.0.4}/tests/support/fixtures.py +0 -0
  206. {riplex-1.0.2 → riplex-1.0.4}/tests/support/provider_mocks.py +0 -0
  207. {riplex-1.0.2 → riplex-1.0.4}/tests/support/sync_runtime.py +0 -0
  208. {riplex-1.0.2 → riplex-1.0.4}/tests/test_autosearch.py +0 -0
  209. {riplex-1.0.2 → riplex-1.0.4}/tests/test_cache.py +0 -0
  210. {riplex-1.0.2 → riplex-1.0.4}/tests/test_dedup.py +0 -0
  211. {riplex-1.0.2 → riplex-1.0.4}/tests/test_detect.py +0 -0
  212. {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_fixtures.py +0 -0
  213. {riplex-1.0.2 → riplex-1.0.4}/tests/test_disc_provider.py +0 -0
  214. {riplex-1.0.2 → riplex-1.0.4}/tests/test_folder_picker.py +0 -0
  215. {riplex-1.0.2 → riplex-1.0.4}/tests/test_formatter.py +0 -0
  216. {riplex-1.0.2 → riplex-1.0.4}/tests/test_lookup.py +0 -0
  217. {riplex-1.0.2 → riplex-1.0.4}/tests/test_lookup_resolve_groups.py +0 -0
  218. {riplex-1.0.2 → riplex-1.0.4}/tests/test_makemkv.py +0 -0
  219. {riplex-1.0.2 → riplex-1.0.4}/tests/test_manifest_prefill_ids.py +0 -0
  220. {riplex-1.0.2 → riplex-1.0.4}/tests/test_manifest_session.py +0 -0
  221. {riplex-1.0.2 → riplex-1.0.4}/tests/test_metadata_screen.py +0 -0
  222. {riplex-1.0.2 → riplex-1.0.4}/tests/test_normalize.py +0 -0
  223. {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_by_group.py +0 -0
  224. {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_cli.py +0 -0
  225. {riplex-1.0.2 → riplex-1.0.4}/tests/test_organize_session_fanout.py +0 -0
  226. {riplex-1.0.2 → riplex-1.0.4}/tests/test_organizer.py +0 -0
  227. {riplex-1.0.2 → riplex-1.0.4}/tests/test_planner.py +0 -0
  228. {riplex-1.0.2 → riplex-1.0.4}/tests/test_progress.py +0 -0
  229. {riplex-1.0.2 → riplex-1.0.4}/tests/test_release_screen.py +0 -0
  230. {riplex-1.0.2 → riplex-1.0.4}/tests/test_resume.py +0 -0
  231. {riplex-1.0.2 → riplex-1.0.4}/tests/test_rip_guide.py +0 -0
  232. {riplex-1.0.2 → riplex-1.0.4}/tests/test_scanner.py +0 -0
  233. {riplex-1.0.2 → riplex-1.0.4}/tests/test_season_select_screen.py +0 -0
  234. {riplex-1.0.2 → riplex-1.0.4}/tests/test_snapshot.py +0 -0
  235. {riplex-1.0.2 → riplex-1.0.4}/tests/test_splitter.py +0 -0
  236. {riplex-1.0.2 → riplex-1.0.4}/tests/test_tagger.py +0 -0
  237. {riplex-1.0.2 → riplex-1.0.4}/tests/test_tmdb_provider.py +0 -0
  238. {riplex-1.0.2 → riplex-1.0.4}/tests/test_ui.py +0 -0
  239. {riplex-1.0.2 → riplex-1.0.4}/tests/test_update_contributors.py +0 -0
  240. {riplex-1.0.2 → riplex-1.0.4}/tests/test_updater.py +0 -0
  241. {riplex-1.0.2 → riplex-1.0.4}/tests/test_updater_selfupdate.py +0 -0
  242. {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.2
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.18
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 — several films, or films plus a
33
- TV series — is detected as *multiple works* instead of one. Each work gets its
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
+ ![Disc Overview grouping a multi-disc box set](https://raw.githubusercontent.com/AnyCredit5518/riplex/main/screenshots/v1.0.0_Disc_Overview.png)
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
+ ![Season Select picker for a multi-season series](https://raw.githubusercontent.com/AnyCredit5518/riplex/main/screenshots/v1.0.0_Multi_Season_Select.png)
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 — not just the next in sequence.
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 — no re-searching, no risk of picking a different
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 — or when a metadata
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
+ ![Organize preview with per-file and target durations](https://raw.githubusercontent.com/AnyCredit5518/riplex/main/screenshots/v1.0.0_Organize_Preview.png)
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 — no re-downloading and no repeated
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
+ ![In-place update screen with release notes](https://raw.githubusercontent.com/AnyCredit5518/riplex/main/screenshots/v1.0.0_In_Place_Update.png)
80
+
89
81
  ### Auto-eject after ripping
90
82
 
91
- The disc ejects automatically once a rip finishes, so you can swap discs — or
92
- just know it's done — without reaching for the drive. On by default; set
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 — **start a fresh session after upgrading**.
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.18",
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
- ]
@@ -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")