Compare commits

...

4 Commits

Author SHA1 Message Date
Yuvi9587
4bf57eb752 Socks 4 and 5 proxy support 2025-12-24 09:27:01 +05:30
Yuvi9587
de202961a0 Proxy Type Dropdown List 2025-12-24 09:26:43 +05:30
Yuvi9587
e806b6de66 Update deviantart_downloader_thread.py 2025-12-24 09:26:07 +05:30
Yuvi9587
cb8dd3b7f3 Proxy Type Key 2025-12-24 09:26:04 +05:30
4 changed files with 73 additions and 40 deletions

View File

@ -75,6 +75,7 @@ PROXY_HOST_KEY = "proxy/host"
PROXY_PORT_KEY = "proxy/port" PROXY_PORT_KEY = "proxy/port"
PROXY_USERNAME_KEY = "proxy/username" PROXY_USERNAME_KEY = "proxy/username"
PROXY_PASSWORD_KEY = "proxy/password" PROXY_PASSWORD_KEY = "proxy/password"
PROXY_TYPE_KEY = "proxy_type"
# --- UI Constants and Identifiers --- # --- UI Constants and Identifiers ---
HTML_PREFIX = "<!HTML!>" HTML_PREFIX = "<!HTML!>"

View File

@ -3,7 +3,7 @@ import time
import requests import requests
import re import re
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, wait # REMOVED: ThreadPoolExecutor, wait (Not needed for sequential speed)
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from ...core.deviantart_client import DeviantArtClient from ...core.deviantart_client import DeviantArtClient
from ...utils.file_utils import clean_folder_name from ...utils.file_utils import clean_folder_name
@ -21,21 +21,17 @@ class DeviantArtDownloadThread(QThread):
self.pause_event = pause_event self.pause_event = pause_event
self.cancellation_event = cancellation_event self.cancellation_event = cancellation_event
# Pass logger to client so we see "Rate Limit" messages in the UI # Pass logger to client
self.client = DeviantArtClient(logger_func=self.progress_signal.emit) self.client = DeviantArtClient(logger_func=self.progress_signal.emit)
self.parent_app = parent self.parent_app = parent
self.download_count = 0 self.download_count = 0
self.skip_count = 0 self.skip_count = 0
# --- THREAD SETTINGS ---
# STRICTLY 1 THREAD (Sequential) to match 1.py and avoid Rate Limits
self.max_threads = 1
def run(self): def run(self):
self.progress_signal.emit("=" * 40) self.progress_signal.emit("=" * 40)
self.progress_signal.emit(f"🚀 Starting DeviantArt download for: {self.url}") self.progress_signal.emit(f"🚀 Starting DeviantArt download for: {self.url}")
self.progress_signal.emit(f" Mode: Sequential (1 thread) to prevent 429 errors.") self.progress_signal.emit(f" Mode: High-Speed Sequential (Matches 1.py)")
try: try:
if not self.client.authenticate(): if not self.client.authenticate():
@ -91,28 +87,24 @@ class DeviantArtDownloadThread(QThread):
if not os.path.exists(base_folder): if not os.path.exists(base_folder):
os.makedirs(base_folder, exist_ok=True) os.makedirs(base_folder, exist_ok=True)
with ThreadPoolExecutor(max_workers=self.max_threads) as executor: # --- OPTIMIZED LOOP (Matches 1.py structure) ---
while has_more: while has_more:
if self._check_pause_cancel(): break
data = self.client.get_gallery_folder(username, offset=offset)
results = data.get('results', [])
has_more = data.get('has_more', False)
offset = data.get('next_offset')
if not results: break
# DIRECT LOOP - No ThreadPoolExecutor overhead
for deviation in results:
if self._check_pause_cancel(): break if self._check_pause_cancel(): break
self._process_deviation_task(deviation, base_folder)
data = self.client.get_gallery_folder(username, offset=offset) # Be nice to API (1 second sleep per batch of 24)
results = data.get('results', []) time.sleep(1)
has_more = data.get('has_more', False)
offset = data.get('next_offset')
if not results: break
futures = []
for deviation in results:
if self._check_pause_cancel(): break
future = executor.submit(self._process_deviation_task, deviation, base_folder)
futures.append(future)
# Wait for this batch to finish before getting the next page
wait(futures)
# Match 1.py: Sleep 1s between pages to be nice to API
time.sleep(1)
def _process_deviation_task(self, deviation, base_folder): def _process_deviation_task(self, deviation, base_folder):
if self._check_pause_cancel(): return if self._check_pause_cancel(): return
@ -121,7 +113,7 @@ class DeviantArtDownloadThread(QThread):
title = deviation.get('title', 'Unknown') title = deviation.get('title', 'Unknown')
try: try:
# This handles the fallback logic internally # Try to get content (Handles fallback internally now)
content = self.client.get_deviation_content(dev_id) content = self.client.get_deviation_content(dev_id)
if content: if content:
self._download_file(content['src'], deviation, override_dir=base_folder) self._download_file(content['src'], deviation, override_dir=base_folder)
@ -155,7 +147,6 @@ class DeviantArtDownloadThread(QThread):
final_filename = f"{safe_title}{ext}" final_filename = f"{safe_title}{ext}"
# Naming logic
if self.parent_app and self.parent_app.manga_mode_checkbox.isChecked(): if self.parent_app and self.parent_app.manga_mode_checkbox.isChecked():
try: try:
creator_name = metadata.get('author', {}).get('username', 'Unknown') creator_name = metadata.get('author', {}).get('username', 'Unknown')
@ -177,7 +168,8 @@ class DeviantArtDownloadThread(QThread):
final_filename = f"{clean_folder_name(new_name)}{ext}" final_filename = f"{clean_folder_name(new_name)}{ext}"
except Exception as e: except Exception as e:
self.progress_signal.emit(f" ⚠️ Renaming failed ({e}), using default.") # Reduced logging verbosity slightly for speed
pass
save_dir = override_dir if override_dir else self.output_dir save_dir = override_dir if override_dir else self.output_dir
if not os.path.exists(save_dir): if not os.path.exists(save_dir):

View File

@ -249,13 +249,22 @@ class FutureSettingsDialog(QDialog):
self.proxy_enabled_checkbox.stateChanged.connect(self._proxy_setting_changed) self.proxy_enabled_checkbox.stateChanged.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_enabled_checkbox, 0, 0, 1, 2) proxy_layout.addWidget(self.proxy_enabled_checkbox, 0, 0, 1, 2)
# Proxy Type Dropdown
self.proxy_type_label = QLabel("Proxy Type:")
self.proxy_type_combo = QComboBox()
self.proxy_type_combo.addItems(["HTTP", "SOCKS4", "SOCKS5"])
self.proxy_type_combo.currentIndexChanged.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_type_label, 1, 0)
proxy_layout.addWidget(self.proxy_type_combo, 1, 1)
# Host / IP # Host / IP
self.proxy_host_label = QLabel() self.proxy_host_label = QLabel()
self.proxy_host_input = QLineEdit() self.proxy_host_input = QLineEdit()
self.proxy_host_input.setPlaceholderText("127.0.0.1") self.proxy_host_input.setPlaceholderText("127.0.0.1")
self.proxy_host_input.editingFinished.connect(self._proxy_setting_changed) self.proxy_host_input.editingFinished.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_host_label, 1, 0) proxy_layout.addWidget(self.proxy_host_label, 2, 0) # Changed row to 2
proxy_layout.addWidget(self.proxy_host_input, 1, 1) proxy_layout.addWidget(self.proxy_host_input, 2, 1)
# Port # Port
self.proxy_port_label = QLabel() self.proxy_port_label = QLabel()
@ -263,16 +272,16 @@ class FutureSettingsDialog(QDialog):
self.proxy_port_input.setPlaceholderText("8080") self.proxy_port_input.setPlaceholderText("8080")
self.proxy_port_input.setValidator(QIntValidator(1, 65535, self)) # Only numbers self.proxy_port_input.setValidator(QIntValidator(1, 65535, self)) # Only numbers
self.proxy_port_input.editingFinished.connect(self._proxy_setting_changed) self.proxy_port_input.editingFinished.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_port_label, 2, 0) proxy_layout.addWidget(self.proxy_port_label, 3, 0)
proxy_layout.addWidget(self.proxy_port_input, 2, 1) proxy_layout.addWidget(self.proxy_port_input, 3, 1)
# Username # Username
self.proxy_user_label = QLabel() self.proxy_user_label = QLabel()
self.proxy_user_input = QLineEdit() self.proxy_user_input = QLineEdit()
self.proxy_user_input.setPlaceholderText("(Optional)") self.proxy_user_input.setPlaceholderText("(Optional)")
self.proxy_user_input.editingFinished.connect(self._proxy_setting_changed) self.proxy_user_input.editingFinished.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_user_label, 3, 0) proxy_layout.addWidget(self.proxy_user_label, 4, 0)
proxy_layout.addWidget(self.proxy_user_input, 3, 1) proxy_layout.addWidget(self.proxy_user_input, 4, 1)
# Password # Password
self.proxy_pass_label = QLabel() self.proxy_pass_label = QLabel()
@ -280,8 +289,8 @@ class FutureSettingsDialog(QDialog):
self.proxy_pass_input.setPlaceholderText("(Optional)") self.proxy_pass_input.setPlaceholderText("(Optional)")
self.proxy_pass_input.setEchoMode(QLineEdit.Password) # Mask input self.proxy_pass_input.setEchoMode(QLineEdit.Password) # Mask input
self.proxy_pass_input.editingFinished.connect(self._proxy_setting_changed) self.proxy_pass_input.editingFinished.connect(self._proxy_setting_changed)
proxy_layout.addWidget(self.proxy_pass_label, 4, 0) proxy_layout.addWidget(self.proxy_pass_label, 5, 0)
proxy_layout.addWidget(self.proxy_pass_input, 4, 1) proxy_layout.addWidget(self.proxy_pass_input, 5, 1)
network_tab_layout.addWidget(self.proxy_group_box) network_tab_layout.addWidget(self.proxy_group_box)
network_tab_layout.addStretch(1) network_tab_layout.addStretch(1)
@ -379,19 +388,32 @@ class FutureSettingsDialog(QDialog):
# --- START: New Proxy Logic --- # --- START: New Proxy Logic ---
def _load_proxy_settings(self): def _load_proxy_settings(self):
"""Loads proxy settings from QSettings into the UI.""" """Loads proxy settings from QSettings into the UI."""
# Block signals to prevent triggering auto-save while loading
self.proxy_enabled_checkbox.blockSignals(True) self.proxy_enabled_checkbox.blockSignals(True)
self.proxy_type_combo.blockSignals(True) # <--- NEW
self.proxy_host_input.blockSignals(True) self.proxy_host_input.blockSignals(True)
self.proxy_port_input.blockSignals(True) self.proxy_port_input.blockSignals(True)
self.proxy_user_input.blockSignals(True) self.proxy_user_input.blockSignals(True)
self.proxy_pass_input.blockSignals(True) self.proxy_pass_input.blockSignals(True)
# Load values
enabled = self.parent_app.settings.value(PROXY_ENABLED_KEY, False, type=bool) enabled = self.parent_app.settings.value(PROXY_ENABLED_KEY, False, type=bool)
proxy_type = self.parent_app.settings.value("proxy_type", "HTTP", type=str) # <--- NEW
host = self.parent_app.settings.value(PROXY_HOST_KEY, "", type=str) host = self.parent_app.settings.value(PROXY_HOST_KEY, "", type=str)
port = self.parent_app.settings.value(PROXY_PORT_KEY, "", type=str) port = self.parent_app.settings.value(PROXY_PORT_KEY, "", type=str)
user = self.parent_app.settings.value(PROXY_USERNAME_KEY, "", type=str) user = self.parent_app.settings.value(PROXY_USERNAME_KEY, "", type=str)
password = self.parent_app.settings.value(PROXY_PASSWORD_KEY, "", type=str) password = self.parent_app.settings.value(PROXY_PASSWORD_KEY, "", type=str)
# Apply values to UI
self.proxy_enabled_checkbox.setChecked(enabled) self.proxy_enabled_checkbox.setChecked(enabled)
# <--- NEW: Set the dropdown selection
index = self.proxy_type_combo.findText(proxy_type)
if index >= 0:
self.proxy_type_combo.setCurrentIndex(index)
else:
self.proxy_type_combo.setCurrentIndex(0) # Default to first item if not found
self.proxy_host_input.setText(host) self.proxy_host_input.setText(host)
self.proxy_port_input.setText(port) self.proxy_port_input.setText(port)
self.proxy_user_input.setText(user) self.proxy_user_input.setText(user)
@ -399,7 +421,9 @@ class FutureSettingsDialog(QDialog):
self._update_proxy_fields_state(enabled) self._update_proxy_fields_state(enabled)
# Unblock signals
self.proxy_enabled_checkbox.blockSignals(False) self.proxy_enabled_checkbox.blockSignals(False)
self.proxy_type_combo.blockSignals(False) # <--- NEW
self.proxy_host_input.blockSignals(False) self.proxy_host_input.blockSignals(False)
self.proxy_port_input.blockSignals(False) self.proxy_port_input.blockSignals(False)
self.proxy_user_input.blockSignals(False) self.proxy_user_input.blockSignals(False)
@ -408,16 +432,19 @@ class FutureSettingsDialog(QDialog):
def _proxy_setting_changed(self): def _proxy_setting_changed(self):
"""Saves the current proxy UI state to QSettings.""" """Saves the current proxy UI state to QSettings."""
enabled = self.proxy_enabled_checkbox.isChecked() enabled = self.proxy_enabled_checkbox.isChecked()
proxy_type = self.proxy_type_combo.currentText() # <--- NEW
host = self.proxy_host_input.text().strip() host = self.proxy_host_input.text().strip()
port = self.proxy_port_input.text().strip() port = self.proxy_port_input.text().strip()
user = self.proxy_user_input.text().strip() user = self.proxy_user_input.text().strip()
password = self.proxy_pass_input.text().strip() password = self.proxy_pass_input.text().strip()
self.parent_app.settings.setValue(PROXY_ENABLED_KEY, enabled) self.parent_app.settings.setValue(PROXY_ENABLED_KEY, enabled)
self.parent_app.settings.setValue("proxy_type", proxy_type) # <--- NEW
self.parent_app.settings.setValue(PROXY_HOST_KEY, host) self.parent_app.settings.setValue(PROXY_HOST_KEY, host)
self.parent_app.settings.setValue(PROXY_PORT_KEY, port) self.parent_app.settings.setValue(PROXY_PORT_KEY, port)
self.parent_app.settings.setValue(PROXY_USERNAME_KEY, user) self.parent_app.settings.setValue(PROXY_USERNAME_KEY, user)
self.parent_app.settings.setValue(PROXY_PASSWORD_KEY, password) self.parent_app.settings.setValue(PROXY_PASSWORD_KEY, password)
self.parent_app.settings.sync() self.parent_app.settings.sync()
self._update_proxy_fields_state(enabled) self._update_proxy_fields_state(enabled)
@ -427,6 +454,7 @@ class FutureSettingsDialog(QDialog):
def _update_proxy_fields_state(self, enabled): def _update_proxy_fields_state(self, enabled):
"""Enables or disables input fields based on the checkbox.""" """Enables or disables input fields based on the checkbox."""
self.proxy_type_combo.setEnabled(enabled)
self.proxy_host_input.setEnabled(enabled) self.proxy_host_input.setEnabled(enabled)
self.proxy_port_input.setEnabled(enabled) self.proxy_port_input.setEnabled(enabled)
self.proxy_user_input.setEnabled(enabled) self.proxy_user_input.setEnabled(enabled)

View File

@ -849,12 +849,24 @@ class DownloaderApp (QWidget ):
settings['proxy_port'] = self.settings.value(PROXY_PORT_KEY, "", type=str) settings['proxy_port'] = self.settings.value(PROXY_PORT_KEY, "", type=str)
settings['proxy_username'] = self.settings.value(PROXY_USERNAME_KEY, "", type=str) settings['proxy_username'] = self.settings.value(PROXY_USERNAME_KEY, "", type=str)
settings['proxy_password'] = self.settings.value(PROXY_PASSWORD_KEY, "", type=str) settings['proxy_password'] = self.settings.value(PROXY_PASSWORD_KEY, "", type=str)
proxy_type_str = self.settings.value("proxy_type", "HTTP", type=str)
settings['proxies'] = None settings['proxies'] = None
if settings['proxy_enabled'] and settings['proxy_host'] and settings['proxy_port']: if settings['proxy_enabled'] and settings['proxy_host'] and settings['proxy_port']:
proxy_str = f"http://{settings['proxy_host']}:{settings['proxy_port']}"
# Determine correct scheme
scheme = "http"
if proxy_type_str == "SOCKS5":
scheme = "socks5h" # 'socks5h' forces remote DNS resolution (safer/better for bypassing)
elif proxy_type_str == "SOCKS4":
scheme = "socks4"
# Build URL string
if settings['proxy_username'] and settings['proxy_password']: if settings['proxy_username'] and settings['proxy_password']:
proxy_str = f"http://{settings['proxy_username']}:{settings['proxy_password']}@{settings['proxy_host']}:{settings['proxy_port']}" proxy_str = f"{scheme}://{settings['proxy_username']}:{settings['proxy_password']}@{settings['proxy_host']}:{settings['proxy_port']}"
else:
proxy_str = f"{scheme}://{settings['proxy_host']}:{settings['proxy_port']}"
settings['proxies'] = {'http': proxy_str, 'https': proxy_str} settings['proxies'] = {'http': proxy_str, 'https': proxy_str}
return settings return settings