Skip to content

Commit 78c7b67

Browse files
committed
Version 3.0.9
- Improve muti-thread downloader
1 parent 81adcea commit 78c7b67

File tree

4 files changed

+156
-51
lines changed

4 files changed

+156
-51
lines changed

oclp_r/application_entry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def func():
6363
hookd=threading.Thread(target=func,daemon=True)
6464
hookd.start()
6565
hookd.join()
66+
time.sleep(1)
6667
def _fix_cwd(self) -> None:
6768
"""
6869
In some extreme scenarios, our current working directory may disappear

oclp_r/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(self) -> None:
2020
self.metallib_api_link: str = ""
2121

2222
# Patcher Versioning
23-
self.patcher_version: str = "3.0.8" # OCLP-R
23+
self.patcher_version: str = "3.0.9" # OCLP-R
2424
self.patcher_support_pkg_version: str = "1.11.1" # PatcherSupportPkg
2525
self.copyright_date: str = "Copyright © 2020-2026 Dortania and Hackdoc"
2626
self.patcher_name: str = "OCLP-R"

oclp_r/support/network_handler.py

Lines changed: 149 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import atexit
1515
import json
1616
import math
17+
import io
1718
from typing import Union
1819
from pathlib import Path
1920

@@ -191,8 +192,9 @@ def __init__(self, url: str, path: str, size:str=None, resume_download: bool = T
191192
self.start_time: float = time.time()
192193
self.multipart_threshold: float = 1024 * 1024 * 50
193194
self.chunk_count: int = 16
194-
self.part_paths: list[Path] = []
195+
self.part_buffers: list[io.BytesIO] = []
195196
self.part_errors: list[str] = []
197+
self.part_progress: dict[int, dict] = {}
196198
self._download_lock = threading.Lock()
197199

198200
self.error: bool = False
@@ -443,46 +445,91 @@ def _build_download_ranges(self) -> list[tuple[int, int]]:
443445

444446
return ranges
445447

446-
def _download_part(self, part_index: int, start: int, end: int) -> None:
447-
part_path = Path(f"{self.filepath}.part{part_index}")
448-
self.part_paths[part_index] = part_path
448+
def _download_part(self, part_index: int, start: int, end: int, max_retries: int = 3) -> None:
449+
buffer = io.BytesIO()
450+
self.part_buffers[part_index] = buffer
451+
part_size = end - start + 1
452+
453+
with self._download_lock:
454+
self.part_progress[part_index] = {
455+
'status': 'pending',
456+
'downloaded': 0,
457+
'total': part_size,
458+
'percent': 0.0,
459+
'attempt': 0
460+
}
461+
449462
headers = {"Range": f"bytes={start}-{end}"}
450-
logging.info(f"- Download part {part_index + 1}/{len(self.part_paths)}: bytes={start}-{end}")
451-
response = NetworkUtilities().get(self.url, stream=True, timeout=100, headers=headers)
463+
464+
for attempt in range(max_retries):
465+
try:
466+
with self._download_lock:
467+
self.part_progress[part_index]['status'] = 'downloading'
468+
self.part_progress[part_index]['attempt'] = attempt + 1
469+
470+
if attempt > 0:
471+
logging.info(f"- Retry part {part_index + 1} (attempt {attempt + 1}/{max_retries})")
472+
buffer.seek(0)
473+
buffer.truncate(0)
474+
with self._download_lock:
475+
self.part_progress[part_index]['downloaded'] = 0
476+
self.part_progress[part_index]['percent'] = 0.0
477+
478+
logging.info(f"- Download part {part_index + 1}/{len(self.part_buffers)}: bytes={start}-{end}")
479+
response = NetworkUtilities().get(self.url, stream=True, timeout=100, headers=headers)
452480

453-
if response.status_code != 206:
454-
raise Exception(f"Unexpected status code for part {part_index}: {response.status_code}")
481+
if response.status_code != 206:
482+
raise Exception(f"Unexpected status code for part {part_index}: {response.status_code}")
455483

456-
with open(part_path, "wb") as file:
457-
for chunk in response.iter_content(1024 * 1024 * 4):
484+
for chunk in response.iter_content(1024 * 1024 * 4):
485+
if self.should_stop:
486+
raise Exception(self.trans["Download stopped"])
487+
if chunk:
488+
buffer.write(chunk)
489+
with self._download_lock:
490+
self.downloaded_file_size += len(chunk)
491+
self.part_progress[part_index]['downloaded'] = len(buffer.getvalue())
492+
self.part_progress[part_index]['percent'] = (
493+
self.part_progress[part_index]['downloaded'] /
494+
self.part_progress[part_index]['total'] * 100
495+
)
496+
497+
with self._download_lock:
498+
self.part_progress[part_index]['status'] = 'complete'
499+
logging.info(f"- Completed part {part_index + 1}/{len(self.part_buffers)}: {len(buffer.getvalue())} bytes in memory")
500+
return
501+
except Exception as e:
458502
if self.should_stop:
459-
raise Exception(self.trans["Download stopped"])
460-
if chunk:
461-
file.write(chunk)
462503
with self._download_lock:
463-
self.downloaded_file_size += len(chunk)
464-
465-
logging.info(f"- Completed part {part_index + 1}/{len(self.part_paths)}: {part_path}")
504+
self.part_progress[part_index]['status'] = 'cancelled'
505+
raise e
506+
with self._download_lock:
507+
self.part_progress[part_index]['status'] = 'error'
508+
if attempt < max_retries - 1:
509+
logging.warning(f"- Part {part_index + 1} failed: {e}, retrying...")
510+
time.sleep(2 ** attempt)
511+
else:
512+
raise e
466513

467514
def _merge_download_parts(self) -> None:
468515
with open(self.filepath, "wb") as destination:
469-
for part_path in self.part_paths:
470-
with open(part_path, "rb") as source:
471-
while True:
472-
chunk = source.read(1024 * 1024 * 4)
473-
if not chunk:
474-
break
475-
destination.write(chunk)
476-
if self.should_checksum:
477-
self._update_checksum(chunk)
478-
479-
for part_path in self.part_paths:
480-
if part_path and part_path.exists():
481-
part_path.unlink()
516+
for buffer in self.part_buffers:
517+
if buffer is None:
518+
continue
519+
buffer.seek(0)
520+
while True:
521+
chunk = buffer.read(1024 * 1024 * 4)
522+
if not chunk:
523+
break
524+
destination.write(chunk)
525+
if self.should_checksum:
526+
self._update_checksum(chunk)
527+
528+
self.part_buffers.clear()
482529

483530
def _download_multipart(self) -> None:
484531
ranges = self._build_download_ranges()
485-
self.part_paths = [None] * len(ranges)
532+
self.part_buffers = [None] * len(ranges)
486533
self.part_errors = []
487534
threads = []
488535

@@ -600,10 +647,10 @@ def get_percent(self) -> float:
600647
Returns:
601648
float: The download percent, or -1 if unknown
602649
"""
603-
604-
if self.total_file_size == 0.0:
605-
return -1
606-
return self.downloaded_file_size / self.total_file_size * 100
650+
with self._download_lock:
651+
if self.total_file_size == 0.0:
652+
return -1
653+
return self.downloaded_file_size / self.total_file_size * 100
607654

608655

609656
def get_speed(self) -> float:
@@ -613,8 +660,11 @@ def get_speed(self) -> float:
613660
Returns:
614661
float: The download speed in bytes per second
615662
"""
616-
617-
return self.downloaded_file_size / (time.time() - self.start_time)
663+
with self._download_lock:
664+
elapsed = time.time() - self.start_time
665+
if elapsed <= 0:
666+
return 0
667+
return self.downloaded_file_size / elapsed
618668

619669

620670
def get_time_remaining(self) -> float:
@@ -624,13 +674,16 @@ def get_time_remaining(self) -> float:
624674
Returns:
625675
float: The time remaining in seconds, or -1 if unknown
626676
"""
627-
628-
if self.total_file_size == 0.0:
629-
return -1
630-
speed = self.get_speed()
631-
if speed <= 0:
632-
return -1
633-
return (self.total_file_size - self.downloaded_file_size) / speed
677+
with self._download_lock:
678+
if self.total_file_size == 0.0:
679+
return -1
680+
elapsed = time.time() - self.start_time
681+
if elapsed <= 0:
682+
return -1
683+
speed = self.downloaded_file_size / elapsed
684+
if speed <= 0:
685+
return -1
686+
return (self.total_file_size - self.downloaded_file_size) / speed
634687

635688

636689
def get_file_size(self) -> float:
@@ -640,7 +693,56 @@ def get_file_size(self) -> float:
640693
Returns:
641694
float: The file size in bytes, or 0.0 if unknown
642695
"""
643-
return self.total_file_size
696+
with self._download_lock:
697+
return self.total_file_size
698+
699+
700+
def get_downloaded_size(self) -> float:
701+
"""
702+
Query the downloaded file size
703+
704+
Returns:
705+
float: The downloaded file size in bytes
706+
"""
707+
with self._download_lock:
708+
return self.downloaded_file_size
709+
710+
711+
def get_part_progress(self) -> dict[int, dict]:
712+
"""
713+
Query the progress of each download part
714+
715+
Returns:
716+
dict: Dictionary with part index as key and progress info as value
717+
Each entry contains: status, downloaded, total, percent, attempt
718+
"""
719+
with self._download_lock:
720+
return dict(self.part_progress)
721+
722+
723+
def print_part_progress(self) -> None:
724+
"""
725+
Print the progress of all parts to console
726+
"""
727+
progress = self.get_part_progress()
728+
if not progress:
729+
return
730+
731+
print(f"\n--- Part Progress ({len(progress)} parts) ---")
732+
for idx, info in sorted(progress.items()):
733+
status_icon = {
734+
'pending': '⏳',
735+
'downloading': '⬇️',
736+
'complete': '✅',
737+
'error': '❌',
738+
'cancelled': '🚫'
739+
}.get(info['status'], '❓')
740+
741+
retry_info = f" [retry {info['attempt']}]" if info['attempt'] > 1 else ""
742+
print(f"Part {idx + 1:2d}: {status_icon} {info['status']:12s} "
743+
f"{info['percent']:6.1f}% ({utilities.human_fmt(info['downloaded'])}/{utilities.human_fmt(info['total'])})"
744+
f"{retry_info}")
745+
print("----------------------------------------")
644746

645747

646748
def is_active(self) -> bool:
@@ -670,10 +772,9 @@ def delete_temp_files(self) -> None:
670772
self.progress_file.unlink()
671773
logging.info(self.trans["Deleted progress file: {0}"].format(self.progress_file))
672774

673-
for part_path in self.part_paths:
674-
if part_path and part_path.exists():
675-
part_path.unlink()
676-
logging.info(self.trans["Deleted partially downloaded file: {0}"].format(part_path))
775+
# Clear memory buffers
776+
self.part_buffers.clear()
777+
logging.info(self.trans["Cleared download memory buffers"])
677778
except Exception as e:
678779
logging.warning(self.trans["Failed to delete temporary files: {0}"].format(str(e)))
679780

oclp_r/wx_gui/gui_download.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ def _generate_elements(self, frame: wx.Dialog = None) -> None:
7575
while self.download_obj.is_active():
7676

7777
percentage: int = round(self.download_obj.get_percent())
78+
downloaded_size = self.download_obj.get_downloaded_size()
79+
total_size = self.download_obj.get_file_size()
80+
7881
if percentage == 0:
7982
percentage = 1
8083

8184
if percentage == -1:
82-
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} {self.trans['downloaded']} ({utilities.human_fmt(self.download_obj.get_speed())}/s)"
85+
amount_str = f"{utilities.human_fmt(downloaded_size)} {self.trans['downloaded']} ({utilities.human_fmt(self.download_obj.get_speed())}/s)"
8386
progress_bar.Pulse()
8487
else:
85-
amount_str = self.trans["{0} left - {1} of {2} ({3}/s)"].format(utilities.seconds_to_readable_time(self.download_obj.get_time_remaining()), utilities.human_fmt(self.download_obj.downloaded_file_size), utilities.human_fmt(self.download_obj.total_file_size), utilities.human_fmt(self.download_obj.get_speed()))
88+
amount_str = self.trans["{0} left - {1} of {2} ({3}/s)"].format(utilities.seconds_to_readable_time(self.download_obj.get_time_remaining()), utilities.human_fmt(downloaded_size), utilities.human_fmt(total_size), utilities.human_fmt(self.download_obj.get_speed()))
8689
progress_bar.SetValue(int(percentage))
8790

8891
label_amount.SetLabel(amount_str)

0 commit comments

Comments
 (0)