From 77bd428b9106e217347ee12a14e4f22c23534737 Mon Sep 17 00:00:00 2001 From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:56:04 +0530 Subject: [PATCH] Commit --- src/core/allcomic_client.py | 19 ++- src/core/api_client.py | 19 ++- src/core/deviantart_client.py | 42 +++--- src/core/nhentai_client.py | 34 ++--- src/core/workers.py | 8 +- src/ui/classes/allcomic_downloader_thread.py | 37 ++++-- .../classes/deviantart_downloader_thread.py | 34 +++-- src/ui/classes/nhentai_downloader_thread.py | 72 ++++++----- src/ui/dialogs/HelpGuideDialog.py | 56 +++++++- src/ui/dialogs/UpdateCheckDialog.py | 57 ++++---- src/ui/main_window.py | 122 +++++++++++++----- 11 files changed, 348 insertions(+), 152 deletions(-) diff --git a/src/core/allcomic_client.py b/src/core/allcomic_client.py index 98de4e2..f160e71 100644 --- a/src/core/allcomic_client.py +++ b/src/core/allcomic_client.py @@ -5,7 +5,8 @@ import time import random from urllib.parse import urlparse -def get_chapter_list(scraper, series_url, logger_func): +# 1. Update arguments to accept proxies=None +def get_chapter_list(scraper, series_url, logger_func, proxies=None): """ Checks if a URL is a series page and returns a list of all chapter URLs if it is. Relies on a passed-in scraper session for connection. @@ -16,9 +17,13 @@ def get_chapter_list(scraper, series_url, logger_func): response = None max_retries = 8 + # 2. Define smart timeout logic + req_timeout = (30, 120) if proxies else 30 + for attempt in range(max_retries): try: - response = scraper.get(series_url, headers=headers, timeout=30) + # 3. Add proxies, verify=False, and the new timeout + response = scraper.get(series_url, headers=headers, timeout=req_timeout, proxies=proxies, verify=False) response.raise_for_status() logger_func(f" [AllComic] Successfully connected to series page on attempt {attempt + 1}.") break @@ -53,7 +58,8 @@ def get_chapter_list(scraper, series_url, logger_func): logger_func(f" [AllComic] ❌ Error parsing chapters after successful connection: {e}") return [] -def fetch_chapter_data(scraper, chapter_url, logger_func): +# 4. Update arguments here too +def fetch_chapter_data(scraper, chapter_url, logger_func, proxies=None): """ Fetches the comic title, chapter title, and image URLs for a single chapter page. Relies on a passed-in scraper session for connection. @@ -64,9 +70,14 @@ def fetch_chapter_data(scraper, chapter_url, logger_func): response = None max_retries = 8 + + # 5. Define smart timeout logic again + req_timeout = (30, 120) if proxies else 30 + for attempt in range(max_retries): try: - response = scraper.get(chapter_url, headers=headers, timeout=30) + # 6. Add proxies, verify=False, and timeout + response = scraper.get(chapter_url, headers=headers, timeout=req_timeout, proxies=proxies, verify=False) response.raise_for_status() break except requests.RequestException as e: diff --git a/src/core/api_client.py b/src/core/api_client.py index 01257d8..c192680 100644 --- a/src/core/api_client.py +++ b/src/core/api_client.py @@ -40,8 +40,11 @@ def fetch_posts_paginated(api_url_base, headers, offset, logger, cancellation_ev log_message += f" (Attempt {attempt + 1}/{max_retries})" logger(log_message) + request_timeout = (30, 120) if proxies else (15, 60) + try: - with requests.get(paginated_url, headers=headers, timeout=(15, 60), cookies=cookies_dict, proxies=proxies) as response: + with requests.get(paginated_url, headers=headers, timeout=request_timeout, cookies=cookies_dict, proxies=proxies, verify=False) as response: + response.raise_for_status() response.encoding = 'utf-8' return response.json() @@ -92,7 +95,11 @@ def fetch_single_post_data(api_domain, service, user_id, post_id, headers, logge scraper = None try: scraper = cloudscraper.create_scraper() - response = scraper.get(post_api_url, headers=headers, timeout=(15, 300), cookies=cookies_dict, proxies=proxies) + # Keep the 300s read timeout for both, but increase connect timeout for proxies + request_timeout = (30, 300) if proxies else (15, 300) + + response = scraper.get(post_api_url, headers=headers, timeout=request_timeout, cookies=cookies_dict, proxies=proxies, verify=False) + response.raise_for_status() full_post_data = response.json() @@ -120,7 +127,9 @@ def fetch_post_comments(api_domain, service, user_id, post_id, headers, logger, logger(f" Fetching comments: {comments_api_url}") try: - with requests.get(comments_api_url, headers=headers, timeout=(10, 30), cookies=cookies_dict, proxies=proxies) as response: + request_timeout = (30, 60) if proxies else (10, 30) + + with requests.get(comments_api_url, headers=headers, timeout=request_timeout, cookies=cookies_dict, proxies=proxies, verify=False) as response: response.raise_for_status() response.encoding = 'utf-8' return response.json() @@ -180,7 +189,9 @@ def download_from_api( direct_post_api_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}/post/{target_post_id}" logger(f" Attempting direct fetch for target post: {direct_post_api_url}") try: - with requests.get(direct_post_api_url, headers=headers, timeout=(10, 30), cookies=cookies_for_api, proxies=proxies) as direct_response: + request_timeout = (30, 60) if proxies else (10, 30) + + with requests.get(direct_post_api_url, headers=headers, timeout=request_timeout, cookies=cookies_for_api, proxies=proxies, verify=False) as direct_response: direct_response.raise_for_status() direct_response.encoding = 'utf-8' direct_post_data = direct_response.json() diff --git a/src/core/deviantart_client.py b/src/core/deviantart_client.py index 146ae8e..80b8601 100644 --- a/src/core/deviantart_client.py +++ b/src/core/deviantart_client.py @@ -11,9 +11,18 @@ class DeviantArtClient: CLIENT_SECRET = "76b08c69cfb27f26d6161f9ab6d061a1" BASE_API = "https://www.deviantart.com/api/v1/oauth2" - def __init__(self, logger_func=print): + # 1. Accept proxies in init + def __init__(self, logger_func=print, proxies=None): self.session = requests.Session() - # Headers matching 1.py (Firefox) + + # 2. Configure Session with Proxy & SSL settings immediately + if proxies: + self.session.proxies.update(proxies) + self.session.verify = False # Ignore SSL for proxies + self.proxies_enabled = True + else: + self.proxies_enabled = False + self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8", @@ -41,7 +50,10 @@ class DeviantArtClient: "client_id": self.CLIENT_ID, "client_secret": self.CLIENT_SECRET } - resp = self.session.post(url, data=data, timeout=10) + # 3. Smart timeout (longer if proxy) + req_timeout = 30 if self.proxies_enabled else 10 + + resp = self.session.post(url, data=data, timeout=req_timeout) resp.raise_for_status() data = resp.json() self.access_token = data.get("access_token") @@ -63,18 +75,22 @@ class DeviantArtClient: retries = 0 max_retries = 4 backoff_delay = 2 + + # 4. Smart timeout + req_timeout = 30 if self.proxies_enabled else 20 while True: try: - resp = self.session.get(url, params=params, timeout=20) + resp = self.session.get(url, params=params, timeout=req_timeout) - # 429: Rate Limit (Retry infinitely like 1.py) + # 429: Rate Limit if resp.status_code == 429: retry_after = resp.headers.get('Retry-After') if retry_after: - sleep_time = int(retry_after) + 1 + sleep_time = int(retry_after) + 2 # Add buffer else: - sleep_time = 5 # Default sleep from 1.py + # 5. Increase default wait time for 429s + sleep_time = 15 self._log_once(sleep_time, f" [DeviantArt] ⚠️ Rate limit (429). Sleeping {sleep_time}s...") time.sleep(sleep_time) @@ -90,7 +106,7 @@ class DeviantArtClient: raise Exception("Failed to refresh token") if 400 <= resp.status_code < 500: - resp.raise_for_status() # This raises immediately, breaking the loop + resp.raise_for_status() if 500 <= resp.status_code < 600: resp.raise_for_status() @@ -105,12 +121,9 @@ class DeviantArtClient: except requests.exceptions.HTTPError as e: if e.response is not None and 400 <= e.response.status_code < 500: raise e - - # Otherwise fall through to general retry logic (for 5xx) pass except requests.exceptions.RequestException as e: - # Network errors / 5xx errors -> Retry if retries < max_retries: self._log_once("conn_error", f" [DeviantArt] Connection error: {e}. Retrying...") time.sleep(backoff_delay) @@ -131,7 +144,8 @@ class DeviantArtClient: def get_deviation_uuid(self, url): """Scrapes the deviation page to find the UUID.""" try: - resp = self.session.get(url, timeout=15) + req_timeout = 30 if self.proxies_enabled else 15 + resp = self.session.get(url, timeout=req_timeout) match = re.search(r'"deviationUuid":"([^"]+)"', resp.text) if match: return match.group(1) @@ -144,17 +158,13 @@ class DeviantArtClient: def get_deviation_content(self, uuid): """Fetches download info.""" - # 1. Try high-res download endpoint try: data = self._api_call(f"/deviation/download/{uuid}") if 'src' in data: return data except: - # If 400/403 (Not downloadable), we fail silently here - # and proceed to step 2 (Metadata fallback) pass - # 2. Fallback to standard content try: meta = self._api_call(f"/deviation/{uuid}") if 'content' in meta: diff --git a/src/core/nhentai_client.py b/src/core/nhentai_client.py index 945bf62..ad822f3 100644 --- a/src/core/nhentai_client.py +++ b/src/core/nhentai_client.py @@ -1,31 +1,35 @@ import requests -import cloudscraper import json -def fetch_nhentai_gallery(gallery_id, logger=print): +# 1. Update arguments to accept proxies=None +def fetch_nhentai_gallery(gallery_id, logger=print, proxies=None): """ - Fetches the metadata for a single nhentai gallery using cloudscraper to bypass Cloudflare. - - Args: - gallery_id (str or int): The ID of the nhentai gallery. - logger (function): A function to log progress and error messages. - - Returns: - dict: A dictionary containing the gallery's metadata if successful, otherwise None. + Fetches the metadata for a single nhentai gallery. + Switched to standard requests to support proxies with self-signed certs. """ api_url = f"https://nhentai.net/api/gallery/{gallery_id}" - scraper = cloudscraper.create_scraper() + # 2. Use a real User-Agent to avoid immediate blocking + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' + } logger(f" Fetching nhentai gallery metadata from: {api_url}") + # 3. Smart timeout logic + req_timeout = (30, 120) if proxies else 20 + try: - # Use the scraper to make the GET request - response = scraper.get(api_url, timeout=20) + # 4. Use requests.get with proxies, verify=False, and timeout + response = requests.get(api_url, headers=headers, timeout=req_timeout, proxies=proxies, verify=False) if response.status_code == 404: logger(f" ❌ Gallery not found (404): ID {gallery_id}") return None + elif response.status_code == 403: + logger(f" ❌ Access Denied (403): Cloudflare blocked the request. Try a different proxy or User-Agent.") + return None response.raise_for_status() @@ -36,9 +40,9 @@ def fetch_nhentai_gallery(gallery_id, logger=print): gallery_data['pages'] = gallery_data.pop('images')['pages'] return gallery_data else: - logger(" ❌ API response is missing essential keys (id, media_id, or images).") + logger(" ❌ API response is missing essential keys (id, media_id, images).") return None except Exception as e: - logger(f" ❌ An error occurred while fetching gallery {gallery_id}: {e}") + logger(f" ❌ Error fetching nhentai metadata: {e}") return None \ No newline at end of file diff --git a/src/core/workers.py b/src/core/workers.py index 290bc44..43fcd93 100644 --- a/src/core/workers.py +++ b/src/core/workers.py @@ -263,7 +263,7 @@ class PostProcessorWorker: new_url = parsed_url._replace(netloc=new_domain).geturl() try: - with requests.head(new_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=5, allow_redirects=True, proxies=self.proxies) as resp: + with requests.head(new_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=5, allow_redirects=True, proxies=self.proxies, verify=False) as resp: if resp.status_code == 200: return new_url except requests.RequestException: @@ -338,7 +338,7 @@ class PostProcessorWorker: api_original_filename_for_size_check = file_info.get('_original_name_for_log', file_info.get('name')) try: # Use a stream=True HEAD request to get headers without downloading the body - with requests.head(file_url, headers=file_download_headers, timeout=15, cookies=cookies_to_use_for_file, allow_redirects=True, proxies=self.proxies) as head_response: + with requests.head(file_url, headers=file_download_headers, timeout=15, cookies=cookies_to_use_for_file, allow_redirects=True, proxies=self.proxies, verify=False) as head_response: head_response.raise_for_status() content_length = head_response.headers.get('Content-Length') @@ -673,7 +673,7 @@ class PostProcessorWorker: current_url_to_try = file_url - response = requests.get(current_url_to_try, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies) + response = requests.get(current_url_to_try, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies, verify=False) if response.status_code == 403 and ('kemono.' in current_url_to_try or 'coomer.' in current_url_to_try): self.logger(f" ⚠️ Got 403 Forbidden for '{api_original_filename}'. Attempting subdomain rotation...") @@ -682,7 +682,7 @@ class PostProcessorWorker: self.logger(f" Retrying with new URL: {new_url}") file_url = new_url response.close() # Close the old response - response = requests.get(new_url, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies) + response = requests.get(new_url, headers=file_download_headers, timeout=(30, 300), stream=True, cookies=cookies_to_use_for_file, proxies=self.proxies, verify=False) response.raise_for_status() # --- REVISED AND MOVED SIZE CHECK LOGIC --- diff --git a/src/ui/classes/allcomic_downloader_thread.py b/src/ui/classes/allcomic_downloader_thread.py index 00cbc2e..129279e 100644 --- a/src/ui/classes/allcomic_downloader_thread.py +++ b/src/ui/classes/allcomic_downloader_thread.py @@ -19,12 +19,14 @@ class AllcomicDownloadThread(QThread): finished_signal = pyqtSignal(int, int, bool) overall_progress_signal = pyqtSignal(int, int) - def __init__(self, url, output_dir, parent=None): + # 1. Update __init__ to accept proxies + def __init__(self, url, output_dir, parent=None, proxies=None): super().__init__(parent) self.comic_url = url self.output_dir = output_dir self.is_cancelled = False self.pause_event = parent.pause_event if hasattr(parent, 'pause_event') else threading.Event() + self.proxies = proxies # Store the proxies def _check_pause(self): if self.is_cancelled: return True @@ -40,13 +42,19 @@ class AllcomicDownloadThread(QThread): grand_total_dl = 0 grand_total_skip = 0 - # Create the scraper session ONCE for the entire job - scraper = cloudscraper.create_scraper( - browser={'browser': 'firefox', 'platform': 'windows', 'desktop': True} - ) + if self.proxies: + self.progress_signal.emit(f" 🌍 Network: Using Proxy {self.proxies}") + else: + self.progress_signal.emit(" 🌍 Network: Direct Connection (No Proxy)") - # Pass the scraper to the function - chapters_to_download = allcomic_get_list(scraper, self.comic_url, self.progress_signal.emit) + scraper = requests.Session() + scraper.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' + }) + + # 2. Pass self.proxies to get_chapter_list + chapters_to_download = allcomic_get_list(scraper, self.comic_url, self.progress_signal.emit, proxies=self.proxies) if not chapters_to_download: chapters_to_download = [self.comic_url] @@ -57,8 +65,9 @@ class AllcomicDownloadThread(QThread): if self._check_pause(): break self.progress_signal.emit(f"\n-- Processing Chapter {chapter_idx + 1}/{len(chapters_to_download)} --") - # Pass the scraper to the function - comic_title, chapter_title, image_urls = allcomic_fetch_data(scraper, chapter_url, self.progress_signal.emit) + + # 3. Pass self.proxies to fetch_chapter_data + comic_title, chapter_title, image_urls = allcomic_fetch_data(scraper, chapter_url, self.progress_signal.emit, proxies=self.proxies) if not image_urls: self.progress_signal.emit(f"❌ Failed to get data for chapter. Skipping.") @@ -80,6 +89,9 @@ class AllcomicDownloadThread(QThread): self.overall_progress_signal.emit(total_files_in_chapter, 0) headers = {'Referer': chapter_url} + # 4. Define smart timeout for images + img_timeout = (30, 120) if self.proxies else 60 + for i, img_url in enumerate(image_urls): if self._check_pause(): break @@ -97,8 +109,9 @@ class AllcomicDownloadThread(QThread): if self._check_pause(): break try: self.progress_signal.emit(f" Downloading ({i+1}/{total_files_in_chapter}): '{filename}' (Attempt {attempt + 1})...") - # Use the persistent scraper object - response = scraper.get(img_url, stream=True, headers=headers, timeout=60) + + # 5. Use proxies, verify=False, and new timeout + response = scraper.get(img_url, stream=True, headers=headers, timeout=img_timeout, proxies=self.proxies, verify=False) response.raise_for_status() with open(filepath, 'wb') as f: @@ -125,7 +138,7 @@ class AllcomicDownloadThread(QThread): grand_total_skip += 1 self.overall_progress_signal.emit(total_files_in_chapter, i + 1) - time.sleep(0.5) # Increased delay between images for this site + time.sleep(0.5) if self._check_pause(): break diff --git a/src/ui/classes/deviantart_downloader_thread.py b/src/ui/classes/deviantart_downloader_thread.py index 243b8d0..58c2037 100644 --- a/src/ui/classes/deviantart_downloader_thread.py +++ b/src/ui/classes/deviantart_downloader_thread.py @@ -2,8 +2,8 @@ import os import time import requests import re +import random # Needed for random delays from datetime import datetime -# REMOVED: ThreadPoolExecutor, wait (Not needed for sequential speed) from PyQt5.QtCore import QThread, pyqtSignal from ...core.deviantart_client import DeviantArtClient from ...utils.file_utils import clean_folder_name @@ -14,24 +14,29 @@ class DeviantArtDownloadThread(QThread): overall_progress_signal = pyqtSignal(int, int) finished_signal = pyqtSignal(int, int, bool, list) - def __init__(self, url, output_dir, pause_event, cancellation_event, parent=None): + # 1. Accept proxies in init + def __init__(self, url, output_dir, pause_event, cancellation_event, parent=None, proxies=None): super().__init__(parent) self.url = url self.output_dir = output_dir self.pause_event = pause_event self.cancellation_event = cancellation_event - - # Pass logger to client - self.client = DeviantArtClient(logger_func=self.progress_signal.emit) - + self.proxies = proxies # Store proxies + self.parent_app = parent self.download_count = 0 self.skip_count = 0 def run(self): + self.client = DeviantArtClient(logger_func=self.progress_signal.emit, proxies=self.proxies) + + if self.proxies: + self.progress_signal.emit(f" 🌍 Network: Using Proxy {self.proxies}") + else: + self.progress_signal.emit(" 🌍 Network: Direct Connection") + self.progress_signal.emit("=" * 40) self.progress_signal.emit(f"🚀 Starting DeviantArt download for: {self.url}") - self.progress_signal.emit(f" ℹ️ Mode: High-Speed Sequential (Matches 1.py)") try: if not self.client.authenticate(): @@ -87,7 +92,6 @@ class DeviantArtDownloadThread(QThread): if not os.path.exists(base_folder): os.makedirs(base_folder, exist_ok=True) - # --- OPTIMIZED LOOP (Matches 1.py structure) --- while has_more: if self._check_pause_cancel(): break @@ -98,12 +102,14 @@ class DeviantArtDownloadThread(QThread): if not results: break - # DIRECT LOOP - No ThreadPoolExecutor overhead for deviation in results: if self._check_pause_cancel(): break self._process_deviation_task(deviation, base_folder) + + # 4. FIX 429: Add a small random delay between items + # This prevents hammering the API 24 times in a single second. + time.sleep(random.uniform(0.5, 1.2)) - # Be nice to API (1 second sleep per batch of 24) time.sleep(1) def _process_deviation_task(self, deviation, base_folder): @@ -113,7 +119,6 @@ class DeviantArtDownloadThread(QThread): title = deviation.get('title', 'Unknown') try: - # Try to get content (Handles fallback internally now) content = self.client.get_deviation_content(dev_id) if content: self._download_file(content['src'], deviation, override_dir=base_folder) @@ -168,7 +173,6 @@ class DeviantArtDownloadThread(QThread): final_filename = f"{clean_folder_name(new_name)}{ext}" except Exception as e: - # Reduced logging verbosity slightly for speed pass save_dir = override_dir if override_dir else self.output_dir @@ -185,7 +189,11 @@ class DeviantArtDownloadThread(QThread): try: self.progress_signal.emit(f" ⬇️ Downloading: {final_filename}") - with requests.get(file_url, stream=True, timeout=30) as r: + # 5. Determine smart timeout for files + timeout_val = (30, 120) if self.proxies else 30 + + # 6. Use proxies and verify=False + with requests.get(file_url, stream=True, timeout=timeout_val, proxies=self.proxies, verify=False) as r: r.raise_for_status() with open(filepath, 'wb') as f: diff --git a/src/ui/classes/nhentai_downloader_thread.py b/src/ui/classes/nhentai_downloader_thread.py index 6056fa1..47986f9 100644 --- a/src/ui/classes/nhentai_downloader_thread.py +++ b/src/ui/classes/nhentai_downloader_thread.py @@ -1,6 +1,6 @@ import os import time -import cloudscraper +import requests from PyQt5.QtCore import QThread, pyqtSignal from ...utils.file_utils import clean_folder_name @@ -17,68 +17,78 @@ class NhentaiDownloadThread(QThread): EXTENSION_MAP = {'j': 'jpg', 'p': 'png', 'g': 'gif', 'w': 'webp' } + # 1. Update init to initialize self.proxies def __init__(self, gallery_data, output_dir, parent=None): super().__init__(parent) self.gallery_data = gallery_data self.output_dir = output_dir self.is_cancelled = False + self.proxies = None # Placeholder, will be injected by main_window def run(self): + # 2. Log Proxy Usage + if self.proxies: + self.progress_signal.emit(f" 🌍 Network: Using Proxy {self.proxies}") + else: + self.progress_signal.emit(" 🌍 Network: Direct Connection (No Proxy)") + title = self.gallery_data.get("title", {}).get("english", f"gallery_{self.gallery_data.get('id')}") gallery_id = self.gallery_data.get("id") media_id = self.gallery_data.get("media_id") pages_info = self.gallery_data.get("pages", []) folder_name = clean_folder_name(title) - gallery_path = os.path.join(self.output_dir, folder_name) - + save_path = os.path.join(self.output_dir, folder_name) + try: - os.makedirs(gallery_path, exist_ok=True) - except OSError as e: - self.progress_signal.emit(f"❌ Critical error creating directory: {e}") + os.makedirs(save_path, exist_ok=True) + self.progress_signal.emit(f" Saving to: {folder_name}") + except Exception as e: + self.progress_signal.emit(f" ❌ Error creating directory: {e}") self.finished_signal.emit(0, len(pages_info), False) return - self.progress_signal.emit(f"⬇️ Downloading '{title}' to folder '{folder_name}'...") - - scraper = cloudscraper.create_scraper() download_count = 0 skip_count = 0 + total_pages = len(pages_info) + + # 3. Use requests.Session instead of cloudscraper + scraper = requests.Session() + + # 4. Smart timeout logic + img_timeout = (30, 120) if self.proxies else 60 for i, page_data in enumerate(pages_info): - if self.is_cancelled: - break - - page_num = i + 1 + if self.is_cancelled: break - ext_char = page_data.get('t', 'j') - extension = self.EXTENSION_MAP.get(ext_char, 'jpg') - - relative_path = f"/galleries/{media_id}/{page_num}.{extension}" - - local_filename = f"{page_num:03d}.{extension}" - filepath = os.path.join(gallery_path, local_filename) + file_ext = self.EXTENSION_MAP.get(page_data.get('t'), 'jpg') + local_filename = f"{i+1:03d}.{file_ext}" + filepath = os.path.join(save_path, local_filename) if os.path.exists(filepath): - self.progress_signal.emit(f" -> Skip (Exists): {local_filename}") + self.progress_signal.emit(f" Skipping {local_filename} (already exists).") skip_count += 1 continue download_successful = False + + # Try servers until one works for server in self.IMAGE_SERVERS: - if self.is_cancelled: - break + if self.is_cancelled: break + + # Construct URL: server/galleries/media_id/page_num.ext + full_url = f"{server}/galleries/{media_id}/{i+1}.{file_ext}" - full_url = f"{server}{relative_path}" try: - self.progress_signal.emit(f" Downloading page {page_num}/{len(pages_info)} from {server} ...") + self.progress_signal.emit(f" Downloading page {i+1}/{total_pages}...") headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': f'https://nhentai.net/g/{gallery_id}/' } - response = scraper.get(full_url, headers=headers, timeout=60, stream=True) + # 5. Add proxies, verify=False, and timeout + response = scraper.get(full_url, headers=headers, timeout=img_timeout, stream=True, proxies=self.proxies, verify=False) if response.status_code == 200: with open(filepath, 'wb') as f: @@ -86,12 +96,14 @@ class NhentaiDownloadThread(QThread): f.write(chunk) download_count += 1 download_successful = True - break + break # Stop trying servers else: - self.progress_signal.emit(f" -> {server} returned status {response.status_code}. Trying next server...") + # self.progress_signal.emit(f" -> {server} returned status {response.status_code}...") + pass except Exception as e: - self.progress_signal.emit(f" -> {server} failed to connect or timed out: {e}. Trying next server...") + # self.progress_signal.emit(f" -> {server} failed: {e}") + pass if not download_successful: self.progress_signal.emit(f" ❌ Failed to download {local_filename} from all servers.") diff --git a/src/ui/dialogs/HelpGuideDialog.py b/src/ui/dialogs/HelpGuideDialog.py index 2d9d1fa..9122c18 100644 --- a/src/ui/dialogs/HelpGuideDialog.py +++ b/src/ui/dialogs/HelpGuideDialog.py @@ -73,7 +73,6 @@ class HelpGuideDialog(QDialog):
This feature allows you to queue up multiple distinct downloads with different settings and run them all sequentially.
+ +Before clicking add, configure the download exactly how you want it processed for this specific link:
+✅ Job added to queue.You can repeat steps 1 and 2 as many times as you like. You can even change settings (like the download folder) between adds; the queue remembers the specific settings for each individual link.
+To start processing the queue:
+start queueOnce started, the app will lock the UI, load the first job, download it until finished, and automatically move to the next until the queue is empty.
+ +If you use the Creator Selection popup (the 🎨 button):
+You can add special commands to the "Filter by Character(s)" input field to change download behavior for a single task. Commands are keywords wrapped in square brackets [].
These features provide advanced control over your downloads, sessions, and application settings.
- + +You can now configure a proxy to bypass region blocks or ISP restrictions (e.g., for AllComic or Nhentai).
+Go to Settings ⚙️ > Proxy Tab to set it up:
+This is essential for downloading from sites that require a login (like SimpCity or accessing your favorites on Kemono/Coomer). You can either: