riplex 0.9.7__tar.gz → 0.9.9__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.9/.github/contributors-overrides.json +45 -0
  2. riplex-0.9.9/.github/workflows/update-contributors.yml +43 -0
  3. riplex-0.9.9/CONTRIBUTORS.md +23 -0
  4. {riplex-0.9.7/src/riplex.egg-info → riplex-0.9.9}/PKG-INFO +2 -2
  5. {riplex-0.9.7 → riplex-0.9.9}/issues/planned-features.md +28 -12
  6. {riplex-0.9.7 → riplex-0.9.9}/pyproject.toml +1 -1
  7. riplex-0.9.9/scripts/__init__.py +1 -0
  8. riplex-0.9.9/scripts/update_contributors.py +365 -0
  9. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/disc/provider.py +8 -6
  10. {riplex-0.9.7 → riplex-0.9.9/src/riplex.egg-info}/PKG-INFO +2 -2
  11. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/SOURCES.txt +5 -0
  12. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/requires.txt +1 -1
  13. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/scm_file_list.json +6 -1
  14. riplex-0.9.9/src/riplex.egg-info/scm_version.json +8 -0
  15. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/progress.py +20 -4
  16. riplex-0.9.9/tests/test_update_contributors.py +254 -0
  17. riplex-0.9.7/CONTRIBUTORS.md +0 -14
  18. riplex-0.9.7/src/riplex.egg-info/scm_version.json +0 -8
  19. {riplex-0.9.7 → riplex-0.9.9}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {riplex-0.9.7 → riplex-0.9.9}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
  21. {riplex-0.9.7 → riplex-0.9.9}/.github/agents/riplex.agent.md +0 -0
  22. {riplex-0.9.7 → riplex-0.9.9}/.github/copilot-instructions.md +0 -0
  23. {riplex-0.9.7 → riplex-0.9.9}/.github/workflows/publish.yml +0 -0
  24. {riplex-0.9.7 → riplex-0.9.9}/.github/workflows/release.yml +0 -0
  25. {riplex-0.9.7 → riplex-0.9.9}/.gitignore +0 -0
  26. {riplex-0.9.7 → riplex-0.9.9}/.vscode/settings.json +0 -0
  27. {riplex-0.9.7 → riplex-0.9.9}/LICENSE +0 -0
  28. {riplex-0.9.7 → riplex-0.9.9}/README.md +0 -0
  29. {riplex-0.9.7 → riplex-0.9.9}/docs/architecture.md +0 -0
  30. {riplex-0.9.7 → riplex-0.9.9}/docs/changelog.md +0 -0
  31. {riplex-0.9.7 → riplex-0.9.9}/docs/cli-guide/lookup.md +0 -0
  32. {riplex-0.9.7 → riplex-0.9.9}/docs/cli-guide/orchestrate.md +0 -0
  33. {riplex-0.9.7 → riplex-0.9.9}/docs/cli-guide/organize.md +0 -0
  34. {riplex-0.9.7 → riplex-0.9.9}/docs/cli-guide/workflow.md +0 -0
  35. {riplex-0.9.7 → riplex-0.9.9}/docs/getting-started/configuration.md +0 -0
  36. {riplex-0.9.7 → riplex-0.9.9}/docs/getting-started/installation.md +0 -0
  37. {riplex-0.9.7 → riplex-0.9.9}/docs/gui-guide/gui-walkthrough.md +0 -0
  38. {riplex-0.9.7 → riplex-0.9.9}/docs/index.md +0 -0
  39. {riplex-0.9.7 → riplex-0.9.9}/docs/naming-rules.md +0 -0
  40. {riplex-0.9.7 → riplex-0.9.9}/docs/reference/cli.md +0 -0
  41. {riplex-0.9.7 → riplex-0.9.9}/docs/troubleshooting.md +0 -0
  42. {riplex-0.9.7 → riplex-0.9.9}/issues/debug-artifacts-consolidation.md +0 -0
  43. {riplex-0.9.7 → riplex-0.9.9}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
  44. {riplex-0.9.7 → riplex-0.9.9}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  45. {riplex-0.9.7 → riplex-0.9.9}/mkdocs.yml +0 -0
  46. {riplex-0.9.7 → riplex-0.9.9}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
  47. {riplex-0.9.7 → riplex-0.9.9}/screenshots/1_Welcome_Screen.png +0 -0
  48. {riplex-0.9.7 → riplex-0.9.9}/screenshots/2_Disc_Detection_BTTF.png +0 -0
  49. {riplex-0.9.7 → riplex-0.9.9}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
  50. {riplex-0.9.7 → riplex-0.9.9}/screenshots/4_Disc_Release_BTTF.png +0 -0
  51. {riplex-0.9.7 → riplex-0.9.9}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
  52. {riplex-0.9.7 → riplex-0.9.9}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
  53. {riplex-0.9.7 → riplex-0.9.9}/setup.cfg +0 -0
  54. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/__init__.py +0 -0
  55. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/cache.py +0 -0
  56. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/config.py +0 -0
  57. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/dedup.py +0 -0
  58. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/detect.py +0 -0
  59. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/disc/__init__.py +0 -0
  60. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/disc/analysis.py +0 -0
  61. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/disc/makemkv.py +0 -0
  62. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/formatter.py +0 -0
  63. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/lookup.py +0 -0
  64. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/manifest.py +0 -0
  65. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/matcher.py +0 -0
  66. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/metadata/__init__.py +0 -0
  67. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/metadata/planner.py +0 -0
  68. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/metadata/provider.py +0 -0
  69. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/metadata/sources/__init__.py +0 -0
  70. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/metadata/sources/tmdb.py +0 -0
  71. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/models.py +0 -0
  72. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/normalize.py +0 -0
  73. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/organizer.py +0 -0
  74. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/scanner.py +0 -0
  75. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/snapshot.py +0 -0
  76. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/splitter.py +0 -0
  77. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/tagger.py +0 -0
  78. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/title.py +0 -0
  79. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/ui.py +0 -0
  80. {riplex-0.9.7 → riplex-0.9.9}/src/riplex/updater.py +0 -0
  81. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/dependency_links.txt +0 -0
  82. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/entry_points.txt +0 -0
  83. {riplex-0.9.7 → riplex-0.9.9}/src/riplex.egg-info/top_level.txt +0 -0
  84. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/__init__.py +0 -0
  85. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/bug_report.py +0 -0
  86. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/crash_dump.py +0 -0
  87. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/keep_awake.py +0 -0
  88. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/main.py +0 -0
  89. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/__init__.py +0 -0
  90. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/disc_detection.py +0 -0
  91. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/disc_overview.py +0 -0
  92. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/disc_swap.py +0 -0
  93. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/done.py +0 -0
  94. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/folder_picker.py +0 -0
  95. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/metadata.py +0 -0
  96. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/orchestrate_done.py +0 -0
  97. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/organize_done.py +0 -0
  98. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/organize_preview.py +0 -0
  99. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/release.py +0 -0
  100. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/selection.py +0 -0
  101. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/update.py +0 -0
  102. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_app/screens/welcome.py +0 -0
  103. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/__init__.py +0 -0
  104. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/__init__.py +0 -0
  105. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/lookup.py +0 -0
  106. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/orchestrate.py +0 -0
  107. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/organize.py +0 -0
  108. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/rip.py +0 -0
  109. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/commands/setup.py +0 -0
  110. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/formatting.py +0 -0
  111. {riplex-0.9.7 → riplex-0.9.9}/src/riplex_cli/main.py +0 -0
  112. {riplex-0.9.7 → riplex-0.9.9}/tests/__init__.py +0 -0
  113. {riplex-0.9.7 → riplex-0.9.9}/tests/fixtures/chernobyl_disc1.json +0 -0
  114. {riplex-0.9.7 → riplex-0.9.9}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  115. {riplex-0.9.7 → riplex-0.9.9}/tests/fixtures/makemkvcon_list.txt +0 -0
  116. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  117. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  118. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  119. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  120. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  121. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  122. {riplex-0.9.7 → riplex-0.9.9}/tests/snapshots/Waterworld.snapshot.json +0 -0
  123. {riplex-0.9.7 → riplex-0.9.9}/tests/test_app_main.py +0 -0
  124. {riplex-0.9.7 → riplex-0.9.9}/tests/test_cache.py +0 -0
  125. {riplex-0.9.7 → riplex-0.9.9}/tests/test_cli_utils.py +0 -0
  126. {riplex-0.9.7 → riplex-0.9.9}/tests/test_config.py +0 -0
  127. {riplex-0.9.7 → riplex-0.9.9}/tests/test_dedup.py +0 -0
  128. {riplex-0.9.7 → riplex-0.9.9}/tests/test_detect.py +0 -0
  129. {riplex-0.9.7 → riplex-0.9.9}/tests/test_disc_analysis.py +0 -0
  130. {riplex-0.9.7 → riplex-0.9.9}/tests/test_disc_detection_screen.py +0 -0
  131. {riplex-0.9.7 → riplex-0.9.9}/tests/test_disc_fixtures.py +0 -0
  132. {riplex-0.9.7 → riplex-0.9.9}/tests/test_disc_provider.py +0 -0
  133. {riplex-0.9.7 → riplex-0.9.9}/tests/test_formatter.py +0 -0
  134. {riplex-0.9.7 → riplex-0.9.9}/tests/test_lookup.py +0 -0
  135. {riplex-0.9.7 → riplex-0.9.9}/tests/test_makemkv.py +0 -0
  136. {riplex-0.9.7 → riplex-0.9.9}/tests/test_matcher.py +0 -0
  137. {riplex-0.9.7 → riplex-0.9.9}/tests/test_metadata_screen.py +0 -0
  138. {riplex-0.9.7 → riplex-0.9.9}/tests/test_normalize.py +0 -0
  139. {riplex-0.9.7 → riplex-0.9.9}/tests/test_organizer.py +0 -0
  140. {riplex-0.9.7 → riplex-0.9.9}/tests/test_planner.py +0 -0
  141. {riplex-0.9.7 → riplex-0.9.9}/tests/test_release_screen.py +0 -0
  142. {riplex-0.9.7 → riplex-0.9.9}/tests/test_rip_guide.py +0 -0
  143. {riplex-0.9.7 → riplex-0.9.9}/tests/test_scanner.py +0 -0
  144. {riplex-0.9.7 → riplex-0.9.9}/tests/test_snapshot.py +0 -0
  145. {riplex-0.9.7 → riplex-0.9.9}/tests/test_splitter.py +0 -0
  146. {riplex-0.9.7 → riplex-0.9.9}/tests/test_tagger.py +0 -0
  147. {riplex-0.9.7 → riplex-0.9.9}/tests/test_tmdb_provider.py +0 -0
  148. {riplex-0.9.7 → riplex-0.9.9}/tests/test_ui.py +0 -0
  149. {riplex-0.9.7 → riplex-0.9.9}/tests/test_updater.py +0 -0
  150. {riplex-0.9.7 → riplex-0.9.9}/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.7
3
+ Version: 0.9.9
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.15
11
+ Requires-Dist: dvdcompare-scraper>=0.1.16
12
12
  Requires-Dist: platformdirs>=4.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -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.
@@ -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",
15
+ "dvdcompare-scraper>=0.1.16",
16
16
  "platformdirs>=4.0",
17
17
  ]
18
18
 
@@ -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())
@@ -355,7 +355,9 @@ async def _throttled_find_film(
355
355
  try:
356
356
  if disc_format:
357
357
  return await _find_film_prefer_format(title, disc_format, year)
358
- return await find_film(title, disc_format, year=year)
358
+ return await find_film(
359
+ title, disc_format, year=year, resolve_pointers=True
360
+ )
359
361
  finally:
360
362
  _last_request_at = time.monotonic()
361
363
 
@@ -370,7 +372,7 @@ async def _throttled_get_film_by_url(url: str) -> FilmComparison:
370
372
  log.debug("dvdcompare throttle: waiting %.2fs before request", wait)
371
373
  await asyncio.sleep(wait)
372
374
  try:
373
- return await get_film_by_url(url)
375
+ return await get_film_by_url(url, resolve_pointers=True)
374
376
  finally:
375
377
  _last_request_at = time.monotonic()
376
378
 
@@ -403,21 +405,21 @@ async def _find_film_prefer_format(
403
405
  for sr in results:
404
406
  if fmt_matches(sr) and year_matches(sr):
405
407
  log.debug("dvdcompare pick (format+year): %s", sr.url)
406
- return await get_film_by_url(sr.url)
408
+ return await get_film_by_url(sr.url, resolve_pointers=True)
407
409
  # Tier 2: format only
408
410
  for sr in results:
409
411
  if fmt_matches(sr):
410
412
  log.debug("dvdcompare pick (format-only, year=%s actual=%s): %s",
411
413
  year, sr.year, sr.url)
412
- return await get_film_by_url(sr.url)
414
+ return await get_film_by_url(sr.url, resolve_pointers=True)
413
415
  # Tier 3: year only (no format listed on result)
414
416
  for sr in results:
415
417
  if year_matches(sr) and not sr.disc_format:
416
418
  log.debug("dvdcompare pick (year-only, no result format): %s", sr.url)
417
- return await get_film_by_url(sr.url)
419
+ return await get_film_by_url(sr.url, resolve_pointers=True)
418
420
  # Tier 4: first result
419
421
  log.debug("dvdcompare pick (first result fallback): %s", results[0].url)
420
- return await get_film_by_url(results[0].url)
422
+ return await get_film_by_url(results[0].url, resolve_pointers=True)
421
423
 
422
424
 
423
425
  def _clean_feature_type(raw: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.9.7
3
+ Version: 0.9.9
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.15
11
+ Requires-Dist: dvdcompare-scraper>=0.1.16
12
12
  Requires-Dist: platformdirs>=4.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -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
@@ -127,6 +131,7 @@ tests/test_splitter.py
127
131
  tests/test_tagger.py
128
132
  tests/test_tmdb_provider.py
129
133
  tests/test_ui.py
134
+ tests/test_update_contributors.py
130
135
  tests/test_updater.py
131
136
  tests/test_welcome_screen.py
132
137
  tests/fixtures/chernobyl_disc1.json
@@ -1,6 +1,6 @@
1
1
  certifi>=2024.2.2
2
2
  httpx>=0.27
3
- dvdcompare-scraper>=0.1.15
3
+ dvdcompare-scraper>=0.1.16
4
4
  platformdirs>=4.0
5
5
 
6
6
  [dev]