""" Main window for the Bookys (bookys-ebooks.com) scraper. Harvests direct 1fichier download URLs for a category and writes them to a .txt file. Kept entirely separate from the EBoek window so the EBoek flow is untouched. """ import os import sys import subprocess from pathlib import Path from PyQt5.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSpinBox, QTextEdit, QGroupBox, QComboBox, QLineEdit, QProgressBar, QMessageBox, QFileDialog, QApplication, QAction ) from PyQt5.QtCore import Qt project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from core.credentials import CredentialManager from core.bookys_thread import BookysThread from core.bookys_scraper import CATEGORIES, DEFAULT_PROFILE_DIR from utils.validators import validate_page_range, format_error_message class BookysWindow(QMainWindow): """Interface for scraping 1fichier links from bookys-ebooks.com.""" def __init__(self): super().__init__() self.credential_manager = CredentialManager() self.scraper_thread = None # Bookys settings live under their own key in the shared config. all_settings = self.credential_manager.load_app_settings() or {} self.settings = all_settings.get('bookys', {}) self.init_ui() self.apply_light_theme() # ------------------------------------------------------------------- set-up def init_ui(self): self.setWindowTitle("Bookys Scraper — 1fichier link harvester") self.setMinimumSize(760, 620) self.resize(900, 720) self.create_menu_bar() central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setSpacing(16) layout.setContentsMargins(20, 20, 20, 20) self.create_config_section(layout) self.create_output_section(layout) self.create_control_section(layout) self.create_log_section(layout) self.statusBar().showMessage("Ready") def apply_light_theme(self): self.setStyleSheet("QMainWindow { background-color: #ffffff; }") def create_menu_bar(self): menubar = self.menuBar() file_menu = menubar.addMenu('File') open_out = QAction('Open Output Folder', self) open_out.triggered.connect(self.open_output_folder) file_menu.addAction(open_out) file_menu.addSeparator() exit_action = QAction('Exit', self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) mode_menu = menubar.addMenu('Mode') to_eboek = QAction('Switch to EBoek', self) to_eboek.triggered.connect(self._switch_to_eboek) mode_menu.addAction(to_eboek) browser_menu = menubar.addMenu('Browser') reset_profile = QAction('Reset Browser Profile', self) reset_profile.triggered.connect(self.reset_browser_profile) browser_menu.addAction(reset_profile) help_menu = menubar.addMenu('Help') about_action = QAction('About', self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def create_config_section(self, parent_layout): group = QGroupBox("Scraping Configuration") grid = QGridLayout(group) grid.addWidget(QLabel("Category:"), 0, 0) self.category_combo = QComboBox() self._category_keys = list(CATEGORIES.keys()) for key in self._category_keys: self.category_combo.addItem(CATEGORIES[key]["name"], key) saved_cat = self.settings.get('category', 'bd') if saved_cat in self._category_keys: self.category_combo.setCurrentIndex(self._category_keys.index(saved_cat)) grid.addWidget(self.category_combo, 0, 1, 1, 3) grid.addWidget(QLabel("Start Page:"), 1, 0) self.start_page_spin = QSpinBox() self.start_page_spin.setRange(1, 99999) self.start_page_spin.setValue(self.settings.get('start_page', 1)) grid.addWidget(self.start_page_spin, 1, 1) grid.addWidget(QLabel("End Page:"), 1, 2) self.end_page_spin = QSpinBox() self.end_page_spin.setRange(1, 99999) self.end_page_spin.setValue(self.settings.get('end_page', 1)) grid.addWidget(self.end_page_spin, 1, 3) hint = QLabel("💡 The browser opens visibly. If Cloudflare appears, tick " "its checkbox — scraping resumes automatically.") hint.setWordWrap(True) hint.setStyleSheet("color: #666;") grid.addWidget(hint, 2, 0, 1, 4) parent_layout.addWidget(group) def create_output_section(self, parent_layout): group = QGroupBox("Output File (.txt)") layout = QHBoxLayout(group) default_out = self.settings.get( 'output_file', str(Path.home() / "Downloads" / "bookys_1fichier_links.txt")) self.output_edit = QLineEdit(default_out) layout.addWidget(self.output_edit) browse_btn = QPushButton("Browse…") browse_btn.clicked.connect(self.browse_output) layout.addWidget(browse_btn) parent_layout.addWidget(group) def create_control_section(self, parent_layout): group = QGroupBox("Status & Controls") layout = QVBoxLayout(group) row = QHBoxLayout() info = QVBoxLayout() self.status_label = QLabel("Ready to start.") self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57;") info.addWidget(self.status_label) self.links_label = QLabel("Links found: 0") info.addWidget(self.links_label) self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) info.addWidget(self.progress_bar) row.addLayout(info) row.addStretch() buttons = QVBoxLayout() self.start_btn = QPushButton("Start Scraping") self.start_btn.clicked.connect(self.start_scraping) buttons.addWidget(self.start_btn) self.stop_btn = QPushButton("Stop") self.stop_btn.clicked.connect(self.stop_scraping) self.stop_btn.setEnabled(False) buttons.addWidget(self.stop_btn) row.addLayout(buttons) layout.addLayout(row) parent_layout.addWidget(group) def create_log_section(self, parent_layout): group = QGroupBox("Activity Log") layout = QVBoxLayout(group) self.log_view = QTextEdit() self.log_view.setReadOnly(True) layout.addWidget(self.log_view) parent_layout.addWidget(group) # ------------------------------------------------------------------ actions def log(self, message): self.log_view.append(message) def browse_output(self): path, _ = QFileDialog.getSaveFileName( self, "Choose output file", self.output_edit.text(), "Text files (*.txt);;All files (*.*)") if path: self.output_edit.setText(path) def _current_category(self): return self.category_combo.currentData() or 'bd' def save_settings(self): all_settings = self.credential_manager.load_app_settings() or {} all_settings['bookys'] = { 'category': self._current_category(), 'start_page': self.start_page_spin.value(), 'end_page': self.end_page_spin.value(), 'output_file': self.output_edit.text(), } self.credential_manager.save_app_settings(all_settings) self.settings = all_settings['bookys'] def start_scraping(self): 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 output_file = self.output_edit.text().strip() if not output_file: QMessageBox.warning(self, "No Output File", "Please choose where to save the links (.txt).") return self.save_settings() self.log_view.clear() self.log(f"Starting Bookys scrape — {CATEGORIES[self._current_category()]['name']}, " f"pages {start_page}–{end_page}") self.log(f"Writing links to: {output_file}") self.scraper_thread = BookysThread( category=self._current_category(), start_page=start_page, end_page=end_page, output_file=output_file, ) self._connect_signals() self.scraper_thread.start() self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.progress_bar.setVisible(True) self.progress_bar.setRange(0, 0) # indeterminate until first page completes self.status_label.setText("Opening browser…") self.status_label.setStyleSheet("font-weight: bold; color: #FF8C00;") def stop_scraping(self): if self.scraper_thread and self.scraper_thread.is_running(): self.scraper_thread.request_stop() self.log("Stop requested — finishing current item…") self.stop_btn.setEnabled(False) def _connect_signals(self): t = self.scraper_thread t.cloudflare_waiting.connect(self.on_cloudflare_waiting) t.cloudflare_passed.connect(self.on_cloudflare_passed) t.cloudflare_timeout.connect(self.on_cloudflare_timeout) t.page_started.connect(self.on_page_started) t.page_items_found.connect(self.on_page_items_found) t.page_completed.connect(self.on_page_completed) t.book_started.connect(self.on_book_started) t.book_completed.connect(self.on_book_completed) t.link_found.connect(self.on_link_found) t.scraping_completed.connect(self.on_scraping_completed) t.error_occurred.connect(lambda m: self.log(f"ERROR: {m}")) # ------------------------------------------------------------------- signals def on_cloudflare_waiting(self, message): self.status_label.setText("Waiting for Cloudflare…") self.status_label.setStyleSheet("font-weight: bold; color: #FF8C00;") self.log(f"⏳ {message}") def on_cloudflare_passed(self, item_count): self.log(f"✅ Page loaded ({item_count} items).") self.status_label.setText("Scraping…") def on_cloudflare_timeout(self, timeout): self.log(f"⚠️ Timed out after {timeout}s waiting for the page/Cloudflare.") def on_page_started(self, page_number, page_index, total_pages, url): self.log(f"— Page {page_number} ({page_index}/{total_pages})") if total_pages > 0: self.progress_bar.setRange(0, total_pages) self.progress_bar.setValue(page_index - 1) def on_page_items_found(self, page_number, item_count): self.log(f" Found {item_count} items on page {page_number}.") def on_page_completed(self, page_number, items_processed): self.log(f" Page {page_number} done ({items_processed} items).") self.progress_bar.setValue(self.progress_bar.value() + 1) def on_book_started(self, page_number, book_index, total_books, url): self.status_label.setText(f"Page {page_number}: item {book_index}/{total_books}") def on_book_completed(self, title, links_found): label = title if title else "(untitled)" self.log(f" • {label} — {links_found} 1fichier link(s)") def on_link_found(self, url, total): self.links_label.setText(f"Links found: {total}") def on_scraping_completed(self, summary): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.progress_bar.setRange(0, 1) self.progress_bar.setValue(1) total = summary.get('total_links_found', 0) if summary.get('cancelled'): self.status_label.setText("Cancelled") self.status_label.setStyleSheet("font-weight: bold; color: #FF6B35;") elif summary.get('success'): self.status_label.setText("Completed") self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57;") else: self.status_label.setText("Completed with errors") self.status_label.setStyleSheet("font-weight: bold; color: #f44336;") self.log(f"Done. {total} link(s) written to {summary.get('output_file', '')}") errors = summary.get('errors') or [] if errors: self.log(f"{len(errors)} error(s) occurred:") for e in errors[:10]: self.log(f" ! {e}") # --------------------------------------------------------------------- misc def open_output_folder(self): folder = Path(self.output_edit.text()).parent try: if sys.platform == "win32": os.startfile(folder) elif sys.platform == "darwin": subprocess.run(["open", str(folder)]) else: subprocess.run(["xdg-open", str(folder)]) except Exception as e: QMessageBox.information(self, "Output Folder", f"Links are saved to:\n{folder}\n\n" f"Could not open folder automatically: {e}") def reset_browser_profile(self): """Delete the persistent Chrome profile (forces a fresh Cloudflare pass).""" if self.scraper_thread and self.scraper_thread.is_running(): QMessageBox.warning(self, "Scraping in Progress", "Stop the current scrape before resetting the profile.") return reply = QMessageBox.question( self, "Reset Browser Profile", f"Delete the saved browser profile?\n\n{DEFAULT_PROFILE_DIR}\n\n" "You'll need to pass the Cloudflare check once more on the next run. " "Use this if the browser stops loading pages correctly.", QMessageBox.Yes | QMessageBox.No) if reply != QMessageBox.Yes: return import shutil try: if DEFAULT_PROFILE_DIR.exists(): shutil.rmtree(DEFAULT_PROFILE_DIR) self.log("Browser profile reset.") QMessageBox.information(self, "Profile Reset", "The browser profile has been cleared.") except Exception as e: QMessageBox.warning(self, "Reset Failed", f"Could not reset profile:\n{e}") def show_about(self): QMessageBox.about(self, "About Bookys Scraper", "Bookys Scraper\n\n" "Harvests direct 1fichier download URLs from " "bookys-ebooks.com and saves them to a .txt file.\n\n" "• Visible browser (Cloudflare-friendly)\n" "• Auto-detects when the challenge clears\n" "• Ignores pop-up ads\n\n" "Built with Python and PyQt5.") def _switch_to_eboek(self): app = QApplication.instance() if hasattr(app, 'show_eboek'): app.show_eboek() def closeEvent(self, event): if self.scraper_thread and self.scraper_thread.is_running(): reply = QMessageBox.question( self, "Scraping in Progress", "Scraping is running. Stop and continue?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.scraper_thread.request_stop() self.scraper_thread.wait(3000) event.accept() else: event.ignore() return else: self.save_settings() event.accept()