riplex 0.3.5__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.5/src/riplex.egg-info → riplex-0.3.6}/PKG-INFO +1 -1
  2. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/disc/makemkv.py +15 -1
  3. {riplex-0.3.5 → riplex-0.3.6/src/riplex.egg-info}/PKG-INFO +1 -1
  4. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/progress.py +83 -19
  5. {riplex-0.3.5 → riplex-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  6. {riplex-0.3.5 → riplex-0.3.6}/.github/agents/riplex.agent.md +0 -0
  7. {riplex-0.3.5 → riplex-0.3.6}/.github/copilot-instructions.md +0 -0
  8. {riplex-0.3.5 → riplex-0.3.6}/.github/workflows/publish.yml +0 -0
  9. {riplex-0.3.5 → riplex-0.3.6}/.github/workflows/release.yml +0 -0
  10. {riplex-0.3.5 → riplex-0.3.6}/.gitignore +0 -0
  11. {riplex-0.3.5 → riplex-0.3.6}/LICENSE +0 -0
  12. {riplex-0.3.5 → riplex-0.3.6}/README.md +0 -0
  13. {riplex-0.3.5 → riplex-0.3.6}/REFACTOR_PLAN.md +0 -0
  14. {riplex-0.3.5 → riplex-0.3.6}/docs/architecture.md +0 -0
  15. {riplex-0.3.5 → riplex-0.3.6}/docs/changelog.md +0 -0
  16. {riplex-0.3.5 → riplex-0.3.6}/docs/getting-started/configuration.md +0 -0
  17. {riplex-0.3.5 → riplex-0.3.6}/docs/getting-started/installation.md +0 -0
  18. {riplex-0.3.5 → riplex-0.3.6}/docs/guide/lookup.md +0 -0
  19. {riplex-0.3.5 → riplex-0.3.6}/docs/guide/orchestrate.md +0 -0
  20. {riplex-0.3.5 → riplex-0.3.6}/docs/guide/organize.md +0 -0
  21. {riplex-0.3.5 → riplex-0.3.6}/docs/guide/workflow.md +0 -0
  22. {riplex-0.3.5 → riplex-0.3.6}/docs/index.md +0 -0
  23. {riplex-0.3.5 → riplex-0.3.6}/docs/naming-rules.md +0 -0
  24. {riplex-0.3.5 → riplex-0.3.6}/docs/reference/cli.md +0 -0
  25. {riplex-0.3.5 → riplex-0.3.6}/docs/troubleshooting.md +0 -0
  26. {riplex-0.3.5 → riplex-0.3.6}/issues/cross-disc-dvdcompare-matching.md +0 -0
  27. {riplex-0.3.5 → riplex-0.3.6}/issues/orchestrate-dvdcompare-fallback.md +0 -0
  28. {riplex-0.3.5 → riplex-0.3.6}/issues/planned-features.md +0 -0
  29. {riplex-0.3.5 → riplex-0.3.6}/mkdocs.yml +0 -0
  30. {riplex-0.3.5 → riplex-0.3.6}/pyproject.toml +0 -0
  31. {riplex-0.3.5 → riplex-0.3.6}/setup.cfg +0 -0
  32. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/__init__.py +0 -0
  33. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/cache.py +0 -0
  34. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/config.py +0 -0
  35. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/dedup.py +0 -0
  36. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/detect.py +0 -0
  37. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/disc/__init__.py +0 -0
  38. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/disc/analysis.py +0 -0
  39. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/disc/provider.py +0 -0
  40. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/formatter.py +0 -0
  41. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/lookup.py +0 -0
  42. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/manifest.py +0 -0
  43. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/matcher.py +0 -0
  44. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/metadata/__init__.py +0 -0
  45. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/metadata/planner.py +0 -0
  46. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/metadata/provider.py +0 -0
  47. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/metadata/sources/__init__.py +0 -0
  48. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/metadata/sources/tmdb.py +0 -0
  49. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/models.py +0 -0
  50. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/normalize.py +0 -0
  51. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/organizer.py +0 -0
  52. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/scanner.py +0 -0
  53. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/snapshot.py +0 -0
  54. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/splitter.py +0 -0
  55. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/tagger.py +0 -0
  56. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/title.py +0 -0
  57. {riplex-0.3.5 → riplex-0.3.6}/src/riplex/ui.py +0 -0
  58. {riplex-0.3.5 → riplex-0.3.6}/src/riplex.egg-info/SOURCES.txt +0 -0
  59. {riplex-0.3.5 → riplex-0.3.6}/src/riplex.egg-info/dependency_links.txt +0 -0
  60. {riplex-0.3.5 → riplex-0.3.6}/src/riplex.egg-info/entry_points.txt +0 -0
  61. {riplex-0.3.5 → riplex-0.3.6}/src/riplex.egg-info/requires.txt +0 -0
  62. {riplex-0.3.5 → riplex-0.3.6}/src/riplex.egg-info/top_level.txt +0 -0
  63. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/__init__.py +0 -0
  64. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/main.py +0 -0
  65. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/__init__.py +0 -0
  66. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/disc_detection.py +0 -0
  67. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/done.py +0 -0
  68. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/folder_picker.py +0 -0
  69. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/metadata.py +0 -0
  70. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/organize_done.py +0 -0
  71. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/organize_preview.py +0 -0
  72. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/release.py +0 -0
  73. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/selection.py +0 -0
  74. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/screens/welcome.py +0 -0
  75. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_app/updater.py +0 -0
  76. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/__init__.py +0 -0
  77. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/__init__.py +0 -0
  78. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/lookup.py +0 -0
  79. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/orchestrate.py +0 -0
  80. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/organize.py +0 -0
  81. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/rip.py +0 -0
  82. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/commands/setup.py +0 -0
  83. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/formatting.py +0 -0
  84. {riplex-0.3.5 → riplex-0.3.6}/src/riplex_cli/main.py +0 -0
  85. {riplex-0.3.5 → riplex-0.3.6}/tests/__init__.py +0 -0
  86. {riplex-0.3.5 → riplex-0.3.6}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
  87. {riplex-0.3.5 → riplex-0.3.6}/tests/fixtures/makemkvcon_list.txt +0 -0
  88. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/Batman Begins.snapshot.json +0 -0
  89. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
  90. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
  91. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
  92. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
  93. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
  94. {riplex-0.3.5 → riplex-0.3.6}/tests/snapshots/Waterworld.snapshot.json +0 -0
  95. {riplex-0.3.5 → riplex-0.3.6}/tests/test_cache.py +0 -0
  96. {riplex-0.3.5 → riplex-0.3.6}/tests/test_cli_utils.py +0 -0
  97. {riplex-0.3.5 → riplex-0.3.6}/tests/test_config.py +0 -0
  98. {riplex-0.3.5 → riplex-0.3.6}/tests/test_dedup.py +0 -0
  99. {riplex-0.3.5 → riplex-0.3.6}/tests/test_detect.py +0 -0
  100. {riplex-0.3.5 → riplex-0.3.6}/tests/test_disc_analysis.py +0 -0
  101. {riplex-0.3.5 → riplex-0.3.6}/tests/test_disc_provider.py +0 -0
  102. {riplex-0.3.5 → riplex-0.3.6}/tests/test_formatter.py +0 -0
  103. {riplex-0.3.5 → riplex-0.3.6}/tests/test_makemkv.py +0 -0
  104. {riplex-0.3.5 → riplex-0.3.6}/tests/test_matcher.py +0 -0
  105. {riplex-0.3.5 → riplex-0.3.6}/tests/test_normalize.py +0 -0
  106. {riplex-0.3.5 → riplex-0.3.6}/tests/test_organizer.py +0 -0
  107. {riplex-0.3.5 → riplex-0.3.6}/tests/test_planner.py +0 -0
  108. {riplex-0.3.5 → riplex-0.3.6}/tests/test_rip_guide.py +0 -0
  109. {riplex-0.3.5 → riplex-0.3.6}/tests/test_scanner.py +0 -0
  110. {riplex-0.3.5 → riplex-0.3.6}/tests/test_snapshot.py +0 -0
  111. {riplex-0.3.5 → riplex-0.3.6}/tests/test_splitter.py +0 -0
  112. {riplex-0.3.5 → riplex-0.3.6}/tests/test_tagger.py +0 -0
  113. {riplex-0.3.5 → riplex-0.3.6}/tests/test_ui.py +0 -0
  114. {riplex-0.3.5 → 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.5
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
@@ -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.5
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,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):
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
File without changes
File without changes