// ==UserScript== // @name ExamPrepper Content Dumper // @namespace http://tampermonkey.net/ // @version 1.1 // @description Dump accordion content from ExamPrepper pages to OPFS (AJAX-aware) // @match https://www.examprepper.co/exam/* // @grant none // ==/UserScript== (async function() { 'use strict'; alert('*') const FILENAME = 'questions.html'; const PAGE_SEPARATOR = '\n\n'; let isProcessing = false; let processedPages = new Set(); console.log('[ExamPrepper Dumper] Script started'); console.log('[ExamPrepper Dumper] Current URL:', window.location.href); // Wait 2 seconds for initial page load console.log('[ExamPrepper Dumper] Waiting 2 seconds for page to load...'); await sleep(2000); // Process the first page await processPage(); // Set up MutationObserver to detect AJAX page changes const observer = new MutationObserver(async (mutations) => { // Check if accordion items have changed (new page loaded) for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { // Check if new accordion items were added const hasNewAccordion = Array.from(mutation.addedNodes).some(node => { if (node.nodeType === 1) { // Element node return node.classList?.contains('chakra-accordion__item') || node.querySelector?.('.chakra-accordion__item'); } return false; }); if (hasNewAccordion && !isProcessing) { console.log('[ExamPrepper Dumper] New content detected after AJAX load'); await sleep(1000); // Give it a moment to settle await processPage(); } } } }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true }); console.log('[ExamPrepper Dumper] Now monitoring for AJAX page changes...'); async function processPage() { if (isProcessing) { console.log('[ExamPrepper Dumper] Already processing, skipping...'); return; } isProcessing = true; try { // Get OPFS root const root = await navigator.storage.getDirectory(); // Find all accordion items const accordionItems = document.querySelectorAll('.chakra-accordion__item'); console.log(`[ExamPrepper Dumper] Found ${accordionItems.length} accordion items`); if (accordionItems.length === 0) { console.warn('[ExamPrepper Dumper] No accordion items found! Check if page loaded correctly or CAPTCHA present.'); isProcessing = false; return; } // Create a unique page identifier (could use URL + first question text) const firstItemText = accordionItems[0]?.textContent?.substring(0, 100) || ''; const pageId = window.location.href + firstItemText; if (processedPages.has(pageId)) { console.log('[ExamPrepper Dumper] This page was already processed, skipping...'); isProcessing = false; return; } processedPages.add(pageId); console.log(`[ExamPrepper Dumper] Processing new page (total pages processed: ${processedPages.size})`); let pageContent = ''; // Loop through each accordion item for (let i = 0; i < accordionItems.length; i++) { const item = accordionItems[i]; console.log(`[ExamPrepper Dumper] Processing item ${i + 1}/${accordionItems.length}`); // Find "Show Answer" button within this item const buttons = item.querySelectorAll('button'); let showAnswerButton = null; for (const button of buttons) { if (button.textContent.trim() === 'Show Answer') { showAnswerButton = button; break; } } if (showAnswerButton) { console.log(`[ExamPrepper Dumper] Clicking "Show Answer" button for item ${i + 1}`); showAnswerButton.click(); // Wait 1 second after clicking await sleep(1000); // Append outerHTML to content pageContent += item.outerHTML; console.log(`[ExamPrepper Dumper] Item ${i + 1} content captured`); } else { console.warn(`[ExamPrepper Dumper] No "Show Answer" button found in item ${i + 1}`); } } // Append page content to file await appendToFile(root, FILENAME, pageContent + PAGE_SEPARATOR); console.log('[ExamPrepper Dumper] Content appended to file successfully'); // Wait 2 more seconds console.log('[ExamPrepper Dumper] Waiting 2 seconds before looking for Next button...'); await sleep(2000); // Find and click "Next" button const allButtons = document.querySelectorAll('button'); let nextButton = null; for (const button of allButtons) { if (button.textContent.trim() === 'Next') { nextButton = button; break; } } if (nextButton) { console.log('[ExamPrepper Dumper] "Next" button found, clicking...'); nextButton.click(); console.log('[ExamPrepper Dumper] Waiting for AJAX to load next page...'); } else { console.log('[ExamPrepper Dumper] No "Next" button found. Either all pages exhausted or CAPTCHA/error present.'); console.log('[ExamPrepper Dumper] ✓ Scraping complete!'); observer.disconnect(); // Stop observing } } catch (error) { console.error('[ExamPrepper Dumper] Error occurred:', error); } finally { isProcessing = false; } } // Helper function to sleep function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Helper function to append content to OPFS file async function appendToFile(root, filename, content) { const fileHandle = await root.getFileHandle(filename, { create: true }); const writable = await fileHandle.createWritable({ keepExistingData: true }); // Get current file size to append at the end const file = await fileHandle.getFile(); const size = file.size; // Seek to end and write await writable.write({ type: 'write', position: size, data: content }); await writable.close(); console.log(`[ExamPrepper Dumper] Appended ${content.length} characters to ${filename} (total size: ${size + content.length} bytes)`); } })();