Learn how Universal Manga Downloader uses asyncio and aiohttp for efficient concurrent image downloads
Universal Manga Downloader uses Python’s asyncio and aiohttp libraries to download manga images concurrently. This approach dramatically improves download speeds compared to sequential downloading.
Headers are passed to the session to bypass anti-bot protections:
headers = { "Referer": "https://manga-site.com/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."}async with aiohttp.ClientSession(headers=headers) as session: # Session headers apply to all requests
Each site handler provides its own headers from core/config.py:
chunk_size = BATCH_SIZE # Default: 10results = []for i in range(0, len(image_urls), chunk_size): # Check for cancellation if check_cancel and check_cancel(): log_callback("[INFO] Process cancelled by user.") break # Get next batch of URLs chunk = image_urls[i:i+chunk_size] # Create download tasks tasks = [ download_image(session, u, temp_folder, i + idx + 1, log_callback, headers) for idx, u in enumerate(chunk) ] # Execute batch concurrently res = await asyncio.gather(*tasks) results.extend(res) # Update progress if progress_callback: progress_callback(min(i + chunk_size, len(image_urls)), len(image_urls))
Why use batches instead of downloading everything at once?
Memory management - Each download consumes memory. Batching prevents excessive memory usage.Connection limits - Servers may limit concurrent connections per client.Error handling - If one batch fails, others can still succeed.Progress tracking - Batches provide natural checkpoints for progress updates.Resource fairness - Prevents one download job from monopolizing all system resources.
Progress callbacks fire after each batch completes:
for i in range(0, len(image_urls), chunk_size): chunk = image_urls[i:i+chunk_size] tasks = [...] res = await asyncio.gather(*tasks) results.extend(res) # Called here after each batch if progress_callback: progress_callback(min(i + chunk_size, len(image_urls)), len(image_urls))
For 100 images with BATCH_SIZE=10, the callback fires 10 times:
Cancellation is checked between batches, not during:
for i in range(0, len(image_urls), chunk_size): # Check before starting next batch if check_cancel and check_cancel(): log_callback("[INFO] Process cancelled by user.") break # This batch will complete even if cancelled during execution chunk = image_urls[i:i+chunk_size] res = await asyncio.gather(*tasks)
Cancellation is not instantaneous. The current batch must complete before cancellation takes effect. This ensures files aren’t left in a corrupted state.
# 95 files returned, 5 returned Nonefiles = [f for f in results if f] # Length: 95files.sort()if files: # PDF is created with 95 images finalize_pdf_flow(files, output_name, log_callback, temp_folder)
Users receive error logs for failed images but still get a usable PDF.
Use shared sessions - Create one session per download job, not per image
# ✅ Goodasync with aiohttp.ClientSession() as session: for url in urls: await download_image(session, url, ...)# ❌ Badfor url in urls: async with aiohttp.ClientSession() as session: await download_image(session, url, ...)
Respect batch sizes - Don’t download hundreds of images simultaneouslyCheck for cancellation - Test check_cancel() between batchesHandle None results - Failed downloads return None, filter them outClose resources properly - Use async with for automatic cleanup