Skip to content

Commit 0e7e1a0

Browse files
authored
Add handling for ChunkedEncodingError causes
Update save_file_with_progress to catch ChunkedEncodingError and mark downloads as failed when they are interrupted. Handles IncompleteRead and ConnectionResetError (wrapped inside ChunkedEncodingError), allowing partial files to be retried during the final synchronous attempt.
1 parent 6ca9a45 commit 0e7e1a0

File tree

4 files changed

+145
-135
lines changed

4 files changed

+145
-135
lines changed

helpers/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ class HTTPStatus(IntEnum):
7272

7373
OK = 200
7474
FORBIDDEN = 403
75+
TOO_MANY_REQUESTS = 429
7576
INTERNAL_ERROR = 500
7677
BAD_GATEWAY = 502
78+
SERVICE_UNAVAILABLE = 503
7779
SERVER_DOWN = 521
7880

7981
# Mapping of HTTP error codes to human-readable fetch error messages.

helpers/downloaders/album_downloader.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def execute_item_download(
4848

4949
# Download item
5050
if item_download_link:
51-
downloader = MediaDownloader(
51+
media_downloader = MediaDownloader(
5252
session_info=self.session_info,
5353
download_info=DownloadInfo(
5454
download_link=item_download_link,
@@ -58,52 +58,53 @@ async def execute_item_download(
5858
live_manager=self.live_manager,
5959
)
6060

61-
failed_download = await asyncio.to_thread(downloader.download)
61+
failed_download = await asyncio.to_thread(media_downloader.download)
6262
if failed_download:
6363
self.failed_downloads.append(failed_download)
6464

65-
async def retry_failed_download(
65+
async def download_album(self, max_workers: int = MAX_WORKERS) -> None:
66+
"""Handle the album download."""
67+
num_tasks = len(self.album_info.item_pages)
68+
self.live_manager.add_overall_task(
69+
description=self.album_info.album_id,
70+
num_tasks=num_tasks,
71+
)
72+
73+
# Create tasks for downloading each item in the album
74+
semaphore = asyncio.Semaphore(max_workers)
75+
tasks = [
76+
self.execute_item_download(item_page, current_task, semaphore)
77+
for current_task, item_page in enumerate(self.album_info.item_pages)
78+
]
79+
await asyncio.gather(*tasks)
80+
81+
# If there are failed downloads, process them after all downloads are complete
82+
if self.failed_downloads:
83+
await self._process_failed_downloads()
84+
85+
# Private methods
86+
async def _retry_failed_download(
6687
self,
6788
task: int,
6889
filename: str,
6990
download_link: str,
7091
) -> None:
7192
"""Handle failed downloads and retries them."""
72-
downloader = MediaDownloader(
93+
media_downloader = MediaDownloader(
7394
session_info=self.session_info,
7495
download_info=DownloadInfo(download_link, filename, task),
7596
live_manager=self.live_manager,
7697
retries=1, # Retry once for failed downloads
7798
)
7899
# Run the synchronous download function in a separate thread
79-
await asyncio.to_thread(downloader.download)
100+
await asyncio.to_thread(media_downloader.download)
80101

81-
async def process_failed_downloads(self) -> None:
102+
async def _process_failed_downloads(self) -> None:
82103
"""Process any failed downloads after the initial attempt."""
83104
for data in self.failed_downloads:
84-
await self.retry_failed_download(
105+
await self._retry_failed_download(
85106
data["id"],
86107
data["filename"],
87108
data["download_link"],
88109
)
89110
self.failed_downloads.clear()
90-
91-
async def download_album(self, max_workers: int = MAX_WORKERS) -> None:
92-
"""Handle the album download."""
93-
num_tasks = len(self.album_info.item_pages)
94-
self.live_manager.add_overall_task(
95-
description=self.album_info.album_id,
96-
num_tasks=num_tasks,
97-
)
98-
99-
# Create tasks for downloading each item in the album
100-
semaphore = asyncio.Semaphore(max_workers)
101-
tasks = [
102-
self.execute_item_download(item_page, current_task, semaphore)
103-
for current_task, item_page in enumerate(self.album_info.item_pages)
104-
]
105-
await asyncio.gather(*tasks)
106-
107-
# If there are failed downloads, process them after all downloads are complete
108-
if self.failed_downloads:
109-
await self.process_failed_downloads()

helpers/downloaders/download_utils.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66

77
from requests import Response
8+
from requests.exceptions import ChunkedEncodingError
89

910
from helpers.config import LARGE_FILE_CHUNK_SIZE, THRESHOLDS
1011
from helpers.managers.progress_manager import ProgressManager
@@ -28,29 +29,36 @@ def save_file_with_progress(
2829
) -> bool:
2930
"""Save the file from the response to the specified path.
3031
31-
Add a `.temp` extension if the download is partial.
32+
Add a `.temp` extension if the download is partial. Handles network interruptions
33+
such as IncompleteRead and ConnectionResetError (wrapped in ChunkedEncodingError)
34+
by marking the download as incomplete.
3235
"""
3336
file_size = int(response.headers.get("Content-Length", -1))
3437
if file_size == -1:
3538
logging.warning("Content length not provided in response headers.")
3639

37-
# Create a temporary download path with the .temp extension
40+
# Initialize a temporary download path with the .temp extension
3841
temp_download_path = Path(download_path).with_suffix(".temp")
3942
chunk_size = get_chunk_size(file_size)
4043
total_downloaded = 0
4144

42-
with temp_download_path.open("wb") as file:
43-
for chunk in response.iter_content(chunk_size=chunk_size):
44-
if chunk is not None:
45-
file.write(chunk)
46-
total_downloaded += len(chunk)
47-
completed = (total_downloaded / file_size) * 100
48-
progress_manager.update_task(task, completed=completed)
45+
try:
46+
with temp_download_path.open("wb") as file:
47+
for chunk in response.iter_content(chunk_size=chunk_size):
48+
if chunk is not None:
49+
file.write(chunk)
50+
total_downloaded += len(chunk)
51+
completed = (total_downloaded / file_size) * 100
52+
progress_manager.update_task(task, completed=completed)
4953

50-
# After download is complete, rename the temp file to the original filename
54+
# Handle partial downloads caused by network interruptions
55+
except ChunkedEncodingError:
56+
return True
57+
58+
# Rename temp file to final filename if fully downloaded
5159
if total_downloaded == file_size:
5260
shutil.move(temp_download_path, download_path)
5361
return False
5462

55-
# Return True if the download is incomplete
63+
# Keep partial file and return True if incomplete
5664
return True

0 commit comments

Comments
 (0)