riplex 0.9.7__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.
- riplex-0.9.8/.github/contributors-overrides.json +45 -0
- riplex-0.9.8/.github/workflows/update-contributors.yml +43 -0
- riplex-0.9.8/CONTRIBUTORS.md +23 -0
- {riplex-0.9.7/src/riplex.egg-info → riplex-0.9.8}/PKG-INFO +1 -1
- {riplex-0.9.7 → riplex-0.9.8}/issues/planned-features.md +28 -12
- riplex-0.9.8/scripts/__init__.py +1 -0
- riplex-0.9.8/scripts/update_contributors.py +365 -0
- {riplex-0.9.7 → riplex-0.9.8/src/riplex.egg-info}/PKG-INFO +1 -1
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/SOURCES.txt +5 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/scm_file_list.json +6 -1
- riplex-0.9.8/src/riplex.egg-info/scm_version.json +8 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/progress.py +20 -4
- riplex-0.9.8/tests/test_update_contributors.py +254 -0
- riplex-0.9.7/CONTRIBUTORS.md +0 -14
- riplex-0.9.7/src/riplex.egg-info/scm_version.json +0 -8
- {riplex-0.9.7 → riplex-0.9.8}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.github/ISSUE_TEMPLATE/crash_report.yml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.github/copilot-instructions.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.github/workflows/publish.yml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.github/workflows/release.yml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.gitignore +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/.vscode/settings.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/LICENSE +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/README.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/architecture.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/changelog.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/cli-guide/lookup.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/cli-guide/orchestrate.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/cli-guide/organize.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/cli-guide/workflow.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/getting-started/configuration.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/getting-started/installation.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/gui-guide/gui-walkthrough.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/index.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/naming-rules.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/reference/cli.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/docs/troubleshooting.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/issues/debug-artifacts-consolidation.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/issues/manual-rip-and-organize-escape-hatch.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/mkdocs.yml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/pyproject.toml +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/0_Rip_Flow_BTTF.gif +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/1_Welcome_Screen.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/2_Disc_Detection_BTTF.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/3_Metadata_Lookup_BTTF.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/4_Disc_Release_BTTF.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/5_Multi_Disc_Overview_BTTF.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/screenshots/5_Select_Title_to_RIP_BTTF.png +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/setup.cfg +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/cache.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/config.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/dedup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/detect.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/disc/provider.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/formatter.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/lookup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/manifest.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/matcher.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/models.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/normalize.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/organizer.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/scanner.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/snapshot.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/splitter.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/tagger.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/title.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/ui.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex/updater.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/bug_report.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/crash_dump.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/keep_awake.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/main.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/release.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/update.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_app/screens/welcome.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/src/riplex_cli/main.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/__init__.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_app_main.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_cache.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_cli_utils.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_config.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_dedup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_detect.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_disc_analysis.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_disc_detection_screen.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_disc_provider.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_formatter.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_lookup.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_makemkv.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_matcher.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_metadata_screen.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_normalize.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_organizer.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_planner.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_release_screen.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_rip_guide.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_scanner.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_snapshot.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_splitter.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_tagger.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_tmdb_provider.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_ui.py +0 -0
- {riplex-0.9.7 → riplex-0.9.8}/tests/test_updater.py +0 -0
- {riplex-0.9.7 → 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.
|
|
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).
|
|
121
|
-
which
|
|
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
|
-
###
|
|
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 `
|
|
127
|
-
|
|
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.)
|
|
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
|
|
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
|
|
142
|
-
replacement,
|
|
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 `
|
|
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.
|
|
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
|
|
@@ -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
|
|
@@ -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,6 +94,7 @@
|
|
|
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",
|
|
96
99
|
"tests/test_metadata_screen.py",
|
|
97
100
|
"tests/test_cli_utils.py",
|
|
@@ -127,11 +130,13 @@
|
|
|
127
130
|
"screenshots/3_Metadata_Lookup_BTTF.png",
|
|
128
131
|
"screenshots/1_Welcome_Screen.png",
|
|
129
132
|
"screenshots/2_Disc_Detection_BTTF.png",
|
|
133
|
+
".github/contributors-overrides.json",
|
|
130
134
|
".github/copilot-instructions.md",
|
|
131
135
|
".github/agents/riplex.agent.md",
|
|
132
136
|
".github/ISSUE_TEMPLATE/crash_report.yml",
|
|
133
137
|
".github/ISSUE_TEMPLATE/bug_report.yml",
|
|
134
138
|
".github/workflows/release.yml",
|
|
135
|
-
".github/workflows/publish.yml"
|
|
139
|
+
".github/workflows/publish.yml",
|
|
140
|
+
".github/workflows/update-contributors.yml"
|
|
136
141
|
]
|
|
137
142
|
}
|
|
@@ -205,10 +205,17 @@ class ProgressScreen:
|
|
|
205
205
|
# Brief pause then navigate
|
|
206
206
|
time.sleep(1)
|
|
207
207
|
|
|
208
|
+
# Navigation must run on the Flet event loop, not this bg thread,
|
|
209
|
+
# or the client won't receive the new screen until the next OS event
|
|
210
|
+
# (e.g. the user moving the window). See _update() note.
|
|
208
211
|
if self.app.state.get("workflow") == "orchestrate":
|
|
209
|
-
|
|
212
|
+
async def _nav_next():
|
|
213
|
+
self._advance_orchestrate(results)
|
|
214
|
+
self.app.page.run_task(_nav_next)
|
|
210
215
|
else:
|
|
211
|
-
|
|
216
|
+
async def _nav_done():
|
|
217
|
+
self.app.navigate("done")
|
|
218
|
+
self.app.page.run_task(_nav_done)
|
|
212
219
|
|
|
213
220
|
def _write_snapshots(self, results: list[RipResult]):
|
|
214
221
|
"""Write debug snapshots to _riplex/ folder after rip."""
|
|
@@ -384,8 +391,17 @@ class ProgressScreen:
|
|
|
384
391
|
self._update()
|
|
385
392
|
|
|
386
393
|
def _update(self):
|
|
387
|
-
"""
|
|
388
|
-
|
|
394
|
+
"""Push a page update from a background thread.
|
|
395
|
+
|
|
396
|
+
page.update() called directly from a non-loop thread mutates state but
|
|
397
|
+
doesn't flush to the Flutter client until something else wakes the
|
|
398
|
+
event loop (e.g. the user moves the window). Scheduling via
|
|
399
|
+
page.run_task() runs the update on Flet's own loop via
|
|
400
|
+
asyncio.run_coroutine_threadsafe, which does trigger the flush.
|
|
401
|
+
"""
|
|
402
|
+
async def _do_update():
|
|
389
403
|
self.app.page.update()
|
|
404
|
+
try:
|
|
405
|
+
self.app.page.run_task(_do_update)
|
|
390
406
|
except Exception:
|
|
391
407
|
pass
|