|
|
|
|
@@ -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 = ("🗃️ <b>All Comics (Complete Archive)</b><br>"
|
|
|
|
|
"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 = ("🆕 <b>Latest Comics (Recent Additions)</b><br>"
|
|
|
|
|
"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 = """
|
|
|
|
|
<b>Timing Configuration Guide</b><br><br>
|
|
|
|
|
|
|
|
|
|
<b>🎯 Purpose:</b> Simulate human-like browsing to avoid automated detection<br><br>
|
|
|
|
|
|
|
|
|
|
<b>⚡ Action Delays:</b> Time between clicks and scrolls<br>
|
|
|
|
|
• Lower = Faster scraping, higher detection risk<br>
|
|
|
|
|
• Higher = Slower scraping, more realistic behavior<br><br>
|
|
|
|
|
|
|
|
|
|
<b>⏸️ Page Breaks:</b> Random pauses between pages<br>
|
|
|
|
|
• Simulates reading time and human fatigue<br>
|
|
|
|
|
• 70% chance with 15-45s duration is realistic<br><br>
|
|
|
|
|
|
|
|
|
|
<b>📚 Batch Breaks:</b> Pauses after processing multiple comics<br>
|
|
|
|
|
• Every 5 comics with 3-7s breaks simulates attention shifts<br><br>
|
|
|
|
|
|
|
|
|
|
<b>⌨️ Typing Speed:</b> Character delay when entering credentials<br>
|
|
|
|
|
• 0.1 sec = Average typing speed (40 WPM)<br><br>
|
|
|
|
|
|
|
|
|
|
<b>💡 Recommendations:</b><br>
|
|
|
|
|
• <b>Fast Mode:</b> Testing or when speed matters more than stealth<br>
|
|
|
|
|
• <b>Balanced Mode:</b> Recommended for regular use<br>
|
|
|
|
|
• <b>Stealth Mode:</b> 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)
|
|
|
|
|
|