riplex 0.9.6__tar.gz → 0.9.8__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 (150) hide show
  1. riplex-0.9.8/.github/contributors-overrides.json +45 -0
  2. riplex-0.9.8/.github/workflows/update-contributors.yml +43 -0
  3. riplex-0.9.8/CONTRIBUTORS.md +23 -0
  4. {riplex-0.9.6/src/riplex.egg-info → riplex-0.9.8}/PKG-INFO +1 -1
  5. {riplex-0.9.6 → riplex-0.9.8}/issues/planned-features.md +28 -12
  6. riplex-0.9.8/scripts/__init__.py +1 -0
  7. riplex-0.9.8/scripts/update_contributors.py +365 -0
  8. {riplex-0.9.6 → riplex-0.9.8/src/riplex.egg-info}/PKG-INFO +1 -1
  9. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/SOURCES.txt +6 -0
  10. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/scm_file_list.json +7 -1
  11. riplex-0.9.8/src/riplex.egg-info/scm_version.json +8 -0
  12. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/metadata.py +11 -1
  13. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/progress.py +20 -4
  14. riplex-0.9.8/tests/test_metadata_screen.py +56 -0
  15. riplex-0.9.8/tests/test_update_contributors.py +254 -0
  16. riplex-0.9.6/CONTRIBUTORS.md +0 -14
  17. riplex-0.9.6/src/riplex.egg-info/scm_version.json +0 -8
  18. {riplex-0.9.6 → riplex-0.9.8}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {riplex-0.9.6 → riplex-0.9.8}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
  20. {riplex-0.9.6 → riplex-0.9.8}/.github/agents/riplex.agent.md +0 -0
  21. {riplex-0.9.6 → riplex-0.9.8}/.github/copilot-instructions.md +0 -0
  22. {riplex-0.9.6 → riplex-0.9.8}/.github/workflows/publish.yml +0 -0
  23. {riplex-0.9.6 → riplex-0.9.8}/.github/workflows/release.yml +0 -0
  24. {riplex-0.9.6 → riplex-0.9.8}/.gitignore +0 -0
  25. {riplex-0.9.6 → riplex-0.9.8}/.vscode/settings.json +0 -0
  26. {riplex-0.9.6 → riplex-0.9.8}/LICENSE +0 -0
  27. {riplex-0.9.6 → riplex-0.9.8}/README.md +0 -0
  28. {riplex-0.9.6 → riplex-0.9.8}/docs/architecture.md +0 -0
  29. {riplex-0.9.6 → riplex-0.9.8}/docs/changelog.md +0 -0
  30. {riplex-0.9.6 → riplex-0.9.8}/docs/cli-guide/lookup.md +0 -0
  31. {riplex-0.9.6 → riplex-0.9.8}/docs/cli-guide/orchestrate.md +0 -0
  32. {riplex-0.9.6 → riplex-0.9.8}/docs/cli-guide/organize.md +0 -0
  33. {riplex-0.9.6 → riplex-0.9.8}/docs/cli-guide/workflow.md +0 -0
  34. {riplex-0.9.6 → riplex-0.9.8}/docs/getting-started/configuration.md +0 -0
  35. {riplex-0.9.6 → riplex-0.9.8}/docs/getting-started/installation.md +0 -0
  36. {riplex-0.9.6 → riplex-0.9.8}/docs/gui-guide/gui-walkthrough.md +0 -0
  37. {riplex-0.9.6 → riplex-0.9.8}/docs/index.md +0 -0
  38. {riplex-0.9.6 → riplex-0.9.8}/docs/naming-rules.md +0 -0
  39. {riplex-0.9.6 → riplex-0.9.8}/docs/reference/cli.md +0 -0
  40. {riplex-0.9.6 → riplex-0.9.8}/docs/troubleshooting.md +0 -0
  41. {riplex-0.9.6 → riplex-0.9.8}/issues/debug-artifacts-consolidation.md +0 -0
  42. {riplex-0.9.6 → riplex-0.9.8}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
  43. {riplex-0.9.6 → riplex-0.9.8}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  44. {riplex-0.9.6 → riplex-0.9.8}/mkdocs.yml +0 -0
  45. {riplex-0.9.6 → riplex-0.9.8}/pyproject.toml +0 -0
  46. {riplex-0.9.6 → riplex-0.9.8}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
  47. {riplex-0.9.6 → riplex-0.9.8}/screenshots/1_Welcome_Screen.png +0 -0
  48. {riplex-0.9.6 → riplex-0.9.8}/screenshots/2_Disc_Detection_BTTF.png +0 -0
  49. {riplex-0.9.6 → riplex-0.9.8}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
  50. {riplex-0.9.6 → riplex-0.9.8}/screenshots/4_Disc_Release_BTTF.png +0 -0
  51. {riplex-0.9.6 → riplex-0.9.8}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
  52. {riplex-0.9.6 → riplex-0.9.8}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
  53. {riplex-0.9.6 → riplex-0.9.8}/setup.cfg +0 -0
  54. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/__init__.py +0 -0
  55. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/cache.py +0 -0
  56. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/config.py +0 -0
  57. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/dedup.py +0 -0
  58. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/detect.py +0 -0
  59. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/disc/__init__.py +0 -0
  60. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/disc/analysis.py +0 -0
  61. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/disc/makemkv.py +0 -0
  62. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/disc/provider.py +0 -0
  63. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/formatter.py +0 -0
  64. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/lookup.py +0 -0
  65. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/manifest.py +0 -0
  66. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/matcher.py +0 -0
  67. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/metadata/__init__.py +0 -0
  68. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/metadata/planner.py +0 -0
  69. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/metadata/provider.py +0 -0
  70. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/metadata/sources/__init__.py +0 -0
  71. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/metadata/sources/tmdb.py +0 -0
  72. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/models.py +0 -0
  73. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/normalize.py +0 -0
  74. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/organizer.py +0 -0
  75. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/scanner.py +0 -0
  76. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/snapshot.py +0 -0
  77. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/splitter.py +0 -0
  78. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/tagger.py +0 -0
  79. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/title.py +0 -0
  80. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/ui.py +0 -0
  81. {riplex-0.9.6 → riplex-0.9.8}/src/riplex/updater.py +0 -0
  82. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/dependency_links.txt +0 -0
  83. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/entry_points.txt +0 -0
  84. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/requires.txt +0 -0
  85. {riplex-0.9.6 → riplex-0.9.8}/src/riplex.egg-info/top_level.txt +0 -0
  86. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/__init__.py +0 -0
  87. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/bug_report.py +0 -0
  88. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/crash_dump.py +0 -0
  89. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/keep_awake.py +0 -0
  90. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/main.py +0 -0
  91. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/__init__.py +0 -0
  92. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/disc_detection.py +0 -0
  93. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/disc_overview.py +0 -0
  94. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/disc_swap.py +0 -0
  95. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/done.py +0 -0
  96. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/folder_picker.py +0 -0
  97. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/orchestrate_done.py +0 -0
  98. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/organize_done.py +0 -0
  99. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/organize_preview.py +0 -0
  100. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/release.py +0 -0
  101. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/selection.py +0 -0
  102. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/update.py +0 -0
  103. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_app/screens/welcome.py +0 -0
  104. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/__init__.py +0 -0
  105. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/__init__.py +0 -0
  106. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/lookup.py +0 -0
  107. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/orchestrate.py +0 -0
  108. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/organize.py +0 -0
  109. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/rip.py +0 -0
  110. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/commands/setup.py +0 -0
  111. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/formatting.py +0 -0
  112. {riplex-0.9.6 → riplex-0.9.8}/src/riplex_cli/main.py +0 -0
  113. {riplex-0.9.6 → riplex-0.9.8}/tests/__init__.py +0 -0
  114. {riplex-0.9.6 → riplex-0.9.8}/tests/fixtures/chernobyl_disc1.json +0 -0
  115. {riplex-0.9.6 → riplex-0.9.8}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  116. {riplex-0.9.6 → riplex-0.9.8}/tests/fixtures/makemkvcon_list.txt +0 -0
  117. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  118. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  119. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  120. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  121. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  122. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  123. {riplex-0.9.6 → riplex-0.9.8}/tests/snapshots/Waterworld.snapshot.json +0 -0
  124. {riplex-0.9.6 → riplex-0.9.8}/tests/test_app_main.py +0 -0
  125. {riplex-0.9.6 → riplex-0.9.8}/tests/test_cache.py +0 -0
  126. {riplex-0.9.6 → riplex-0.9.8}/tests/test_cli_utils.py +0 -0
  127. {riplex-0.9.6 → riplex-0.9.8}/tests/test_config.py +0 -0
  128. {riplex-0.9.6 → riplex-0.9.8}/tests/test_dedup.py +0 -0
  129. {riplex-0.9.6 → riplex-0.9.8}/tests/test_detect.py +0 -0
  130. {riplex-0.9.6 → riplex-0.9.8}/tests/test_disc_analysis.py +0 -0
  131. {riplex-0.9.6 → riplex-0.9.8}/tests/test_disc_detection_screen.py +0 -0
  132. {riplex-0.9.6 → riplex-0.9.8}/tests/test_disc_fixtures.py +0 -0
  133. {riplex-0.9.6 → riplex-0.9.8}/tests/test_disc_provider.py +0 -0
  134. {riplex-0.9.6 → riplex-0.9.8}/tests/test_formatter.py +0 -0
  135. {riplex-0.9.6 → riplex-0.9.8}/tests/test_lookup.py +0 -0
  136. {riplex-0.9.6 → riplex-0.9.8}/tests/test_makemkv.py +0 -0
  137. {riplex-0.9.6 → riplex-0.9.8}/tests/test_matcher.py +0 -0
  138. {riplex-0.9.6 → riplex-0.9.8}/tests/test_normalize.py +0 -0
  139. {riplex-0.9.6 → riplex-0.9.8}/tests/test_organizer.py +0 -0
  140. {riplex-0.9.6 → riplex-0.9.8}/tests/test_planner.py +0 -0
  141. {riplex-0.9.6 → riplex-0.9.8}/tests/test_release_screen.py +0 -0
  142. {riplex-0.9.6 → riplex-0.9.8}/tests/test_rip_guide.py +0 -0
  143. {riplex-0.9.6 → riplex-0.9.8}/tests/test_scanner.py +0 -0
  144. {riplex-0.9.6 → riplex-0.9.8}/tests/test_snapshot.py +0 -0
  145. {riplex-0.9.6 → riplex-0.9.8}/tests/test_splitter.py +0 -0
  146. {riplex-0.9.6 → riplex-0.9.8}/tests/test_tagger.py +0 -0
  147. {riplex-0.9.6 → riplex-0.9.8}/tests/test_tmdb_provider.py +0 -0
  148. {riplex-0.9.6 → riplex-0.9.8}/tests/test_ui.py +0 -0
  149. {riplex-0.9.6 → riplex-0.9.8}/tests/test_updater.py +0 -0
  150. {riplex-0.9.6 → riplex-0.9.8}/tests/test_welcome_screen.py +0 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "include": [
3
+ {
4
+ "number": 1,
5
+ "reason": "Early unlabeled bug report that led to CLI/config handling fixes."
6
+ },
7
+ {
8
+ "number": 5,
9
+ "reason": "Bug report fixed through the dvdcompare-scraper dependency."
10
+ },
11
+ {
12
+ "number": 6,
13
+ "reason": "Bug report that led to Windows title sanitization fixes."
14
+ },
15
+ {
16
+ "number": 7,
17
+ "reason": "Bug report that led to Flet compatibility fixes."
18
+ },
19
+ {
20
+ "number": 8,
21
+ "reason": "Setup report that led to improved prerequisite documentation."
22
+ },
23
+ {
24
+ "number": 9,
25
+ "reason": "Bug report that led to dvdcompare caching and rate-limit mitigations."
26
+ },
27
+ {
28
+ "number": 11,
29
+ "reason": "Setup report that led to a friendlier missing-GUI-extra error."
30
+ },
31
+ {
32
+ "number": 16,
33
+ "reason": "Bug report that led to packaged-app SSL certificate handling improvements."
34
+ },
35
+ {
36
+ "number": 17,
37
+ "reason": "Bug report that led to clearer TMDb API credential handling."
38
+ },
39
+ {
40
+ "number": 20,
41
+ "reason": "Bug report that led to better installed-tool detection after winget installs."
42
+ }
43
+ ],
44
+ "exclude": []
45
+ }
@@ -0,0 +1,43 @@
1
+ name: Update Contributors
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 12 * * 1"
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ update-contributors:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Run updater tests
28
+ run: pytest tests/test_update_contributors.py -v
29
+
30
+ - name: Update contributors
31
+ env:
32
+ GITHUB_TOKEN: ${{ github.token }}
33
+ run: python scripts/update_contributors.py
34
+
35
+ - name: Create pull request
36
+ uses: peter-evans/create-pull-request@v6
37
+ with:
38
+ commit-message: chore: refresh CONTRIBUTORS.md
39
+ title: chore: refresh CONTRIBUTORS.md
40
+ body: |
41
+ Updates the generated Bug Bashers table in CONTRIBUTORS.md from closed issue reports.
42
+ branch: chore/update-contributors
43
+ delete-branch: true
@@ -0,0 +1,23 @@
1
+ # Contributors
2
+
3
+ Thank you to everyone who helps make riplex better!
4
+
5
+ ## Bug Bashers
6
+
7
+ These users have gone out of their way to report bugs, provide logs, and help
8
+ us track down issues. We really appreciate it.
9
+
10
+ > **Rank:** 🧪 Bug Spotter (1+) · 🐛 Bug Hunter (4+) · 🔍 Bug Detective (8+) · 🛡️ Bug Slayer (15+) · 💎 Legend (25+)
11
+
12
+ <!-- BUG_BASHERS:START -->
13
+ | User | Issues |
14
+ |------|--------|
15
+ | 🐛 [@JelloEmperor](https://github.com/JelloEmperor) | [#1](https://github.com/AnyCredit5518/riplex/issues/1), [#5](https://github.com/AnyCredit5518/riplex/issues/5), [#6](https://github.com/AnyCredit5518/riplex/issues/6), [#7](https://github.com/AnyCredit5518/riplex/issues/7), [#12](https://github.com/AnyCredit5518/riplex/issues/12) |
16
+ | 🧪 [@titanzeonprime-lab](https://github.com/titanzeonprime-lab) | [#8](https://github.com/AnyCredit5518/riplex/issues/8), [#11](https://github.com/AnyCredit5518/riplex/issues/11) |
17
+ | 🧪 [@bigswannyd](https://github.com/bigswannyd) | [#16](https://github.com/AnyCredit5518/riplex/issues/16) |
18
+ | 🧪 [@darsovit](https://github.com/darsovit) | [#17](https://github.com/AnyCredit5518/riplex/issues/17) |
19
+ | 🧪 [@LtKaneDagger](https://github.com/LtKaneDagger) | [#20](https://github.com/AnyCredit5518/riplex/issues/20) |
20
+ | 🧪 [@nathanpyoung](https://github.com/nathanpyoung) | [#21](https://github.com/AnyCredit5518/riplex/issues/21) |
21
+ | 🧪 [@nerdguy1338](https://github.com/nerdguy1338) | [#9](https://github.com/AnyCredit5518/riplex/issues/9) |
22
+ | 🧪 [@TallGeekyCool](https://github.com/TallGeekyCool) | [#22](https://github.com/AnyCredit5518/riplex/issues/22) |
23
+ <!-- BUG_BASHERS:END -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.9.6
3
+ Version: 0.9.8
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
@@ -117,35 +117,51 @@ subtitle tracks differ, we should:
117
117
  ## Automated Contributors List Updater
118
118
 
119
119
  `CONTRIBUTORS.md` lists community members who have helped riplex by reporting
120
- bugs (and eventually other contribution categories). Today it's hand-edited,
121
- which means it goes stale fast and we forget to credit people.
120
+ bugs (and eventually other contribution categories). The Bug Bashers table used
121
+ to be hand-edited, which meant it went stale fast and we forgot to credit
122
+ people.
122
123
 
123
- ### Plan
124
+ ### Status
125
+
126
+ Implemented by `scripts/update_contributors.py`,
127
+ `.github/contributors-overrides.json`, and
128
+ `.github/workflows/update-contributors.yml`.
129
+
130
+ The updater owns only the generated Bug Bashers table between
131
+ `<!-- BUG_BASHERS:START -->` and `<!-- BUG_BASHERS:END -->`, while the
132
+ surrounding prose stays hand-editable.
133
+
134
+ ### Implemented behavior
124
135
 
125
136
  1. **Script** — `scripts/update_contributors.py` that:
126
- - Uses the GitHub REST API (via `requests` or `PyGithub`) to enumerate
127
- closed issues in `AnyCredit5518/riplex` labeled `bug`.
137
+ - Uses the GitHub REST API (via `httpx`) to enumerate closed issues in
138
+ `AnyCredit5518/riplex`.
128
139
  - For each issue, checks whether it was referenced by a merged commit
129
140
  on the default branch (search commit messages for `#N`, `fixes #N`,
130
- `closes #N`, etc.) only counts issues that led to an actual fix.
141
+ `closes #N`, etc.) and counts closed issues labeled `bug` that led to an
142
+ actual fix.
143
+ - Supports manual include/exclude overrides for valid reports that the
144
+ strict commit-reference signal cannot infer, such as early unlabeled
145
+ reports or fixes made in a sibling dependency.
131
146
  - Tallies issues per opener, excluding `AnyCredit5518` and any account
132
147
  ending in `[bot]`.
133
- - Maps tally to rank: 🐛 4+, 🔍 8+, 🛡️ 15+, 💎 25+ (matches existing
134
- thresholds in `CONTRIBUTORS.md`).
148
+ - Maps tally to rank: 🧪 1+, 🐛 4+, 🔍 8+, 🛡️ 15+, 💎 25+ (matches
149
+ existing thresholds in `CONTRIBUTORS.md`).
135
150
  - Renders the "Bug Bashers" table between two HTML comment markers
136
151
  (`<!-- BUG_BASHERS:START -->` / `<!-- BUG_BASHERS:END -->`) so the
137
152
  surrounding prose stays hand-editable.
138
153
  - Sorts by issue count (desc), then username (asc) for stable diffs.
139
154
  - Reads token from `GITHUB_TOKEN` env var; falls back to unauthenticated
140
155
  requests (lower rate limit) for local runs without a token.
141
- - Has unit tests covering the rank-mapping, exclusion list, marker
142
- replacement, and the "referenced by merged commit" detection (with
143
- mocked API responses).
156
+ - Has unit tests covering rank mapping, exclusion rules, marker
157
+ replacement, overrides, dry-run behavior, issue-reference detection, and
158
+ mocked API responses.
144
159
 
145
160
  2. **Workflow** — `.github/workflows/update-contributors.yml`:
146
161
  - Triggers: `schedule: cron '0 12 * * 1'` (every Monday 12:00 UTC) +
147
162
  `workflow_dispatch` for on-demand runs.
148
- - Runs `python scripts/update_contributors.py`, then uses
163
+ - Runs `pytest tests/test_update_contributors.py -v` and
164
+ `python scripts/update_contributors.py`, then uses
149
165
  `peter-evans/create-pull-request` to open a PR titled
150
166
  `chore: refresh CONTRIBUTORS.md` if there's a diff.
151
167
  - Uses the built-in `GITHUB_TOKEN` — no extra secrets needed.
@@ -0,0 +1 @@
1
+ """Repository maintenance scripts."""
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import difflib
5
+ import json
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Iterable, Sequence
13
+
14
+ import httpx
15
+
16
+
17
+ DEFAULT_REPO = "AnyCredit5518/riplex"
18
+ DEFAULT_CONTRIBUTORS_FILE = Path("CONTRIBUTORS.md")
19
+ DEFAULT_OVERRIDES_FILE = Path(".github/contributors-overrides.json")
20
+ DEFAULT_GIT_REF = "origin/main"
21
+ START_MARKER = "<!-- BUG_BASHERS:START -->"
22
+ END_MARKER = "<!-- BUG_BASHERS:END -->"
23
+ OWNER_LOGIN = "AnyCredit5518"
24
+ ISSUE_REF_RE = re.compile(
25
+ r"(?i)(?:fix(?:e[sd])?|close[sd]?|resolve[sd]?|issue|issues)?\s*#(\d+)"
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Issue:
31
+ number: int
32
+ author: str
33
+ labels: frozenset[str]
34
+ url: str
35
+
36
+ @property
37
+ def is_bug(self) -> bool:
38
+ return any(label.lower() == "bug" for label in self.labels)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Overrides:
43
+ include: frozenset[int]
44
+ exclude: frozenset[int]
45
+
46
+ @classmethod
47
+ def empty(cls) -> "Overrides":
48
+ return cls(include=frozenset(), exclude=frozenset())
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Contributor:
53
+ username: str
54
+ issue_numbers: tuple[int, ...]
55
+ issue_urls: dict[int, str]
56
+
57
+
58
+ def rank_for_count(count: int) -> str:
59
+ if count >= 25:
60
+ return "💎"
61
+ if count >= 15:
62
+ return "🛡️"
63
+ if count >= 8:
64
+ return "🔍"
65
+ if count >= 4:
66
+ return "🐛"
67
+ return "🧪"
68
+
69
+
70
+ def parse_issue(item: dict[str, Any]) -> Issue | None:
71
+ if "pull_request" in item:
72
+ return None
73
+
74
+ user = item.get("user") or {}
75
+ login = user.get("login")
76
+ if not login:
77
+ return None
78
+
79
+ labels = item.get("labels") or []
80
+ label_names = frozenset(
81
+ label.get("name", "") if isinstance(label, dict) else str(label)
82
+ for label in labels
83
+ )
84
+ return Issue(
85
+ number=int(item["number"]),
86
+ author=str(login),
87
+ labels=label_names,
88
+ url=str(item.get("html_url") or ""),
89
+ )
90
+
91
+
92
+ def fetch_closed_issues(repo: str, token: str | None = None) -> list[Issue]:
93
+ headers = {
94
+ "Accept": "application/vnd.github+json",
95
+ "X-GitHub-Api-Version": "2022-11-28",
96
+ }
97
+ if token:
98
+ headers["Authorization"] = f"Bearer {token}"
99
+
100
+ issues: list[Issue] = []
101
+ page = 1
102
+ per_page = 100
103
+ with httpx.Client(headers=headers, timeout=30.0, follow_redirects=True) as client:
104
+ while True:
105
+ response = client.get(
106
+ f"https://api.github.com/repos/{repo}/issues",
107
+ params={"state": "closed", "per_page": per_page, "page": page},
108
+ )
109
+ response.raise_for_status()
110
+ items = response.json()
111
+ if not items:
112
+ break
113
+
114
+ for item in items:
115
+ issue = parse_issue(item)
116
+ if issue is not None:
117
+ issues.append(issue)
118
+
119
+ if len(items) < per_page:
120
+ break
121
+ page += 1
122
+
123
+ return issues
124
+
125
+
126
+ def extract_issue_references(text: str) -> frozenset[int]:
127
+ return frozenset(int(match.group(1)) for match in ISSUE_REF_RE.finditer(text))
128
+
129
+
130
+ def git_ref_exists(repo_root: Path, git_ref: str) -> bool:
131
+ result = subprocess.run(
132
+ ["git", "rev-parse", "--verify", "--quiet", git_ref],
133
+ cwd=repo_root,
134
+ capture_output=True,
135
+ text=True,
136
+ )
137
+ return result.returncode == 0
138
+
139
+
140
+ def find_referenced_issues(repo_root: Path, git_ref: str = DEFAULT_GIT_REF) -> frozenset[int]:
141
+ log_ref = git_ref if git_ref_exists(repo_root, git_ref) else "HEAD"
142
+ result = subprocess.run(
143
+ ["git", "log", log_ref, "--format=%B"],
144
+ cwd=repo_root,
145
+ check=True,
146
+ capture_output=True,
147
+ text=True,
148
+ encoding="utf-8",
149
+ errors="replace",
150
+ )
151
+ return extract_issue_references(result.stdout)
152
+
153
+
154
+ def load_overrides(path: Path) -> Overrides:
155
+ if not path.exists():
156
+ return Overrides.empty()
157
+
158
+ data = json.loads(path.read_text(encoding="utf-8"))
159
+ return Overrides(
160
+ include=frozenset(_parse_override_numbers(data.get("include", []), "include")),
161
+ exclude=frozenset(_parse_override_numbers(data.get("exclude", []), "exclude")),
162
+ )
163
+
164
+
165
+ def _parse_override_numbers(entries: Any, field_name: str) -> list[int]:
166
+ if not isinstance(entries, list):
167
+ raise ValueError(f"overrides field {field_name!r} must be a list")
168
+
169
+ numbers: list[int] = []
170
+ for entry in entries:
171
+ if isinstance(entry, int):
172
+ numbers.append(entry)
173
+ continue
174
+ if isinstance(entry, dict) and isinstance(entry.get("number"), int):
175
+ numbers.append(entry["number"])
176
+ continue
177
+ raise ValueError(
178
+ f"overrides field {field_name!r} must contain issue numbers or objects with a number"
179
+ )
180
+ return numbers
181
+
182
+
183
+ def should_exclude_author(username: str, owner_login: str = OWNER_LOGIN) -> bool:
184
+ return username.lower() == owner_login.lower() or username.lower().endswith("[bot]")
185
+
186
+
187
+ def eligible_issues(
188
+ issues: Iterable[Issue], referenced_issue_numbers: set[int] | frozenset[int], overrides: Overrides
189
+ ) -> list[Issue]:
190
+ selected: list[Issue] = []
191
+ for issue in issues:
192
+ if issue.number in overrides.exclude:
193
+ continue
194
+ if should_exclude_author(issue.author):
195
+ continue
196
+
197
+ has_strict_signal = issue.is_bug and issue.number in referenced_issue_numbers
198
+ has_manual_signal = issue.number in overrides.include
199
+ if has_strict_signal or has_manual_signal:
200
+ selected.append(issue)
201
+ return selected
202
+
203
+
204
+ def build_contributors(issues: Iterable[Issue]) -> list[Contributor]:
205
+ grouped: dict[str, dict[int, str]] = {}
206
+ display_names: dict[str, str] = {}
207
+ for issue in issues:
208
+ key = issue.author.lower()
209
+ display_names.setdefault(key, issue.author)
210
+ grouped.setdefault(key, {})[issue.number] = issue.url
211
+
212
+ contributors = [
213
+ Contributor(
214
+ username=display_names[key],
215
+ issue_numbers=tuple(sorted(issue_urls)),
216
+ issue_urls=issue_urls,
217
+ )
218
+ for key, issue_urls in grouped.items()
219
+ ]
220
+ return sorted(
221
+ contributors,
222
+ key=lambda contributor: (-len(contributor.issue_numbers), contributor.username.lower()),
223
+ )
224
+
225
+
226
+ def render_table(contributors: Sequence[Contributor]) -> str:
227
+ lines = ["| User | Issues |", "|------|--------|"]
228
+ for contributor in contributors:
229
+ rank = rank_for_count(len(contributor.issue_numbers))
230
+ user = f"{rank} [@{contributor.username}](https://github.com/{contributor.username})"
231
+ issue_links = ", ".join(
232
+ f"[#{number}]({contributor.issue_urls[number]})"
233
+ for number in contributor.issue_numbers
234
+ )
235
+ lines.append(f"| {user} | {issue_links} |")
236
+ return "\n".join(lines)
237
+
238
+
239
+ def replace_marker_block(content: str, replacement: str) -> str:
240
+ start = content.find(START_MARKER)
241
+ end = content.find(END_MARKER)
242
+ if start == -1 or end == -1 or end < start:
243
+ raise ValueError(
244
+ f"Could not find ordered {START_MARKER!r} and {END_MARKER!r} markers"
245
+ )
246
+
247
+ block_start = start + len(START_MARKER)
248
+ before = content[:block_start]
249
+ after = content[end:]
250
+ return f"{before}\n{replacement.rstrip()}\n{after}"
251
+
252
+
253
+ def unified_diff(original: str, updated: str, path: Path) -> str:
254
+ return "".join(
255
+ difflib.unified_diff(
256
+ original.splitlines(keepends=True),
257
+ updated.splitlines(keepends=True),
258
+ fromfile=f"a/{path.as_posix()}",
259
+ tofile=f"b/{path.as_posix()}",
260
+ )
261
+ )
262
+
263
+
264
+ def update_contributors_file(
265
+ contributors_file: Path, table: str, dry_run: bool = False
266
+ ) -> tuple[bool, str]:
267
+ original = contributors_file.read_text(encoding="utf-8")
268
+ updated = replace_marker_block(original, table)
269
+ if original == updated:
270
+ return False, ""
271
+
272
+ diff = unified_diff(original, updated, contributors_file)
273
+ if not dry_run:
274
+ contributors_file.write_text(updated, encoding="utf-8")
275
+ return True, diff
276
+
277
+
278
+ def generate_table(
279
+ repo: str,
280
+ repo_root: Path,
281
+ overrides_file: Path,
282
+ token: str | None,
283
+ git_ref: str = DEFAULT_GIT_REF,
284
+ ) -> str:
285
+ issues = fetch_closed_issues(repo, token=token)
286
+ referenced_issue_numbers = find_referenced_issues(repo_root, git_ref=git_ref)
287
+ overrides = load_overrides(overrides_file)
288
+ selected_issues = eligible_issues(issues, referenced_issue_numbers, overrides)
289
+ contributors = build_contributors(selected_issues)
290
+ return render_table(contributors)
291
+
292
+
293
+ def build_parser() -> argparse.ArgumentParser:
294
+ parser = argparse.ArgumentParser(
295
+ description="Update the generated Bug Bashers table in CONTRIBUTORS.md."
296
+ )
297
+ parser.add_argument("--repo", default=DEFAULT_REPO, help="GitHub repo as owner/name.")
298
+ parser.add_argument(
299
+ "--contributors-file",
300
+ type=Path,
301
+ default=DEFAULT_CONTRIBUTORS_FILE,
302
+ help="Path to CONTRIBUTORS.md.",
303
+ )
304
+ parser.add_argument(
305
+ "--overrides-file",
306
+ type=Path,
307
+ default=DEFAULT_OVERRIDES_FILE,
308
+ help="Path to the manual include/exclude override JSON file.",
309
+ )
310
+ parser.add_argument(
311
+ "--repo-root",
312
+ type=Path,
313
+ default=Path("."),
314
+ help="Repository root used for local git history scanning.",
315
+ )
316
+ parser.add_argument(
317
+ "--git-ref",
318
+ default=DEFAULT_GIT_REF,
319
+ help="Git ref to scan for issue references. Falls back to HEAD if missing.",
320
+ )
321
+ parser.add_argument(
322
+ "--token",
323
+ default=None,
324
+ help="GitHub token. Defaults to GITHUB_TOKEN when omitted.",
325
+ )
326
+ parser.add_argument(
327
+ "--dry-run",
328
+ action="store_true",
329
+ help="Print the diff without writing CONTRIBUTORS.md.",
330
+ )
331
+ return parser
332
+
333
+
334
+ def main(argv: Sequence[str] | None = None) -> int:
335
+ parser = build_parser()
336
+ args = parser.parse_args(argv)
337
+
338
+ repo_root = args.repo_root.resolve()
339
+ contributors_file = (repo_root / args.contributors_file).resolve()
340
+ overrides_file = (repo_root / args.overrides_file).resolve()
341
+ token = args.token or os.environ.get("GITHUB_TOKEN")
342
+
343
+ if not token:
344
+ print(
345
+ "GITHUB_TOKEN is not set; using unauthenticated GitHub API requests with a lower rate limit.",
346
+ file=sys.stderr,
347
+ )
348
+
349
+ table = generate_table(
350
+ args.repo, repo_root, overrides_file, token=token, git_ref=args.git_ref
351
+ )
352
+ changed, diff = update_contributors_file(
353
+ contributors_file, table=table, dry_run=args.dry_run
354
+ )
355
+ if args.dry_run:
356
+ print(diff or "CONTRIBUTORS.md is already up to date.")
357
+ elif changed:
358
+ print(f"Updated {contributors_file.relative_to(repo_root)}")
359
+ else:
360
+ print("CONTRIBUTORS.md is already up to date.")
361
+ return 0
362
+
363
+
364
+ if __name__ == "__main__":
365
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.9.6
3
+ Version: 0.9.8
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
@@ -4,12 +4,14 @@ LICENSE
4
4
  README.md
5
5
  mkdocs.yml
6
6
  pyproject.toml
7
+ .github/contributors-overrides.json
7
8
  .github/copilot-instructions.md
8
9
  .github/ISSUE_TEMPLATE/bug_report.yml
9
10
  .github/ISSUE_TEMPLATE/crash_report.yml
10
11
  .github/agents/riplex.agent.md
11
12
  .github/workflows/publish.yml
12
13
  .github/workflows/release.yml
14
+ .github/workflows/update-contributors.yml
13
15
  .vscode/settings.json
14
16
  docs/architecture.md
15
17
  docs/changelog.md
@@ -35,6 +37,8 @@ screenshots/3_Metadata_Lookup_BTTF.png
35
37
  screenshots/4_Disc_Release_BTTF.png
36
38
  screenshots/5_Multi_Disc_Overview_BTTF.png
37
39
  screenshots/5_Select_Title_to_RIP_BTTF.png
40
+ scripts/__init__.py
41
+ scripts/update_contributors.py
38
42
  src/riplex/__init__.py
39
43
  src/riplex/cache.py
40
44
  src/riplex/config.py
@@ -115,6 +119,7 @@ tests/test_formatter.py
115
119
  tests/test_lookup.py
116
120
  tests/test_makemkv.py
117
121
  tests/test_matcher.py
122
+ tests/test_metadata_screen.py
118
123
  tests/test_normalize.py
119
124
  tests/test_organizer.py
120
125
  tests/test_planner.py
@@ -126,6 +131,7 @@ tests/test_splitter.py
126
131
  tests/test_tagger.py
127
132
  tests/test_tmdb_provider.py
128
133
  tests/test_ui.py
134
+ tests/test_update_contributors.py
129
135
  tests/test_updater.py
130
136
  tests/test_welcome_screen.py
131
137
  tests/fixtures/chernobyl_disc1.json
@@ -10,6 +10,8 @@
10
10
  "issues/orchestrate-dvdcompare-fallback.md",
11
11
  "issues/manual-rip-and-organize-escape-hatch.md",
12
12
  "issues/planned-features.md",
13
+ "scripts/__init__.py",
14
+ "scripts/update_contributors.py",
13
15
  ".vscode/settings.json",
14
16
  "docs/naming-rules.md",
15
17
  "docs/troubleshooting.md",
@@ -92,7 +94,9 @@
92
94
  "tests/test_detect.py",
93
95
  "tests/test_dedup.py",
94
96
  "tests/test_app_main.py",
97
+ "tests/test_update_contributors.py",
95
98
  "tests/test_scanner.py",
99
+ "tests/test_metadata_screen.py",
96
100
  "tests/test_cli_utils.py",
97
101
  "tests/test_disc_detection_screen.py",
98
102
  "tests/test_lookup.py",
@@ -126,11 +130,13 @@
126
130
  "screenshots/3_Metadata_Lookup_BTTF.png",
127
131
  "screenshots/1_Welcome_Screen.png",
128
132
  "screenshots/2_Disc_Detection_BTTF.png",
133
+ ".github/contributors-overrides.json",
129
134
  ".github/copilot-instructions.md",
130
135
  ".github/agents/riplex.agent.md",
131
136
  ".github/ISSUE_TEMPLATE/crash_report.yml",
132
137
  ".github/ISSUE_TEMPLATE/bug_report.yml",
133
138
  ".github/workflows/release.yml",
134
- ".github/workflows/publish.yml"
139
+ ".github/workflows/publish.yml",
140
+ ".github/workflows/update-contributors.yml"
135
141
  ]
136
142
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.9.8",
3
+ "distance": 0,
4
+ "node": "g42cfaf8eae6632de91f8bc13a6e3ede4248255e3",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-01"
8
+ }
@@ -238,8 +238,18 @@ class MetadataScreen:
238
238
  idx = int(self.radio_group.value)
239
239
  selected = self.tmdb_results[idx]
240
240
  self.app.state["tmdb_match"] = selected
241
- # New TMDb match invalidates any prior dvdcompare title override.
241
+ # A new TMDb match invalidates any prior dvdcompare selection. The
242
+ # release screen reuses state["release"]/state["dvdcompare_discs"] when
243
+ # present (to avoid re-querying on back-navigation), so leaving stale
244
+ # values here would make a freshly chosen film silently reuse the
245
+ # previous film's disc structure — e.g. starting "The Patriot" right
246
+ # after ripping "The Last Reef" in the same app session showed the
247
+ # Last Reef discs. Clear them so the lookup re-runs for this film.
242
248
  self.app.state.pop("dvdcompare_title_override", None)
249
+ self.app.state["release"] = None
250
+ self.app.state["dvdcompare_discs"] = []
251
+ self.app.state.pop("_dvdcompare_film", None)
252
+ self.app.state.pop("_dvdcompare_error", None)
243
253
 
244
254
  # For movies, fetch full detail to get runtime before proceeding
245
255
  if selected.media_type == "movie":