diff --git a/.DS_Store b/.DS_Store
index 640058d..981bcbf 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/core/credentials.py b/core/credentials.py
index 511d427..e78e83a 100644
--- a/core/credentials.py
+++ b/core/credentials.py
@@ -257,7 +257,17 @@ class CredentialManager:
'download_path': str(Path.home() / "Downloads"),
'default_start_page': 1,
'default_end_page': 1,
- 'scraping_mode': 0 # 0=All Comics, 1=Latest Comics
+ 'scraping_mode': 0, # 0=All Comics, 1=Latest Comics
+ # Timing configuration defaults
+ 'action_delay_min': 0.5, # Minimum delay between actions (seconds)
+ 'action_delay_max': 2.0, # Maximum delay between actions (seconds)
+ 'page_break_chance': 70, # Percentage chance of taking a break between pages
+ 'page_break_min': 15, # Minimum page break duration (seconds)
+ 'page_break_max': 45, # Maximum page break duration (seconds)
+ 'batch_break_interval': 5, # Take a break every N comics
+ 'batch_break_min': 3, # Minimum batch break duration (seconds)
+ 'batch_break_max': 7, # Maximum batch break duration (seconds)
+ 'typing_delay': 0.1, # Delay between character typing (seconds)
}
def export_settings(self, export_path):
diff --git a/core/scraper.py b/core/scraper.py
index 29ebd36..a3bf8c5 100644
--- a/core/scraper.py
+++ b/core/scraper.py
@@ -25,7 +25,7 @@ class Scraper:
callback mechanisms for progress updates to a GUI application.
"""
- def __init__(self, headless=False, progress_callback=None, scraping_mode=0):
+ def __init__(self, headless=False, progress_callback=None, scraping_mode=0, timing_config=None):
"""
Initialize the scraper with optional GUI callback support.
@@ -34,11 +34,16 @@ class Scraper:
progress_callback (callable): Optional callback function for progress updates
Callback signature: callback(event_type: str, data: dict)
scraping_mode (int): Scraping mode (0=All Comics, 1=Latest Comics)
+ timing_config (dict): Timing configuration settings
"""
self.progress_callback = progress_callback
self._stop_requested = False
self.scraping_mode = scraping_mode
+ # Set up timing configuration with defaults
+ self.timing = timing_config or {}
+ self._setup_timing_defaults()
+
# Set up Chrome options with anti-detection measures
chrome_options = Options()
if headless:
@@ -103,16 +108,42 @@ class Scraper:
self._stop_requested = True
self._emit_progress("stop_requested", {})
- def human_delay(self, min_sec=0.5, max_sec=2):
+ def _setup_timing_defaults(self):
+ """Set up timing configuration with default values."""
+ defaults = {
+ 'action_delay_min': 0.5,
+ 'action_delay_max': 2.0,
+ 'page_break_chance': 70,
+ 'page_break_min': 15,
+ 'page_break_max': 45,
+ 'batch_break_interval': 5,
+ 'batch_break_min': 3,
+ 'batch_break_max': 7,
+ 'typing_delay': 0.1,
+ }
+
+ # Fill in any missing values with defaults
+ for key, default_value in defaults.items():
+ if key not in self.timing:
+ self.timing[key] = default_value
+
+ def human_delay(self, min_sec=None, max_sec=None):
"""
Simulate human-like delay with cancellation support.
Args:
- min_sec (float): Minimum delay time
- max_sec (float): Maximum delay time
+ min_sec (float): Minimum delay time (uses config default if None)
+ max_sec (float): Maximum delay time (uses config default if None)
"""
if self._stop_requested:
return
+
+ # Use configured timing if no specific values provided
+ if min_sec is None:
+ min_sec = self.timing['action_delay_min']
+ if max_sec is None:
+ max_sec = self.timing['action_delay_max']
+
delay_time = random.uniform(min_sec, max_sec)
self._emit_progress("delay_started", {"duration": delay_time})
time.sleep(delay_time)
@@ -129,7 +160,8 @@ class Scraper:
if self._stop_requested:
return
element.send_keys(char)
- time.sleep(random.uniform(0.05, 0.15))
+ typing_delay = self.timing['typing_delay']
+ time.sleep(random.uniform(typing_delay * 0.5, typing_delay * 1.5))
def navigate(self, url):
"""
@@ -330,8 +362,9 @@ class Scraper:
# Take a break between pages (more likely and longer)
if page_num > start_page:
- if random.random() < 0.7: # 70% chance of break
- break_time = random.uniform(15, 45) # 15-45 seconds
+ break_chance = self.timing['page_break_chance'] / 100.0
+ if random.random() < break_chance:
+ break_time = random.uniform(self.timing['page_break_min'], self.timing['page_break_max'])
self._emit_progress("page_break_started", {
"duration": break_time,
"page_number": page_num
@@ -446,9 +479,10 @@ class Scraper:
"comic_index": i
})
- # Take a longer break every 5 comics
- if i % 5 == 0 and i < len(comic_urls):
- break_time = random.uniform(3, 7)
+ # Take a longer break every N comics (configurable)
+ batch_interval = self.timing['batch_break_interval']
+ if i % batch_interval == 0 and i < len(comic_urls):
+ break_time = random.uniform(self.timing['batch_break_min'], self.timing['batch_break_max'])
self._emit_progress("comic_batch_break", {
"duration": break_time,
"comics_processed": i
diff --git a/core/scraper_thread.py b/core/scraper_thread.py
index d471a43..21855db 100644
--- a/core/scraper_thread.py
+++ b/core/scraper_thread.py
@@ -58,7 +58,7 @@ class ScraperThread(QThread):
comic_batch_break = pyqtSignal(float, int) # duration, comics_processed
download_delay = pyqtSignal(float, int) # duration, remaining_downloads
- def __init__(self, username, password, start_page, end_page, scraping_mode=0, headless=True):
+ def __init__(self, username, password, start_page, end_page, scraping_mode=0, headless=True, timing_config=None):
"""
Initialize the scraper thread.
@@ -69,6 +69,7 @@ class ScraperThread(QThread):
end_page (int): Ending page number
scraping_mode (int): Scraping mode (0=All Comics, 1=Latest Comics)
headless (bool): Whether to run Chrome in headless mode
+ timing_config (dict): Timing configuration settings
"""
super().__init__()
self.username = username
@@ -77,6 +78,7 @@ class ScraperThread(QThread):
self.end_page = end_page
self.scraping_mode = scraping_mode
self.headless = headless
+ self.timing_config = timing_config
self.scraper = None
self._is_running = False
@@ -92,7 +94,8 @@ class ScraperThread(QThread):
self.scraper = Scraper(
headless=self.headless,
progress_callback=self._handle_scraper_progress,
- scraping_mode=self.scraping_mode
+ scraping_mode=self.scraping_mode,
+ timing_config=self.timing_config
)
# Perform login
diff --git a/gui/main_window.py b/gui/main_window.py
index 378e657..efc86d2 100644
--- a/gui/main_window.py
+++ b/gui/main_window.py
@@ -6,9 +6,9 @@ import sys
from pathlib import Path
from PyQt5.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
- QPushButton, QLabel, QSpinBox, QTextEdit, QGroupBox,
+ QPushButton, QLabel, QSpinBox, QDoubleSpinBox, QTextEdit, QGroupBox,
QCheckBox, QProgressBar, QMessageBox, QFileDialog, QMenuBar, QMenu, QAction,
- QComboBox
+ QComboBox, QFrame, QScrollArea, QSizePolicy
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QFont, QIcon
@@ -54,33 +54,55 @@ class MainWindow(QMainWindow):
self.app_settings = self.credential_manager.get_default_settings()
self.init_ui()
+ self.apply_light_theme()
self.update_credential_status()
def init_ui(self):
"""Initialize the user interface."""
self.setWindowTitle("EBoek.info Scraper")
- self.setMinimumSize(600, 500)
- self.resize(700, 600)
+ self.setMinimumSize(900, 650)
+ self.resize(1200, 750)
# Create menu bar
self.create_menu_bar()
- # Create central widget
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
+ # Create central widget with scroll area for better responsiveness
+ scroll_area = QScrollArea()
+ scroll_widget = QWidget()
+ scroll_area.setWidget(scroll_widget)
+ scroll_area.setWidgetResizable(True)
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.setCentralWidget(scroll_area)
- # Main layout
- layout = QVBoxLayout(central_widget)
+ # Main layout with better spacing
+ layout = QVBoxLayout(scroll_widget)
+ layout.setSpacing(20)
+ layout.setContentsMargins(20, 20, 20, 20)
- # Create sections
+ # Create sections in a more organized way
self.create_credential_section(layout)
self.create_scraping_section(layout)
+ self.create_timing_section(layout)
self.create_status_section(layout)
self.create_control_section(layout)
+ # Add stretch to push content to top
+ layout.addStretch()
+
# Status bar
self.statusBar().showMessage("Ready")
+ def apply_light_theme(self):
+ """Apply minimal light theme styling with completely default system controls."""
+ # Very minimal stylesheet - only style the main containers, no form controls
+ light_stylesheet = """
+ QMainWindow {
+ background-color: #ffffff;
+ }
+ """
+ self.setStyleSheet(light_stylesheet)
+
def create_menu_bar(self):
"""Create the menu bar."""
menubar = self.menuBar()
@@ -118,14 +140,19 @@ class MainWindow(QMainWindow):
def create_credential_section(self, parent_layout):
"""Create the credential management section."""
- group = QGroupBox("Credentials")
+ group = QGroupBox("Account Credentials")
layout = QHBoxLayout(group)
+ # Status with icon
+ status_layout = QHBoxLayout()
self.credential_status_label = QLabel("No credentials configured")
- layout.addWidget(self.credential_status_label)
+ self.credential_status_label.setStyleSheet("font-size: 13px;")
+ status_layout.addWidget(self.credential_status_label)
+ layout.addLayout(status_layout)
layout.addStretch()
+ # Button with default system styling
self.change_credentials_btn = QPushButton("Change Credentials")
self.change_credentials_btn.clicked.connect(self.show_login_dialog)
layout.addWidget(self.change_credentials_btn)
@@ -135,51 +162,71 @@ class MainWindow(QMainWindow):
def create_scraping_section(self, parent_layout):
"""Create the scraping configuration section."""
group = QGroupBox("Scraping Configuration")
- layout = QGridLayout(group)
+ main_layout = QVBoxLayout(group)
+
+ # Create horizontal layout for better space usage
+ top_section = QHBoxLayout()
+
+ # Left side - Mode selection
+ mode_group = QGroupBox("Scraping Mode")
+ mode_layout = QVBoxLayout(mode_group)
- # Scraping mode selection
- layout.addWidget(QLabel("Mode:"), 0, 0)
self.mode_combo = QComboBox()
self.mode_combo.addItems([
- "All Comics (stripverhalen-alle)",
- "Latest Comics (laatste)"
+ "All Comics (Complete Archive)",
+ "Latest Comics (Recent Additions)"
])
self.mode_combo.setCurrentIndex(self.app_settings.get('scraping_mode', 0))
- self.mode_combo.setToolTip("Select which page type to scrape")
self.mode_combo.currentIndexChanged.connect(self.on_mode_changed)
- layout.addWidget(self.mode_combo, 0, 1, 1, 3)
+ mode_layout.addWidget(self.mode_combo)
- # Page range selection
- layout.addWidget(QLabel("Start Page:"), 1, 0)
+ # Mode description label
+ self.mode_description_label = QLabel("")
+ self.mode_description_label.setWordWrap(True)
+ mode_layout.addWidget(self.mode_description_label)
+
+ mode_group.setMaximumWidth(400)
+
+ # Right side - Page range and options
+ config_group = QGroupBox("Configuration")
+ config_layout = QGridLayout(config_group)
+
+ # Page range
+ config_layout.addWidget(QLabel("Start Page:"), 0, 0)
self.start_page_spin = QSpinBox()
self.start_page_spin.setMinimum(1)
self.start_page_spin.setMaximum(9999)
self.start_page_spin.setValue(self.app_settings.get('default_start_page', 1))
- layout.addWidget(self.start_page_spin, 1, 1)
+ config_layout.addWidget(self.start_page_spin, 0, 1)
- layout.addWidget(QLabel("End Page:"), 1, 2)
+ config_layout.addWidget(QLabel("End Page:"), 0, 2)
self.end_page_spin = QSpinBox()
self.end_page_spin.setMinimum(1)
self.end_page_spin.setMaximum(9999)
self.end_page_spin.setValue(self.app_settings.get('default_end_page', 1))
- layout.addWidget(self.end_page_spin, 1, 3)
-
- # Mode description label
- self.mode_description_label = QLabel("")
- self.mode_description_label.setStyleSheet("color: #666; font-size: 11px; font-style: italic;")
- self.mode_description_label.setWordWrap(True)
- layout.addWidget(self.mode_description_label, 2, 0, 1, 4)
+ config_layout.addWidget(self.end_page_spin, 0, 3)
# Options
self.headless_checkbox = QCheckBox("Headless Mode")
self.headless_checkbox.setChecked(self.app_settings.get('headless_mode', True))
- self.headless_checkbox.setToolTip("Run browser in background (recommended)")
- layout.addWidget(self.headless_checkbox, 3, 0, 1, 2)
+ config_layout.addWidget(self.headless_checkbox, 1, 0, 1, 2)
self.verbose_checkbox = QCheckBox("Verbose Logging")
self.verbose_checkbox.setChecked(self.app_settings.get('verbose_logging', False))
- self.verbose_checkbox.setToolTip("Show detailed progress information")
- layout.addWidget(self.verbose_checkbox, 3, 2, 1, 2)
+ config_layout.addWidget(self.verbose_checkbox, 1, 2, 1, 2)
+
+ # Help texts
+ page_help = QLabel("π‘ Start with 1-3 pages to test")
+ config_layout.addWidget(page_help, 2, 0, 1, 4)
+
+ browser_help = QLabel("π₯οΈ Headless = background mode (recommended) β’ Verbose = detailed logs")
+ config_layout.addWidget(browser_help, 3, 0, 1, 4)
+
+ # Add to horizontal layout
+ top_section.addWidget(mode_group)
+ top_section.addWidget(config_group)
+
+ main_layout.addLayout(top_section)
# Update mode description
self.update_mode_description()
@@ -196,49 +243,286 @@ class MainWindow(QMainWindow):
mode_index = self.mode_combo.currentIndex()
if mode_index == 0: # All Comics
- description = ("Scrapes all comics from the 'stripverhalen-alle' page. "
- "This is the original scraping mode with complete comic archives.")
+ description = ("ποΈ All Comics (Complete Archive)
"
+ "Scrapes from 'stripverhalen-alle' page containing the full comic archive. "
+ "Best for systematic collection of all available comics. "
+ "Large page counts available (1000+ pages).")
elif mode_index == 1: # Latest Comics
- description = ("Scrapes latest comics from the 'laatste' page. "
- "This mode gets the most recently added comics with page parameter support.")
+ description = ("π Latest Comics (Recent Additions)
"
+ "Scrapes from 'laatste' page with most recently added comics. "
+ "Perfect for staying up-to-date with new releases. "
+ "Smaller page counts but fresh content.")
else:
description = ""
self.mode_description_label.setText(description)
+ def create_timing_section(self, parent_layout):
+ """Create the timing configuration section."""
+ group = QGroupBox("Timing Configuration")
+ main_layout = QVBoxLayout(group)
+
+ # Create horizontal layout for better space usage
+ top_layout = QHBoxLayout()
+
+ # Left side - Quick presets
+ preset_group = QGroupBox("Quick Presets")
+ preset_layout = QVBoxLayout(preset_group)
+
+ preset_info = QLabel("Choose a predefined timing profile:")
+ preset_layout.addWidget(preset_info)
+
+ preset_buttons_layout = QVBoxLayout()
+
+ fast_btn = QPushButton("Fast Mode")
+ fast_btn.clicked.connect(lambda: self.apply_timing_preset("fast"))
+ preset_buttons_layout.addWidget(fast_btn)
+
+ fast_desc = QLabel("Minimal delays β’ Faster scraping β’ Higher detection risk")
+ fast_desc.setWordWrap(True)
+ preset_buttons_layout.addWidget(fast_desc)
+
+ balanced_btn = QPushButton("Balanced Mode (Recommended)")
+ balanced_btn.clicked.connect(lambda: self.apply_timing_preset("balanced"))
+ preset_buttons_layout.addWidget(balanced_btn)
+
+ balanced_desc = QLabel("Default settings β’ Good balance β’ Recommended for most users")
+ balanced_desc.setWordWrap(True)
+ preset_buttons_layout.addWidget(balanced_desc)
+
+ stealth_btn = QPushButton("Stealth Mode")
+ stealth_btn.clicked.connect(lambda: self.apply_timing_preset("stealth"))
+ preset_buttons_layout.addWidget(stealth_btn)
+
+ stealth_desc = QLabel("Maximum delays β’ Very human-like β’ Slower but undetectable")
+ stealth_desc.setWordWrap(True)
+ preset_buttons_layout.addWidget(stealth_desc)
+
+ preset_layout.addLayout(preset_buttons_layout)
+ preset_layout.addStretch()
+ preset_group.setMinimumWidth(350) # Changed from setMaximumWidth to setMinimumWidth
+
+ # Right side - Manual controls in a more compact layout
+ manual_group = QGroupBox("Manual Configuration")
+ manual_layout = QGridLayout(manual_group)
+
+ # Action delays
+ manual_layout.addWidget(QLabel("Action Delays (sec):"), 0, 0)
+ self.action_delay_min_spin = self.create_double_spinbox(0.1, 10.0, 0.1,
+ self.app_settings.get('action_delay_min', 0.5))
+ manual_layout.addWidget(self.action_delay_min_spin, 0, 1)
+ manual_layout.addWidget(QLabel("to"), 0, 2)
+ self.action_delay_max_spin = self.create_double_spinbox(0.1, 10.0, 0.1,
+ self.app_settings.get('action_delay_max', 2.0))
+ manual_layout.addWidget(self.action_delay_max_spin, 0, 3)
+
+ # Page breaks
+ manual_layout.addWidget(QLabel("Page Break Chance:"), 1, 0)
+ self.page_break_chance_spin = QSpinBox()
+ self.page_break_chance_spin.setRange(0, 100)
+ self.page_break_chance_spin.setValue(self.app_settings.get('page_break_chance', 70))
+ self.page_break_chance_spin.setSuffix("%")
+ manual_layout.addWidget(self.page_break_chance_spin, 1, 1)
+
+ manual_layout.addWidget(QLabel("Duration:"), 1, 2)
+ page_duration_layout = QHBoxLayout()
+ self.page_break_min_spin = QSpinBox()
+ self.page_break_min_spin.setRange(5, 300)
+ self.page_break_min_spin.setValue(self.app_settings.get('page_break_min', 15))
+ page_duration_layout.addWidget(self.page_break_min_spin)
+ page_duration_layout.addWidget(QLabel("-"))
+ self.page_break_max_spin = QSpinBox()
+ self.page_break_max_spin.setRange(5, 300)
+ self.page_break_max_spin.setValue(self.app_settings.get('page_break_max', 45))
+ self.page_break_max_spin.setSuffix("s")
+ page_duration_layout.addWidget(self.page_break_max_spin)
+ manual_layout.addLayout(page_duration_layout, 1, 3)
+
+ # Batch breaks
+ manual_layout.addWidget(QLabel("Batch Break Every:"), 2, 0)
+ self.batch_break_interval_spin = QSpinBox()
+ self.batch_break_interval_spin.setRange(1, 50)
+ self.batch_break_interval_spin.setValue(self.app_settings.get('batch_break_interval', 5))
+ self.batch_break_interval_spin.setSuffix(" comics")
+ manual_layout.addWidget(self.batch_break_interval_spin, 2, 1)
+
+ manual_layout.addWidget(QLabel("Duration:"), 2, 2)
+ batch_duration_layout = QHBoxLayout()
+ self.batch_break_min_spin = QSpinBox()
+ self.batch_break_min_spin.setRange(1, 60)
+ self.batch_break_min_spin.setValue(self.app_settings.get('batch_break_min', 3))
+ batch_duration_layout.addWidget(self.batch_break_min_spin)
+ batch_duration_layout.addWidget(QLabel("-"))
+ self.batch_break_max_spin = QSpinBox()
+ self.batch_break_max_spin.setRange(1, 60)
+ self.batch_break_max_spin.setValue(self.app_settings.get('batch_break_max', 7))
+ self.batch_break_max_spin.setSuffix("s")
+ batch_duration_layout.addWidget(self.batch_break_max_spin)
+ manual_layout.addLayout(batch_duration_layout, 2, 3)
+
+ # Typing speed
+ manual_layout.addWidget(QLabel("Typing Speed:"), 3, 0)
+ self.typing_delay_spin = self.create_double_spinbox(0.01, 1.0, 0.01,
+ self.app_settings.get('typing_delay', 0.1))
+ self.typing_delay_spin.setSuffix(" sec/char")
+ manual_layout.addWidget(self.typing_delay_spin, 3, 1)
+
+ # Bottom row with reset and help buttons
+ button_layout = QHBoxLayout()
+
+ self.reset_timing_btn = QPushButton("Reset to Balanced")
+ self.reset_timing_btn.clicked.connect(self.reset_timing_defaults)
+ button_layout.addWidget(self.reset_timing_btn)
+
+ help_btn = QPushButton("Help")
+ help_btn.clicked.connect(self.show_timing_help_dialog)
+ button_layout.addWidget(help_btn)
+
+ button_layout.addStretch()
+ manual_layout.addLayout(button_layout, 4, 0, 1, 4)
+
+ # Add to horizontal layout with proper proportions
+ top_layout.addWidget(preset_group, 1) # Give preset group more space
+ top_layout.addWidget(manual_group, 1) # Equal space for manual group
+
+ main_layout.addLayout(top_layout)
+
+ # Set the timing section to expand properly
+ group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ parent_layout.addWidget(group)
+
+ def create_double_spinbox(self, min_val, max_val, step, value):
+ """Create a double precision spinbox with specified parameters."""
+ spinbox = QDoubleSpinBox()
+ spinbox.setRange(min_val, max_val)
+ spinbox.setSingleStep(step)
+ spinbox.setDecimals(1)
+ spinbox.setValue(value)
+ return spinbox
+
+ def show_timing_help_dialog(self):
+ """Show a compact help dialog for timing configuration."""
+ dialog = QMessageBox(self)
+ dialog.setWindowTitle("Timing Configuration Help")
+ dialog.setIcon(QMessageBox.Information)
+
+ help_text = """
+Timing Configuration Guide
+
+π― Purpose: Simulate human-like browsing to avoid automated detection
+
+β‘ Action Delays: Time between clicks and scrolls
+β’ Lower = Faster scraping, higher detection risk
+β’ Higher = Slower scraping, more realistic behavior
+
+βΈοΈ Page Breaks: Random pauses between pages
+β’ Simulates reading time and human fatigue
+β’ 70% chance with 15-45s duration is realistic
+
+π Batch Breaks: Pauses after processing multiple comics
+β’ Every 5 comics with 3-7s breaks simulates attention shifts
+
+β¨οΈ Typing Speed: Character delay when entering credentials
+β’ 0.1 sec = Average typing speed (40 WPM)
+
+π‘ Recommendations:
+β’ Fast Mode: Testing or when speed matters more than stealth
+β’ Balanced Mode: Recommended for regular use
+β’ Stealth Mode: Maximum safety but slower operation
+ """
+
+ dialog.setText(help_text)
+ dialog.setStandardButtons(QMessageBox.Ok)
+ dialog.exec_()
+
+ def apply_timing_preset(self, preset_type):
+ """Apply a timing preset configuration."""
+ if preset_type == "fast":
+ # Fast & Aggressive - minimal delays
+ self.action_delay_min_spin.setValue(0.1)
+ self.action_delay_max_spin.setValue(0.5)
+ self.page_break_chance_spin.setValue(20)
+ self.page_break_min_spin.setValue(5)
+ self.page_break_max_spin.setValue(10)
+ self.batch_break_interval_spin.setValue(15)
+ self.batch_break_min_spin.setValue(1)
+ self.batch_break_max_spin.setValue(2)
+ self.typing_delay_spin.setValue(0.05)
+
+ elif preset_type == "balanced":
+ # Balanced - default recommended settings
+ self.action_delay_min_spin.setValue(0.5)
+ self.action_delay_max_spin.setValue(2.0)
+ self.page_break_chance_spin.setValue(70)
+ self.page_break_min_spin.setValue(15)
+ self.page_break_max_spin.setValue(45)
+ self.batch_break_interval_spin.setValue(5)
+ self.batch_break_min_spin.setValue(3)
+ self.batch_break_max_spin.setValue(7)
+ self.typing_delay_spin.setValue(0.1)
+
+ elif preset_type == "stealth":
+ # Stealth - maximum human-like behavior
+ self.action_delay_min_spin.setValue(1.0)
+ self.action_delay_max_spin.setValue(3.0)
+ self.page_break_chance_spin.setValue(90)
+ self.page_break_min_spin.setValue(30)
+ self.page_break_max_spin.setValue(90)
+ self.batch_break_interval_spin.setValue(3)
+ self.batch_break_min_spin.setValue(5)
+ self.batch_break_max_spin.setValue(15)
+ self.typing_delay_spin.setValue(0.15)
+
+ # Save the new settings
+ self.save_current_settings()
+
+ # Show feedback
+ self.statusBar().showMessage(f"Applied {preset_type.title()} timing preset", 3000)
+
+ def reset_timing_defaults(self):
+ """Reset all timing settings to safe defaults."""
+ self.apply_timing_preset("balanced")
+
def create_status_section(self, parent_layout):
"""Create the status display section."""
- group = QGroupBox("Status")
+ group = QGroupBox("Status & Controls")
layout = QVBoxLayout(group)
+ # Status display
+ status_layout = QHBoxLayout()
+
+ status_info_layout = QVBoxLayout()
self.status_label = QLabel("Ready to start scraping...")
- self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57;")
- layout.addWidget(self.status_label)
+ self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57; font-size: 13px;")
+ status_info_layout.addWidget(self.status_label)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
- layout.addWidget(self.progress_bar)
+ status_info_layout.addWidget(self.progress_bar)
- parent_layout.addWidget(group)
+ status_layout.addLayout(status_info_layout)
+ status_layout.addStretch()
-
- def create_control_section(self, parent_layout):
- """Create the control buttons section."""
- layout = QHBoxLayout()
+ # Control buttons
+ button_layout = QVBoxLayout()
self.start_btn = QPushButton("Start Scraping")
self.start_btn.clicked.connect(self.start_scraping)
- self.start_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 8px; }")
- layout.addWidget(self.start_btn)
-
- layout.addStretch()
+ button_layout.addWidget(self.start_btn)
self.downloads_btn = QPushButton("Open Downloads Folder")
self.downloads_btn.clicked.connect(self.open_downloads_folder)
- layout.addWidget(self.downloads_btn)
+ button_layout.addWidget(self.downloads_btn)
- parent_layout.addLayout(layout)
+ status_layout.addLayout(button_layout)
+ layout.addLayout(status_layout)
+
+ parent_layout.addWidget(group)
+
+ def create_control_section(self, parent_layout):
+ """Create the control buttons section - now integrated into status section."""
+ pass # This is now handled by create_status_section
def update_credential_status(self):
"""Update the credential status display."""
@@ -289,13 +573,27 @@ class MainWindow(QMainWindow):
self.log_message(f"Starting scraping: {mode_name} mode, pages {start_page} to {end_page}")
# Create and start scraper thread
+ # Collect current timing configuration
+ timing_config = {
+ 'action_delay_min': self.action_delay_min_spin.value(),
+ 'action_delay_max': self.action_delay_max_spin.value(),
+ 'page_break_chance': self.page_break_chance_spin.value(),
+ 'page_break_min': self.page_break_min_spin.value(),
+ 'page_break_max': self.page_break_max_spin.value(),
+ 'batch_break_interval': self.batch_break_interval_spin.value(),
+ 'batch_break_min': self.batch_break_min_spin.value(),
+ 'batch_break_max': self.batch_break_max_spin.value(),
+ 'typing_delay': self.typing_delay_spin.value(),
+ }
+
self.scraper_thread = ScraperThread(
username=credentials['username'],
password=credentials['password'],
start_page=start_page,
end_page=end_page,
scraping_mode=mode_index,
- headless=self.headless_checkbox.isChecked()
+ headless=self.headless_checkbox.isChecked(),
+ timing_config=timing_config
)
# Connect signals
@@ -398,6 +696,16 @@ class MainWindow(QMainWindow):
'default_start_page': self.start_page_spin.value(),
'default_end_page': self.end_page_spin.value(),
'scraping_mode': self.mode_combo.currentIndex(),
+ # Timing settings
+ 'action_delay_min': self.action_delay_min_spin.value(),
+ 'action_delay_max': self.action_delay_max_spin.value(),
+ 'page_break_chance': self.page_break_chance_spin.value(),
+ 'page_break_min': self.page_break_min_spin.value(),
+ 'page_break_max': self.page_break_max_spin.value(),
+ 'batch_break_interval': self.batch_break_interval_spin.value(),
+ 'batch_break_min': self.batch_break_min_spin.value(),
+ 'batch_break_max': self.batch_break_max_spin.value(),
+ 'typing_delay': self.typing_delay_spin.value(),
}
settings.update(self.app_settings) # Keep other settings
self.credential_manager.save_app_settings(settings)