From 610a20d12d113393767d7d601e9c0d1c1b270054 Mon Sep 17 00:00:00 2001 From: Louis Mylle Date: Sat, 10 Jan 2026 17:36:09 +0100 Subject: [PATCH] feat: Enhance scraper timing configuration with user-defined settings and UI adjustments --- .DS_Store | Bin 8196 -> 8196 bytes core/credentials.py | 12 +- core/scraper.py | 54 +++++- core/scraper_thread.py | 7 +- gui/main_window.py | 416 +++++++++++++++++++++++++++++++++++------ 5 files changed, 422 insertions(+), 67 deletions(-) diff --git a/.DS_Store b/.DS_Store index 640058d470a49ff590c6831f5f6081db79022ab8..981bcbf45d050ac936479e3989826d27d6b0a5d8 100644 GIT binary patch delta 511 zcmZXPy-or_6ou~v6;PDrPl0ITHYPR{w#F3;CC0E(W57SMy9_w8&TM8^5gHQ11Ly;o z*jkz`Z7gkk1mh!UWn$+pU}AKVoAcc}=bp**l%8^3!t8_k7{ja^mF@yvYFt>l$#npH z(JhnOPCox8Nf-!@jE;rEk@1P>+-y}!>YCAP2!76LYux0b zV4jnMhM=__OEV~IlpS*E?9dLWN{K(#ZBjcPj=J9ZZrk!qsXwN5K?v&ymg;qzIOShl zsY)^15Zp9XZc~SHRynj!c&q4;vm&c=@5u{FS|!WBviFFvJ^i;+u4tkwG}a)6_K~US z#4ND_cN~7^Tm3kLGgS~js!Tohex_f<_|0kKmvIH0O8aR4He}lru2E2aYiB$ zSyBD=(5o;k!YXXR9yFl?H*gF0@CeWF3h(fRA 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)