riplex 0.3.4__tar.gz → 0.3.6__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 (114) hide show
  1. {riplex-0.3.4/src/riplex.egg-info → riplex-0.3.6}/PKG-INFO +1 -1
  2. {riplex-0.3.4 → riplex-0.3.6}/docs/changelog.md +6 -0
  3. riplex-0.3.6/docs/troubleshooting.md +78 -0
  4. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/makemkv.py +15 -1
  5. {riplex-0.3.4 → riplex-0.3.6/src/riplex.egg-info}/PKG-INFO +1 -1
  6. {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/SOURCES.txt +1 -0
  7. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/progress.py +83 -19
  8. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/welcome.py +4 -1
  9. {riplex-0.3.4 → riplex-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  10. {riplex-0.3.4 → riplex-0.3.6}/.github/agents/riplex.agent.md +0 -0
  11. {riplex-0.3.4 → riplex-0.3.6}/.github/copilot-instructions.md +0 -0
  12. {riplex-0.3.4 → riplex-0.3.6}/.github/workflows/publish.yml +0 -0
  13. {riplex-0.3.4 → riplex-0.3.6}/.github/workflows/release.yml +0 -0
  14. {riplex-0.3.4 → riplex-0.3.6}/.gitignore +0 -0
  15. {riplex-0.3.4 → riplex-0.3.6}/LICENSE +0 -0
  16. {riplex-0.3.4 → riplex-0.3.6}/README.md +0 -0
  17. {riplex-0.3.4 → riplex-0.3.6}/REFACTOR_PLAN.md +0 -0
  18. {riplex-0.3.4 → riplex-0.3.6}/docs/architecture.md +0 -0
  19. {riplex-0.3.4 → riplex-0.3.6}/docs/getting-started/configuration.md +0 -0
  20. {riplex-0.3.4 → riplex-0.3.6}/docs/getting-started/installation.md +0 -0
  21. {riplex-0.3.4 → riplex-0.3.6}/docs/guide/lookup.md +0 -0
  22. {riplex-0.3.4 → riplex-0.3.6}/docs/guide/orchestrate.md +0 -0
  23. {riplex-0.3.4 → riplex-0.3.6}/docs/guide/organize.md +0 -0
  24. {riplex-0.3.4 → riplex-0.3.6}/docs/guide/workflow.md +0 -0
  25. {riplex-0.3.4 → riplex-0.3.6}/docs/index.md +0 -0
  26. {riplex-0.3.4 → riplex-0.3.6}/docs/naming-rules.md +0 -0
  27. {riplex-0.3.4 → riplex-0.3.6}/docs/reference/cli.md +0 -0
  28. {riplex-0.3.4 → riplex-0.3.6}/issues/cross-disc-dvdcompare-matching.md +0 -0
  29. {riplex-0.3.4 → riplex-0.3.6}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  30. {riplex-0.3.4 → riplex-0.3.6}/issues/planned-features.md +0 -0
  31. {riplex-0.3.4 → riplex-0.3.6}/mkdocs.yml +0 -0
  32. {riplex-0.3.4 → riplex-0.3.6}/pyproject.toml +0 -0
  33. {riplex-0.3.4 → riplex-0.3.6}/setup.cfg +0 -0
  34. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/__init__.py +0 -0
  35. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/cache.py +0 -0
  36. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/config.py +0 -0
  37. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/dedup.py +0 -0
  38. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/detect.py +0 -0
  39. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/__init__.py +0 -0
  40. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/analysis.py +0 -0
  41. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/provider.py +0 -0
  42. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/formatter.py +0 -0
  43. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/lookup.py +0 -0
  44. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/manifest.py +0 -0
  45. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/matcher.py +0 -0
  46. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/__init__.py +0 -0
  47. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/planner.py +0 -0
  48. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/provider.py +0 -0
  49. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/sources/__init__.py +0 -0
  50. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/sources/tmdb.py +0 -0
  51. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/models.py +0 -0
  52. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/normalize.py +0 -0
  53. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/organizer.py +0 -0
  54. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/scanner.py +0 -0
  55. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/snapshot.py +0 -0
  56. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/splitter.py +0 -0
  57. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/tagger.py +0 -0
  58. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/title.py +0 -0
  59. {riplex-0.3.4 → riplex-0.3.6}/src/riplex/ui.py +0 -0
  60. {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/dependency_links.txt +0 -0
  61. {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/entry_points.txt +0 -0
  62. {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/requires.txt +0 -0
  63. {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/top_level.txt +0 -0
  64. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/__init__.py +0 -0
  65. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/main.py +0 -0
  66. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/__init__.py +0 -0
  67. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/disc_detection.py +0 -0
  68. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/done.py +0 -0
  69. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/folder_picker.py +0 -0
  70. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/metadata.py +0 -0
  71. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/organize_done.py +0 -0
  72. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/organize_preview.py +0 -0
  73. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/release.py +0 -0
  74. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/selection.py +0 -0
  75. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/updater.py +0 -0
  76. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/__init__.py +0 -0
  77. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/__init__.py +0 -0
  78. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/lookup.py +0 -0
  79. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/orchestrate.py +0 -0
  80. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/organize.py +0 -0
  81. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/rip.py +0 -0
  82. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/setup.py +0 -0
  83. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/formatting.py +0 -0
  84. {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/main.py +0 -0
  85. {riplex-0.3.4 → riplex-0.3.6}/tests/__init__.py +0 -0
  86. {riplex-0.3.4 → riplex-0.3.6}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  87. {riplex-0.3.4 → riplex-0.3.6}/tests/fixtures/makemkvcon_list.txt +0 -0
  88. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  89. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  90. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  91. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  92. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  93. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  94. {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Waterworld.snapshot.json +0 -0
  95. {riplex-0.3.4 → riplex-0.3.6}/tests/test_cache.py +0 -0
  96. {riplex-0.3.4 → riplex-0.3.6}/tests/test_cli_utils.py +0 -0
  97. {riplex-0.3.4 → riplex-0.3.6}/tests/test_config.py +0 -0
  98. {riplex-0.3.4 → riplex-0.3.6}/tests/test_dedup.py +0 -0
  99. {riplex-0.3.4 → riplex-0.3.6}/tests/test_detect.py +0 -0
  100. {riplex-0.3.4 → riplex-0.3.6}/tests/test_disc_analysis.py +0 -0
  101. {riplex-0.3.4 → riplex-0.3.6}/tests/test_disc_provider.py +0 -0
  102. {riplex-0.3.4 → riplex-0.3.6}/tests/test_formatter.py +0 -0
  103. {riplex-0.3.4 → riplex-0.3.6}/tests/test_makemkv.py +0 -0
  104. {riplex-0.3.4 → riplex-0.3.6}/tests/test_matcher.py +0 -0
  105. {riplex-0.3.4 → riplex-0.3.6}/tests/test_normalize.py +0 -0
  106. {riplex-0.3.4 → riplex-0.3.6}/tests/test_organizer.py +0 -0
  107. {riplex-0.3.4 → riplex-0.3.6}/tests/test_planner.py +0 -0
  108. {riplex-0.3.4 → riplex-0.3.6}/tests/test_rip_guide.py +0 -0
  109. {riplex-0.3.4 → riplex-0.3.6}/tests/test_scanner.py +0 -0
  110. {riplex-0.3.4 → riplex-0.3.6}/tests/test_snapshot.py +0 -0
  111. {riplex-0.3.4 → riplex-0.3.6}/tests/test_splitter.py +0 -0
  112. {riplex-0.3.4 → riplex-0.3.6}/tests/test_tagger.py +0 -0
  113. {riplex-0.3.4 → riplex-0.3.6}/tests/test_ui.py +0 -0
  114. {riplex-0.3.4 → riplex-0.3.6}/tests/test_updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.3.4
3
+ Version: 0.3.6
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,6 +4,12 @@ All notable changes to the riplex documentation are recorded here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## 2026-05-03
8
+
9
+ ### Added
10
+
11
+ - New troubleshooting guide (`docs/troubleshooting.md`) covering: makemkvcon not on PATH (Flatpak issue), drive not detected, invalid config file, TMDb API key signup, and dvdcompare lookup failures
12
+
7
13
  ## 2026-05-02
8
14
 
9
15
  ### Changed
@@ -0,0 +1,78 @@
1
+ # Troubleshooting
2
+
3
+ Common issues and solutions reported by users.
4
+
5
+ ---
6
+
7
+ ## "makemkvcon not on PATH"
8
+
9
+ **Symptom:** `riplex setup` warns that `makemkvcon` is not found, even though MakeMKV is installed.
10
+
11
+ **Cause:** On Linux, installing MakeMKV via Flatpak does not include `makemkvcon` (the command-line tool). The Flatpak only packages the GUI.
12
+
13
+ **Solution:** Install MakeMKV from source using the instructions on the [MakeMKV website](https://www.makemkv.com/forum/viewtopic.php?f=3&t=224). This installs both the GUI and `makemkvcon`.
14
+
15
+ ---
16
+
17
+ ## "Error reading disc" / drive not detected
18
+
19
+ **Symptom:** `riplex orchestrate` or `riplex rip` can't find your optical drive, or reports "no disc found in any drive."
20
+
21
+ **Possible causes:**
22
+
23
+ 1. **MakeMKV itself can't see the drive.** Run `makemkvcon info disc:-1` to check. If MakeMKV doesn't list your drive, the problem is at the OS/driver level, not riplex.
24
+
25
+ 2. **MakeMKV not installed correctly (Linux).** If you see an error like `libmakemkv.so.1: cannot open shared object file`, MakeMKV's libraries aren't linked properly. Reinstalling MakeMKV from source usually fixes this.
26
+
27
+ 3. **External drive not recognized.** Try specifying the drive manually:
28
+ ```
29
+ riplex orchestrate --drive /dev/sr0 # Linux
30
+ riplex orchestrate --drive D: # Windows
31
+ riplex rip --drive 1 # by index
32
+ ```
33
+
34
+ **Solution:** Verify MakeMKV works first (open the GUI or run `makemkvcon info disc:-1`). If it doesn't see the drive, reinstall MakeMKV. If MakeMKV works but riplex doesn't, use the `--drive` flag to specify the drive manually.
35
+
36
+ ---
37
+
38
+ ## Invalid config file (TOML parse error)
39
+
40
+ **Symptom:** Running any riplex command crashes with a `TOMLDecodeError` traceback mentioning your config file.
41
+
42
+ **Cause:** The config file has a syntax error, possibly from hand-editing or a corrupted write.
43
+
44
+ **Solution:** Re-run setup with the `--force` flag to delete the bad config and start fresh:
45
+
46
+ ```
47
+ riplex setup --force
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Getting a TMDb API key
53
+
54
+ **Symptom:** You're not sure what to enter on the TMDb API key request form because you're not a business or app developer.
55
+
56
+ **Solution:** TMDb asks for an app name and URL when you request a key. You can enter "riplex" as the app name and `https://github.com/AnyCredit5518/riplex` as the URL. The rest of the form can be filled with basic info - it doesn't need to be a real business. The key is approved instantly.
57
+
58
+ Sign up and request a key at: https://www.themoviedb.org/settings/api
59
+
60
+ ---
61
+
62
+ ## "dvdcompare lookup failed" / disc not on dvdcompare
63
+
64
+ **Symptom:** `riplex orchestrate` or `riplex organize` exits with an error about dvdcompare failing to find your disc.
65
+
66
+ **Cause:** Not every disc release is listed on dvdcompare.net. Niche or region-specific releases may not have entries.
67
+
68
+ **Workaround:** Use `riplex rip` instead, which handles missing dvdcompare data gracefully by falling back to TMDb runtime matching:
69
+
70
+ ```
71
+ riplex rip "Movie Title"
72
+ riplex rip "Movie Title" --execute
73
+ ```
74
+
75
+ You'll still get duplicate filtering, 4K preference, play-all detection, and TMDb runtime matching. The only thing you lose is automatic title matching and naming, so you'll need to rename and move the file into your Plex library manually after ripping.
76
+
77
+ > [!NOTE]
78
+ > A fix to make `orchestrate` and `organize` handle missing dvdcompare data gracefully is planned.
@@ -6,6 +6,7 @@ import logging
6
6
  import platform
7
7
  import re
8
8
  import subprocess
9
+ import threading
9
10
  from dataclasses import dataclass, field
10
11
  from pathlib import Path
11
12
 
@@ -471,6 +472,7 @@ class MakeMKV:
471
472
  title_index: int,
472
473
  output_dir: Path,
473
474
  progress_callback=None,
475
+ cancel_event: threading.Event | None = None,
474
476
  ) -> RipResult:
475
477
  """Rip a single title from disc via ``makemkvcon mkv``."""
476
478
  exe = self._require_exe()
@@ -494,6 +496,15 @@ class MakeMKV:
494
496
 
495
497
  try:
496
498
  for line in proc.stdout:
499
+ if cancel_event and cancel_event.is_set():
500
+ proc.terminate()
501
+ proc.wait(timeout=5)
502
+ return RipResult(
503
+ title_index=title_index,
504
+ success=False,
505
+ output_file="",
506
+ error_message="Cancelled by user",
507
+ )
497
508
  line = line.rstrip("\n\r")
498
509
  log.debug("makemkvcon: %s", line)
499
510
  raw_lines.append(line)
@@ -559,12 +570,15 @@ def run_rip(
559
570
  output_dir: Path,
560
571
  makemkvcon: Path | None = None,
561
572
  progress_callback=None,
573
+ cancel_event: threading.Event | None = None,
562
574
  ) -> RipResult:
563
575
  """Rip a single title from a disc via makemkvcon mkv.
564
576
 
565
577
  .. deprecated:: Use :pyclass:`MakeMKV` instead.
566
578
  """
567
- return MakeMKV(makemkvcon).rip(drive, title_index, output_dir, progress_callback)
579
+ return MakeMKV(makemkvcon).rip(
580
+ drive, title_index, output_dir, progress_callback, cancel_event
581
+ )
568
582
 
569
583
 
570
584
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: riplex
3
- Version: 0.3.4
3
+ Version: 0.3.6
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
@@ -13,6 +13,7 @@ docs/architecture.md
13
13
  docs/changelog.md
14
14
  docs/index.md
15
15
  docs/naming-rules.md
16
+ docs/troubleshooting.md
16
17
  docs/getting-started/configuration.md
17
18
  docs/getting-started/installation.md
18
19
  docs/guide/lookup.md
@@ -13,13 +13,25 @@ from riplex.snapshot import copy_debug_log, get_debug_dir, save_rip_snapshot
13
13
  log = logging.getLogger(__name__)
14
14
 
15
15
 
16
+ def _format_eta(seconds: int) -> str:
17
+ """Format seconds into HH:MM:SS or MM:SS."""
18
+ if seconds < 0:
19
+ return "..."
20
+ if seconds >= 3600:
21
+ h, rem = divmod(seconds, 3600)
22
+ m, s = divmod(rem, 60)
23
+ return f"{h}:{m:02d}:{s:02d}"
24
+ m, s = divmod(seconds, 60)
25
+ return f"{m}:{s:02d}"
26
+
27
+
16
28
  class ProgressScreen:
17
29
  def __init__(self, app):
18
30
  self.app = app
19
- self.cancelled = False
31
+ self._cancel_event = threading.Event()
20
32
 
21
33
  def build(self) -> ft.Control:
22
- self.cancelled = False
34
+ self._cancel_event.clear()
23
35
  selected = self.app.state["selected_titles"]
24
36
  disc_info = self.app.state["disc_info"]
25
37
  titles = disc_info.titles if disc_info else []
@@ -29,6 +41,10 @@ class ProgressScreen:
29
41
  self.total_count = len(selected)
30
42
  self.completed_count = 0
31
43
 
44
+ self._rip_start_time = 0.0
45
+ self._last_pct = -1
46
+ self._current_title_bytes = 0
47
+
32
48
  self.overall_text = ft.Text(
33
49
  f"Ripping 0/{self.total_count} titles...",
34
50
  size=16,
@@ -36,11 +52,14 @@ class ProgressScreen:
36
52
  )
37
53
  self.current_title_text = ft.Text("Preparing...", size=14, color=ft.Colors.GREY_400)
38
54
  self.progress_bar = ft.ProgressBar(width=700, value=0)
39
- self.progress_detail = ft.Text("0%", size=12, color=ft.Colors.GREY_500)
40
- self.log = ft.ListView(spacing=4, height=250, auto_scroll=True)
55
+ self.progress_pct = ft.Text("0%", size=12, weight=ft.FontWeight.BOLD)
56
+ self.progress_size = ft.Text("", size=12, color=ft.Colors.GREY_400)
57
+ self.progress_speed = ft.Text("", size=12, color=ft.Colors.GREY_400)
58
+ self.progress_eta = ft.Text("", size=12, color=ft.Colors.GREY_400)
59
+ self.log = ft.ListView(spacing=4, height=200, auto_scroll=True)
41
60
  self.cancel_btn = ft.ElevatedButton(
42
- "Cancel",
43
- icon=ft.Icons.CANCEL,
61
+ "Stop Ripping",
62
+ icon=ft.Icons.STOP,
44
63
  on_click=self._cancel,
45
64
  style=ft.ButtonStyle(bgcolor=ft.Colors.RED_700),
46
65
  )
@@ -59,7 +78,15 @@ class ProgressScreen:
59
78
  self.current_title_text,
60
79
  ft.Container(height=10),
61
80
  self.progress_bar,
62
- self.progress_detail,
81
+ ft.Row(
82
+ [
83
+ self.progress_pct,
84
+ self.progress_size,
85
+ self.progress_speed,
86
+ self.progress_eta,
87
+ ],
88
+ spacing=20,
89
+ ),
63
90
  ft.Container(height=20),
64
91
  ft.Text("Log", size=14, weight=ft.FontWeight.BOLD),
65
92
  self.log,
@@ -84,8 +111,8 @@ class ProgressScreen:
84
111
  results: list[RipResult] = []
85
112
 
86
113
  for i, title_idx in enumerate(selected):
87
- if self.cancelled:
88
- self._log_message("Cancelled by user.", ft.Colors.ORANGE)
114
+ if self._cancel_event.is_set():
115
+ self._log_message("Stopped by user.", ft.Colors.ORANGE)
89
116
  break
90
117
 
91
118
  title = self.title_map.get(title_idx)
@@ -96,7 +123,13 @@ class ProgressScreen:
96
123
  self.overall_text.value = f"Ripping {i + 1}/{self.total_count} titles..."
97
124
  self.current_title_text.value = f"Title #{title_idx}: {title_name} ({size_gb:.1f} GB)"
98
125
  self.progress_bar.value = 0
99
- self.progress_detail.value = "Starting..."
126
+ self.progress_pct.value = "0%"
127
+ self.progress_size.value = f"0.0/{size_gb:.1f} GB"
128
+ self.progress_speed.value = ""
129
+ self.progress_eta.value = ""
130
+ self._rip_start_time = time.monotonic()
131
+ self._last_pct = -1
132
+ self._current_title_bytes = title.size_bytes if title else 0
100
133
  self._log_message(f"Starting title #{title_idx}: {title_name}")
101
134
  self._update()
102
135
 
@@ -108,6 +141,7 @@ class ProgressScreen:
108
141
  output_dir,
109
142
  makemkvcon=makemkvcon,
110
143
  progress_callback=self._on_progress,
144
+ cancel_event=self._cancel_event,
111
145
  )
112
146
  results.append(result)
113
147
  elapsed = time.time() - start_time
@@ -137,7 +171,10 @@ class ProgressScreen:
137
171
  self.overall_text.value = f"Complete: {sum(1 for r in results if r.success)}/{len(results)} successful"
138
172
  self.current_title_text.value = ""
139
173
  self.progress_bar.value = 1.0
140
- self.progress_detail.value = "100%"
174
+ self.progress_pct.value = "100%"
175
+ self.progress_size.value = ""
176
+ self.progress_speed.value = ""
177
+ self.progress_eta.value = ""
141
178
  self.cancel_btn.visible = False
142
179
  self._update()
143
180
 
@@ -180,11 +217,38 @@ class ProgressScreen:
180
217
 
181
218
  def _on_progress(self, progress: RipProgress):
182
219
  """Callback from run_rip for progress updates."""
183
- if progress.total > 0:
184
- pct = progress.current / progress.total
185
- self.progress_bar.value = pct
186
- self.progress_detail.value = f"{pct * 100:.0f}%"
187
- self._update()
220
+ if progress.max_val <= 0:
221
+ return
222
+ pct = progress.current * 100 // progress.max_val
223
+ if pct == self._last_pct:
224
+ return # avoid excessive UI updates
225
+ self._last_pct = pct
226
+
227
+ self.progress_bar.value = pct / 100
228
+ self.progress_pct.value = f"{pct}%"
229
+
230
+ total_bytes = self._current_title_bytes
231
+ if total_bytes > 0:
232
+ done_bytes = total_bytes * pct // 100
233
+ done_gb = done_bytes / (1024 ** 3)
234
+ total_gb = total_bytes / (1024 ** 3)
235
+ self.progress_size.value = f"{done_gb:.1f}/{total_gb:.1f} GB"
236
+
237
+ elapsed = time.monotonic() - self._rip_start_time
238
+ if elapsed > 1:
239
+ speed_mbs = (done_bytes / (1024 ** 2)) / elapsed
240
+ self.progress_speed.value = f"{speed_mbs:.0f} MB/s"
241
+ if pct > 0 and speed_mbs > 0:
242
+ remaining_bytes = total_bytes - done_bytes
243
+ eta_secs = int(remaining_bytes / (speed_mbs * 1024 * 1024))
244
+ self.progress_eta.value = f"ETA {_format_eta(eta_secs)}"
245
+ else:
246
+ self.progress_eta.value = "ETA ..."
247
+ else:
248
+ self.progress_speed.value = ""
249
+ self.progress_eta.value = "ETA ..."
250
+
251
+ self._update()
188
252
 
189
253
  def _log_message(self, message: str, color=None):
190
254
  """Append a message to the log."""
@@ -193,10 +257,10 @@ class ProgressScreen:
193
257
  )
194
258
 
195
259
  def _cancel(self, e):
196
- """Set cancel flag (rip in progress will finish current title)."""
197
- self.cancelled = True
260
+ """Signal cancellation terminates the active makemkvcon process."""
261
+ self._cancel_event.set()
198
262
  self.cancel_btn.disabled = True
199
- self.cancel_btn.text = "Cancelling..."
263
+ self.cancel_btn.text = "Stopping..."
200
264
  self._update()
201
265
 
202
266
  def _update(self):
@@ -346,8 +346,11 @@ class WelcomeScreen:
346
346
  self.app.page.update()
347
347
  return
348
348
 
349
- self._install_status.value = "Done! Restart the app for changes to take effect."
349
+ self._install_status.value = "Done! Reloading..."
350
350
  self.app.page.update()
351
+ import time
352
+ time.sleep(1.5)
353
+ self.app.navigate("welcome")
351
354
  except Exception as exc:
352
355
  self._install_status.value = f"Install failed: {exc}. Try the manual links above."
353
356
  self.app.page.update()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes