Files
eboek.info-scraper/gui/main_window.py

818 lines
34 KiB
Python

"""
Main application window for the EBoek.info scraper GUI.
"""
import sys
from pathlib import Path
from PyQt5.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QLabel, QSpinBox, QDoubleSpinBox, QTextEdit, QGroupBox,
QCheckBox, QProgressBar, QMessageBox, QFileDialog, QMenuBar, QMenu, QAction,
QComboBox, QFrame, QScrollArea, QSizePolicy
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QFont, QIcon
# Import our custom modules
import os
import sys
# Add the project root directory to Python path so we can import our modules
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from core.credentials import CredentialManager
from core.scraper_thread import ScraperThread
from utils.validators import validate_page_range, format_error_message
from gui.login_dialog import LoginDialog
from gui.progress_dialog import ProgressDialog
class MainWindow(QMainWindow):
"""
Main application window for the EBoek.info scraper.
This window provides the primary interface for:
- Managing credentials
- Setting scraping parameters
- Starting/stopping scraping operations
- Monitoring progress and logs
"""
# Custom signals
scraping_requested = pyqtSignal(str, str, int, int, bool) # username, password, start_page, end_page, headless
def __init__(self):
super().__init__()
self.credential_manager = CredentialManager()
self.scraper_thread = None
self.progress_dialog = None
# Load application settings
self.app_settings = self.credential_manager.load_app_settings()
if not self.app_settings:
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(900, 650)
self.resize(1200, 750)
# Create menu bar
self.create_menu_bar()
# 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 with better spacing
layout = QVBoxLayout(scroll_widget)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
# 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()
# File menu
file_menu = menubar.addMenu('File')
export_action = QAction('Export Settings', self)
export_action.triggered.connect(self.export_settings)
file_menu.addAction(export_action)
import_action = QAction('Import Settings', self)
import_action.triggered.connect(self.import_settings)
file_menu.addAction(import_action)
file_menu.addSeparator()
exit_action = QAction('Exit', self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# Settings menu
settings_menu = menubar.addMenu('Settings')
clear_creds_action = QAction('Clear Saved Credentials', self)
clear_creds_action.triggered.connect(self.clear_credentials)
settings_menu.addAction(clear_creds_action)
# Help menu
help_menu = menubar.addMenu('Help')
about_action = QAction('About', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_credential_section(self, parent_layout):
"""Create the credential management section."""
group = QGroupBox("Account Credentials")
layout = QHBoxLayout(group)
# Status with icon
status_layout = QHBoxLayout()
self.credential_status_label = QLabel("No credentials configured")
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)
parent_layout.addWidget(group)
def create_scraping_section(self, parent_layout):
"""Create the scraping configuration section."""
group = QGroupBox("Scraping Configuration")
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)
self.mode_combo = QComboBox()
self.mode_combo.addItems([
"All Comics (Complete Archive)",
"Latest Comics (Recent Additions)"
])
self.mode_combo.setCurrentIndex(self.app_settings.get('scraping_mode', 0))
self.mode_combo.currentIndexChanged.connect(self.on_mode_changed)
mode_layout.addWidget(self.mode_combo)
# 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))
config_layout.addWidget(self.start_page_spin, 0, 1)
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))
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))
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))
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()
parent_layout.addWidget(group)
def on_mode_changed(self):
"""Handle scraping mode selection change."""
self.update_mode_description()
self.save_current_settings()
def update_mode_description(self):
"""Update the mode description text based on current selection."""
mode_index = self.mode_combo.currentIndex()
if mode_index == 0: # All Comics
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 = ("🆕 <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 & 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; font-size: 13px;")
status_info_layout.addWidget(self.status_label)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
status_info_layout.addWidget(self.progress_bar)
status_layout.addLayout(status_info_layout)
status_layout.addStretch()
# Control buttons
button_layout = QVBoxLayout()
self.start_btn = QPushButton("Start Scraping")
self.start_btn.clicked.connect(self.start_scraping)
button_layout.addWidget(self.start_btn)
self.downloads_btn = QPushButton("Open Downloads Folder")
self.downloads_btn.clicked.connect(self.open_downloads_folder)
button_layout.addWidget(self.downloads_btn)
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."""
username = self.credential_manager.get_saved_username()
if username:
self.credential_status_label.setText(f"Logged in as: {username}")
self.credential_status_label.setStyleSheet("color: #2E8B57; font-weight: bold;")
else:
self.credential_status_label.setText("No credentials configured")
self.credential_status_label.setStyleSheet("color: #FF6B35; font-weight: bold;")
def show_login_dialog(self):
"""Show the login dialog for credential input."""
dialog = LoginDialog(self, self.credential_manager)
if dialog.exec_() == dialog.Accepted:
self.update_credential_status()
self.log_message("Credentials updated successfully.")
def start_scraping(self):
"""Start the scraping process."""
# Validate credentials
credentials = self.credential_manager.load_credentials()
if not credentials:
QMessageBox.warning(self, "No Credentials",
"Please configure your EBoek.info credentials first.")
self.show_login_dialog()
return
# Validate page range
start_page = self.start_page_spin.value()
end_page = self.end_page_spin.value()
validation = validate_page_range(start_page, end_page)
if not validation['valid']:
QMessageBox.warning(self, "Invalid Page Range",
format_error_message(validation['errors']))
return
# Save current settings
self.save_current_settings()
# Get scraping mode
mode_index = self.mode_combo.currentIndex()
mode_names = ["All Comics", "Latest Comics"]
mode_name = mode_names[mode_index] if mode_index < len(mode_names) else "Unknown"
# Start scraping
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(),
timing_config=timing_config
)
# Connect signals
self.connect_scraper_signals()
# Show progress dialog
self.progress_dialog = ProgressDialog(self, self.scraper_thread)
self.progress_dialog.show()
# Start the thread
self.scraper_thread.start()
# Update UI state
self.start_btn.setEnabled(False)
self.status_label.setText("Scraping in progress...")
self.status_label.setStyleSheet("font-weight: bold; color: #FF8C00;")
def connect_scraper_signals(self):
"""Connect signals from the scraper thread to UI updates."""
if not self.scraper_thread:
return
# Login signals
self.scraper_thread.login_started.connect(self.on_login_started)
self.scraper_thread.login_success.connect(self.on_login_success)
self.scraper_thread.login_failed.connect(self.on_login_failed)
# Scraping completion
self.scraper_thread.scraping_completed.connect(self.on_scraping_completed)
# Status updates
self.scraper_thread.status_update.connect(self.log_message)
self.scraper_thread.error_occurred.connect(self.on_error_occurred)
# Page progress
self.scraper_thread.page_started.connect(self.on_page_started)
self.scraper_thread.page_completed.connect(self.on_page_completed)
def on_login_started(self, username):
"""Handle login started event."""
self.log_message(f"Logging in as {username}...")
def on_login_success(self, username):
"""Handle successful login."""
self.log_message(f"Login successful for {username}")
def on_login_failed(self, username, error):
"""Handle failed login."""
self.log_message(f"Login failed for {username}: {error}")
QMessageBox.critical(self, "Login Failed",
f"Could not log in as {username}.\n\n{error}\n\nPlease check your credentials.")
def on_page_started(self, page_number, page_index, total_pages, url):
"""Handle page started event."""
self.log_message(f"Processing page {page_number} ({page_index}/{total_pages})")
def on_page_completed(self, page_number, comics_processed):
"""Handle page completed event."""
self.log_message(f"Page {page_number} completed - {comics_processed} comics processed")
def on_scraping_completed(self, summary):
"""Handle scraping completion."""
self.start_btn.setEnabled(True)
if summary.get('cancelled'):
self.status_label.setText("Scraping cancelled")
self.status_label.setStyleSheet("font-weight: bold; color: #FF6B35;")
self.log_message("Scraping was cancelled by user")
elif summary.get('success'):
self.status_label.setText("Scraping completed")
self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57;")
self.log_message(f"Scraping completed! Processed {summary.get('total_comics_processed', 0)} comics, "
f"triggered {summary.get('total_downloads_triggered', 0)} downloads")
else:
self.status_label.setText("Scraping failed")
self.status_label.setStyleSheet("font-weight: bold; color: #f44336;")
self.log_message("Scraping failed - see errors above")
# Show summary
if summary.get('errors'):
error_count = len(summary['errors'])
QMessageBox.warning(self, "Scraping Completed with Errors",
f"Scraping completed but {error_count} errors occurred.\n"
f"Check the log for details.")
def on_error_occurred(self, error_message):
"""Handle error events."""
self.log_message(f"ERROR: {error_message}")
def log_message(self, message):
"""Log message - removed from main window, only shown in progress dialog."""
# Activity log now only appears in the scraping progress dialog
pass
def save_current_settings(self):
"""Save current UI settings to configuration."""
settings = {
'headless_mode': self.headless_checkbox.isChecked(),
'verbose_logging': self.verbose_checkbox.isChecked(),
'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)
self.app_settings = settings
def open_downloads_folder(self):
"""Open the downloads folder in the system file manager."""
downloads_path = Path.home() / "Downloads"
import sys
import subprocess
try:
if sys.platform == "win32":
os.startfile(downloads_path)
elif sys.platform == "darwin":
subprocess.run(["open", str(downloads_path)])
else: # linux
subprocess.run(["xdg-open", str(downloads_path)])
except Exception as e:
QMessageBox.information(self, "Downloads Folder",
f"Downloads are saved to:\n{downloads_path}\n\n"
f"Could not open folder automatically: {e}")
def clear_credentials(self):
"""Clear saved credentials."""
reply = QMessageBox.question(self, "Clear Credentials",
"Are you sure you want to clear the saved credentials?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
if self.credential_manager.clear_credentials():
self.update_credential_status()
self.log_message("Saved credentials cleared.")
QMessageBox.information(self, "Credentials Cleared",
"Saved credentials have been cleared.")
else:
QMessageBox.warning(self, "Error", "Could not clear credentials.")
def export_settings(self):
"""Export application settings to a file."""
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Settings",
"eboek_scraper_settings.json",
"JSON files (*.json);;All files (*.*)"
)
if file_path:
if self.credential_manager.export_settings(file_path):
QMessageBox.information(self, "Export Successful",
f"Settings exported to:\n{file_path}")
else:
QMessageBox.warning(self, "Export Failed",
"Could not export settings.")
def import_settings(self):
"""Import application settings from a file."""
file_path, _ = QFileDialog.getOpenFileName(
self, "Import Settings",
"",
"JSON files (*.json);;All files (*.*)"
)
if file_path:
if self.credential_manager.import_settings(file_path):
self.app_settings = self.credential_manager.load_app_settings()
# Update UI with imported settings
self.headless_checkbox.setChecked(self.app_settings.get('headless_mode', True))
self.verbose_checkbox.setChecked(self.app_settings.get('verbose_logging', False))
self.start_page_spin.setValue(self.app_settings.get('default_start_page', 1))
self.end_page_spin.setValue(self.app_settings.get('default_end_page', 1))
QMessageBox.information(self, "Import Successful",
f"Settings imported from:\n{file_path}")
self.log_message("Settings imported successfully.")
else:
QMessageBox.warning(self, "Import Failed",
"Could not import settings.")
def show_about(self):
"""Show the about dialog."""
QMessageBox.about(self, "About EBoek.info Scraper",
"EBoek.info Scraper\n\n"
"A GUI application for downloading comic strips from eboek.info.\n\n"
"Features:\n"
"• Automated login and scraping\n"
"• Real-time progress monitoring\n"
"• Human-like behavior simulation\n"
"• Secure credential storage\n\n"
"Built with Python and PyQt5.")
def closeEvent(self, event):
"""Handle application close event."""
if self.scraper_thread and self.scraper_thread.isRunning():
reply = QMessageBox.question(self, "Scraping in Progress",
"Scraping is currently in progress. "
"Do you want to stop and exit?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.scraper_thread.request_stop()
# Give it a moment to stop gracefully
self.scraper_thread.wait(3000) # Wait up to 3 seconds
event.accept()
else:
event.ignore()
else:
# Save settings before closing
self.save_current_settings()
event.accept()