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.
- {riplex-0.3.4/src/riplex.egg-info → riplex-0.3.6}/PKG-INFO +1 -1
- {riplex-0.3.4 → riplex-0.3.6}/docs/changelog.md +6 -0
- riplex-0.3.6/docs/troubleshooting.md +78 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/makemkv.py +15 -1
- {riplex-0.3.4 → riplex-0.3.6/src/riplex.egg-info}/PKG-INFO +1 -1
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/SOURCES.txt +1 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/progress.py +83 -19
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/welcome.py +4 -1
- {riplex-0.3.4 → riplex-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/.github/copilot-instructions.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/.github/workflows/publish.yml +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/.github/workflows/release.yml +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/.gitignore +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/LICENSE +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/README.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/REFACTOR_PLAN.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/architecture.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/getting-started/configuration.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/getting-started/installation.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/guide/lookup.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/guide/orchestrate.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/guide/organize.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/guide/workflow.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/index.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/naming-rules.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/docs/reference/cli.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/issues/cross-disc-dvdcompare-matching.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/issues/planned-features.md +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/mkdocs.yml +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/pyproject.toml +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/setup.cfg +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/cache.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/config.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/dedup.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/detect.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/disc/provider.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/formatter.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/lookup.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/manifest.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/matcher.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/models.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/normalize.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/organizer.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/scanner.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/snapshot.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/splitter.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/tagger.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/title.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex/ui.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/main.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/release.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_app/updater.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/src/riplex_cli/main.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/__init__.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_cache.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_cli_utils.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_config.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_dedup.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_detect.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_disc_analysis.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_disc_provider.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_formatter.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_makemkv.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_matcher.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_normalize.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_organizer.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_planner.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_rip_guide.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_scanner.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_snapshot.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_splitter.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_tagger.py +0 -0
- {riplex-0.3.4 → riplex-0.3.6}/tests/test_ui.py +0 -0
- {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.
|
|
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(
|
|
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.
|
|
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.
|
|
31
|
+
self._cancel_event = threading.Event()
|
|
20
32
|
|
|
21
33
|
def build(self) -> ft.Control:
|
|
22
|
-
self.
|
|
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.
|
|
40
|
-
self.
|
|
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
|
-
"
|
|
43
|
-
icon=ft.Icons.
|
|
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
|
-
|
|
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.
|
|
88
|
-
self._log_message("
|
|
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.
|
|
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.
|
|
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.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
"""
|
|
197
|
-
self.
|
|
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 = "
|
|
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!
|
|
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
|
|
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
|