您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Archive all your posts from phpBB forums with ZIP and clipboard export options. Includes test mode and verbose logging.
// ==UserScript== // @name Forum Post Archiver for phpBB (Private) // @namespace http://tampermonkey.net/ // @version 3.0.1 // @description Archive all your posts from phpBB forums with ZIP and clipboard export options. Includes test mode and verbose logging. // @author sharmanhall // @match https://macserialjunkie.com/forum/search.php* // @match https://*/forum/search.php* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setClipboard // @license MIT // ==/UserScript== (function() { 'use strict'; // Check if libraries are loaded console.log('JSZip loaded:', typeof JSZip !== 'undefined'); console.log('saveAs loaded:', typeof saveAs !== 'undefined'); // Manual fallback if @require didn't work if (typeof JSZip === 'undefined' || typeof saveAs === 'undefined') { console.warn('Libraries not loaded via @require, loading manually...'); // Load JSZip if (typeof JSZip === 'undefined') { const jsZipScript = document.createElement('script'); jsZipScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'; jsZipScript.onload = () => console.log('JSZip loaded manually'); document.head.appendChild(jsZipScript); } // Load FileSaver if (typeof saveAs === 'undefined') { const fileSaverScript = document.createElement('script'); fileSaverScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js'; fileSaverScript.onload = () => console.log('FileSaver loaded manually'); document.head.appendChild(fileSaverScript); } // Wait for libraries to load before continuing const checkLibraries = setInterval(() => { if (typeof JSZip !== 'undefined' && typeof saveAs !== 'undefined') { clearInterval(checkLibraries); console.log('All libraries loaded, initializing script...'); initScript(); } }, 100); } else { // Libraries already loaded, proceed immediately initScript(); } function initScript() { // Configuration const CONFIG = { DELAY_BETWEEN_REQUESTS: 1500, // milliseconds POSTS_PER_PAGE: 15, // standard phpBB pagination MAX_STATUS_MESSAGES: 10, // Increased for more verbose logging ENABLE_RATE_LIMITING: true, DEBUG_MODE: true, // Enable verbose logging MAX_POSTS_PER_BATCH: 50, // Process posts in batches to avoid memory issues ZIP_COMPRESSION_LEVEL: 1 // Lower compression for faster processing (1-9) }; // Add styles for the UI GM_addStyle(` #archiver-panel { position: fixed; top: 20px; right: 20px; background: #2a2a2a; color: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 10000; min-width: 350px; max-width: 450px; font-family: Arial, sans-serif; max-height: 80vh; overflow-y: auto; } #archiver-panel h3 { margin-top: 0; color: #00ff00; display: flex; align-items: center; gap: 10px; } #archiver-panel button { background: #00ff00; color: black; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-weight: bold; width: 100%; margin-bottom: 10px; } #archiver-panel button:hover { background: #00dd00; } #archiver-panel button:disabled { background: #666; cursor: not-allowed; color: #ccc; } #archiver-panel button.test-btn { background: #ffaa00; } #archiver-panel button.test-btn:hover { background: #ff8800; } #archiver-panel button.clipboard-btn { background: #00aaff; } #archiver-panel button.clipboard-btn:hover { background: #0088dd; } #archiver-progress { margin: 10px 0; background: #444; border-radius: 5px; overflow: hidden; height: 25px; position: relative; } #archiver-progress-bar { background: linear-gradient(90deg, #00ff00, #00dd00); height: 100%; width: 0%; transition: width 0.3s ease; } #archiver-progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; font-weight: bold; text-shadow: 1px 1px 2px rgba(0,0,0,0.5); } #archiver-status { margin: 10px 0; font-size: 12px; color: #aaa; max-height: 200px; overflow-y: auto; padding: 5px; background: rgba(0,0,0,0.2); border-radius: 5px; font-family: 'Courier New', monospace; } .archiver-error { color: #ff4444 !important; } .archiver-success { color: #00ff00 !important; } .archiver-warning { color: #ffaa00 !important; } .archiver-debug { color: #8888ff !important; font-size: 11px; } #archiver-close { position: absolute; top: 10px; right: 10px; background: transparent !important; color: #888; border: none; font-size: 20px; cursor: pointer; padding: 0 !important; width: 25px !important; height: 25px !important; margin: 0 !important; line-height: 1; display: flex; align-items: center; justify-content: center; } #archiver-close:hover { color: #fff; background: transparent !important; } .archiver-info { margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 5px; font-size: 11px; color: #888; } .archiver-stats { margin-top: 10px; padding: 10px; background: rgba(0,255,0,0.1); border-radius: 5px; font-size: 12px; color: #aaa; display: none; } .archiver-stats.active { display: block; } #archiver-memory { color: #ff8800; font-size: 11px; margin-top: 5px; } `); // Check if we're on a search results page with author_id function isValidSearchPage() { const urlParams = new URLSearchParams(window.location.search); return urlParams.has('author_id') && urlParams.get('sr') === 'posts'; } // State management let posts = []; let totalPosts = 0; let processedPosts = 0; let isRunning = false; let authorId = null; let authorName = 'User'; let startTime = null; let lastError = null; // Debug logging function debugLog(message, data = null) { if (CONFIG.DEBUG_MODE) { console.log(`[Forum Archiver] ${message}`, data || ''); updateStatus(`[DEBUG] ${message}`, 'debug'); } } // Create UI panel function createUI() { // Don't create UI if not on valid search page if (!isValidSearchPage()) { console.log('Forum Post Archiver: Not on a valid author search page'); return; } // Get author info const urlParams = new URLSearchParams(window.location.search); authorId = urlParams.get('author_id'); // Try to get username from page const authorLink = document.querySelector('.postprofile .author a'); if (authorLink) { authorName = authorLink.textContent.trim(); } // Count total posts from search results const searchInfo = document.querySelector('.searchresults-title') || document.querySelector('.pagination'); let totalPostsFound = 616; // default if (searchInfo) { const match = searchInfo.textContent.match(/(\d+)\s+matches/); if (match) { totalPostsFound = parseInt(match[1]); } } const panel = document.createElement('div'); panel.id = 'archiver-panel'; panel.innerHTML = ` <button id="archiver-close" title="Close">×</button> <h3>📦 POST ARCHIVER v2.0</h3> <div class="archiver-info"> <strong>Author:</strong> ${authorName} (ID: ${authorId})<br> <strong>Posts found:</strong> <span id="total-posts">${totalPostsFound}</span><br> <strong>Request delay:</strong> ${CONFIG.DELAY_BETWEEN_REQUESTS}ms<br> <strong>Debug mode:</strong> ${CONFIG.DEBUG_MODE ? 'ON' : 'OFF'} </div> <button id="test-zip" class="test-btn" title="Test ZIP creation with 3 posts">🧪 Test ZIP (3 posts)</button> <button id="start-archive">📥 Start Full Archive</button> <button id="copy-clipboard" class="clipboard-btn" style="display:none;">📋 Copy All to Clipboard</button> <button id="stop-archive" style="display:none;">⏹️ Stop</button> <div id="archiver-progress" style="display:none;"> <div id="archiver-progress-bar"></div> <span id="archiver-progress-text">0%</span> </div> <div class="archiver-stats" id="archiver-stats"> <strong>Statistics:</strong><br> <span id="stats-content"></span> <div id="archiver-memory"></div> </div> <div id="archiver-status"></div> `; document.body.appendChild(panel); // Event listeners document.getElementById('test-zip').addEventListener('click', testZipCreation); document.getElementById('start-archive').addEventListener('click', startArchiving); document.getElementById('copy-clipboard').addEventListener('click', copyToClipboard); document.getElementById('stop-archive').addEventListener('click', stopArchiving); document.getElementById('archiver-close').addEventListener('click', () => { if (isRunning && !confirm('Archive in progress. Close anyway?')) { return; } isRunning = false; panel.remove(); }); debugLog('UI created successfully'); } function updateStatus(message, type = 'normal') { const status = document.getElementById('archiver-status'); if (!status) return; const timestamp = new Date().toLocaleTimeString(); const classMap = { 'error': 'archiver-error', 'success': 'archiver-success', 'warning': 'archiver-warning', 'debug': 'archiver-debug', 'normal': '' }; const className = classMap[type] || ''; const msgDiv = document.createElement('div'); msgDiv.className = className; msgDiv.textContent = `[${timestamp}] ${message}`; status.insertBefore(msgDiv, status.firstChild); // Keep only last N messages const messages = status.querySelectorAll('div'); if (messages.length > CONFIG.MAX_STATUS_MESSAGES) { messages[messages.length - 1].remove(); } } function updateProgress() { const percentage = Math.round((processedPosts / totalPosts) * 100); const progressBar = document.getElementById('archiver-progress-bar'); const progressText = document.getElementById('archiver-progress-text'); if (progressBar && progressText) { progressBar.style.width = percentage + '%'; progressText.textContent = `${processedPosts}/${totalPosts} (${percentage}%)`; } // Update stats if (startTime) { const elapsed = (Date.now() - startTime) / 1000; const postsPerSecond = processedPosts / elapsed; const remaining = (totalPosts - processedPosts) / postsPerSecond; const statsDiv = document.getElementById('archiver-stats'); const statsContent = document.getElementById('stats-content'); if (statsDiv && statsContent) { statsDiv.classList.add('active'); statsContent.innerHTML = ` Processed: ${processedPosts}/${totalPosts}<br> Elapsed: ${Math.round(elapsed)}s<br> Speed: ${postsPerSecond.toFixed(2)} posts/sec<br> ETA: ${Math.round(remaining)}s `; } } // Memory usage estimation updateMemoryUsage(); } function updateMemoryUsage() { if (performance.memory) { const memDiv = document.getElementById('archiver-memory'); if (memDiv) { const used = (performance.memory.usedJSHeapSize / 1048576).toFixed(2); const limit = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2); memDiv.textContent = `Memory: ${used}MB / ${limit}MB`; } } } async function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function fetchPage(url) { debugLog(`Fetching: ${url}`); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ForumArchiver/2.0)' }, timeout: 30000, // 30 second timeout onload: function(response) { if (response.status === 200) { debugLog(`Fetch successful: ${url}`); resolve(response.responseText); } else { debugLog(`Fetch failed with status ${response.status}: ${url}`); reject(new Error(`HTTP ${response.status}`)); } }, onerror: function(error) { debugLog(`Fetch error: ${url}`, error); reject(error); }, ontimeout: function() { debugLog(`Fetch timeout: ${url}`); reject(new Error('Request timeout')); } }); }); } function parseSearchPage(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const postElements = doc.querySelectorAll('.search.post'); const pagePosts = []; postElements.forEach(element => { const postLink = element.querySelector('a[href*="viewtopic.php?p="]'); if (postLink) { const postIdMatch = postLink.href.match(/p=(\d+)/); if (!postIdMatch) return; const postId = postIdMatch[1]; const titleElement = element.querySelector('h3 a'); const dateElement = element.querySelector('.search-result-date'); const forumElement = element.querySelector('dd a[href*="viewforum.php"]'); const topicElement = element.querySelector('dd a[href*="viewtopic.php?t="]'); pagePosts.push({ id: postId, title: titleElement ? titleElement.textContent.trim() : 'Untitled', date: dateElement ? dateElement.textContent.trim() : 'Unknown date', forum: forumElement ? forumElement.textContent.trim() : 'Unknown forum', topic: topicElement ? topicElement.textContent.trim() : 'Unknown topic', editUrl: `${window.location.origin}${window.location.pathname.replace('search.php', 'posting.php')}?mode=edit&p=${postId}`, viewUrl: postLink.href }); } }); debugLog(`Parsed ${pagePosts.length} posts from search page`); return pagePosts; } async function fetchPostContent(post, retryCount = 0) { try { debugLog(`Fetching content for post ${post.id} (attempt ${retryCount + 1})`); const html = await fetchPage(post.editUrl); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const messageTextarea = doc.querySelector('#message'); const subjectInput = doc.querySelector('#subject'); if (messageTextarea) { post.content = messageTextarea.value; post.subject = subjectInput ? subjectInput.value : post.title; debugLog(`Content fetched successfully for post ${post.id}`); return true; } else { // Check various error conditions if (html.includes('You are not authorised') || html.includes('not authorized')) { post.content = '[Unable to fetch content - no edit permission]'; post.error = 'No edit permission'; } else if (html.includes('The requested post does not exist')) { post.content = '[Post not found]'; post.error = 'Post not found'; } else { post.content = '[Unable to fetch content - unknown error]'; post.error = 'Content not found'; } debugLog(`Content fetch failed for post ${post.id}: ${post.error}`); return false; } } catch (error) { debugLog(`Error fetching post ${post.id}: ${error.message}`); // Retry logic if (retryCount < 2) { updateStatus(`Retrying post ${post.id}...`, 'warning'); await delay(3000); return fetchPostContent(post, retryCount + 1); } post.content = `[Error fetching content: ${error.message}]`; post.error = error.message; return false; } } async function getAllSearchPages(limit = null) { const allPosts = []; const totalPostsElement = document.getElementById('total-posts'); const totalExpected = totalPostsElement ? parseInt(totalPostsElement.textContent) : 0; const totalPages = Math.ceil((limit || totalExpected) / CONFIG.POSTS_PER_PAGE); updateStatus(`Fetching ${totalPages} pages of search results...`); for (let i = 0; i < totalPages; i++) { if (!isRunning) break; const start = i * CONFIG.POSTS_PER_PAGE; const urlParams = new URLSearchParams(window.location.search); urlParams.set('start', start); const url = `${window.location.origin}${window.location.pathname}?${urlParams.toString()}`; try { const html = await fetchPage(url); const pagePosts = parseSearchPage(html); allPosts.push(...pagePosts); updateStatus(`Fetched page ${i + 1}/${totalPages} (${pagePosts.length} posts)`); if (limit && allPosts.length >= limit) { return allPosts.slice(0, limit); } if (CONFIG.ENABLE_RATE_LIMITING) { await delay(1000); } } catch (error) { updateStatus(`Error fetching page ${i + 1}: ${error.message}`, 'error'); lastError = error.message; } } return allPosts; } function sanitizeFilename(str) { return str.replace(/[^a-z0-9_\-]/gi, '_').substring(0, 50); } function createPostFile(post) { const datePart = post.date.replace(/[^a-z0-9]/gi, '_'); const subjectPart = sanitizeFilename(post.subject || post.title); const fileName = `${post.id}_${datePart}_${subjectPart}.bbcode`; const fileContent = `======================================== POST METADATA ======================================== Post ID: ${post.id} Subject: ${post.subject || post.title} Date: ${post.date} Forum: ${post.forum} Topic: ${post.topic} View URL: ${post.viewUrl} Edit URL: ${post.editUrl} Archive Date: ${new Date().toISOString()} ${post.error ? `Error: ${post.error}` : ''} ======================================== POST CONTENT (BBCode) ======================================== ${post.content || '[No content available]'} `; return { name: fileName, content: fileContent }; } async function testZipCreation() { updateStatus('Starting ZIP test with 3 posts...', 'success'); debugLog('Starting test ZIP creation'); const testBtn = document.getElementById('test-zip'); testBtn.disabled = true; try { // Create test data const testPosts = [ { id: 'test1', title: 'Test Post 1', subject: 'Test Subject 1', date: '2024-01-01', forum: 'Test Forum', topic: 'Test Topic', content: '[b]This is a test post[/b]\n\nWith some BBCode content.', viewUrl: 'http://example.com/post1', editUrl: 'http://example.com/edit1' }, { id: 'test2', title: 'Test Post 2', subject: 'Test Subject 2', date: '2024-01-02', forum: 'Test Forum', topic: 'Test Topic 2', content: 'Another test post with [url=http://example.com]a link[/url]', viewUrl: 'http://example.com/post2', editUrl: 'http://example.com/edit2' }, { id: 'test3', title: 'Test Post 3 with Error', subject: 'Test Subject 3', date: '2024-01-03', forum: 'Test Forum 2', topic: 'Test Topic 3', content: '[Unable to fetch content - no edit permission]', error: 'No edit permission', viewUrl: 'http://example.com/post3', editUrl: 'http://example.com/edit3' } ]; updateStatus('Creating test ZIP with 3 posts...', 'normal'); debugLog('Test posts created', testPosts); const zip = new JSZip(); // Add test posts to ZIP testPosts.forEach(post => { const forumName = sanitizeFilename(post.forum); const folder = zip.folder(forumName); const file = createPostFile(post); folder.file(file.name, file.content); debugLog(`Added test file: ${forumName}/${file.name}`); }); // Add README zip.file('README.txt', 'This is a TEST archive with 3 sample posts.\nIf this downloads successfully, ZIP creation is working!'); updateStatus('Generating test ZIP file...', 'normal'); debugLog('Starting ZIP generation'); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: CONFIG.ZIP_COMPRESSION_LEVEL } }, function(metadata) { debugLog(`ZIP progress: ${metadata.percent.toFixed(2)}%`); }); debugLog(`ZIP blob created, size: ${blob.size} bytes`); const filename = `TEST_Archive_${new Date().toISOString().replace(/[:.]/g, '-')}.zip`; saveAs(blob, filename); updateStatus('✅ TEST ZIP created successfully! Check your downloads.', 'success'); updateStatus(`Test file: ${filename} (${(blob.size/1024).toFixed(2)} KB)`, 'success'); debugLog('Test ZIP saved successfully'); } catch (error) { updateStatus(`❌ TEST ZIP FAILED: ${error.message}`, 'error'); debugLog('Test ZIP creation failed', error); console.error('ZIP Test Error:', error); } finally { testBtn.disabled = false; } } async function startArchiving() { if (isRunning) { updateStatus('Archive process already running', 'warning'); return; } isRunning = true; posts = []; processedPosts = 0; startTime = Date.now(); const startBtn = document.getElementById('start-archive'); const stopBtn = document.getElementById('stop-archive'); const progressDiv = document.getElementById('archiver-progress'); const clipboardBtn = document.getElementById('copy-clipboard'); if (startBtn) startBtn.style.display = 'none'; if (stopBtn) stopBtn.style.display = 'block'; if (progressDiv) progressDiv.style.display = 'block'; if (clipboardBtn) clipboardBtn.style.display = 'none'; updateStatus('Starting archive process...', 'success'); debugLog('Archive process started'); try { // Get all posts from search results posts = await getAllSearchPages(); totalPosts = posts.length; if (totalPosts === 0) { updateStatus('No posts found to archive', 'error'); stopArchiving(); return; } updateStatus(`Found ${totalPosts} posts to archive`, 'success'); debugLog(`Total posts to archive: ${totalPosts}`); // Fetch content for each post let successCount = 0; let errorCount = 0; for (const post of posts) { if (!isRunning) break; const shortTitle = post.title.substring(0, 50) + (post.title.length > 50 ? '...' : ''); updateStatus(`Fetching post ${post.id}: ${shortTitle}`); const success = await fetchPostContent(post); if (success) { successCount++; } else { errorCount++; if (post.error) { updateStatus(`⚠️ ${post.error} for post ${post.id}`, 'warning'); } } processedPosts++; updateProgress(); // Rate limiting if (CONFIG.ENABLE_RATE_LIMITING) { await delay(CONFIG.DELAY_BETWEEN_REQUESTS); } // Periodic memory check if (processedPosts % 50 === 0) { updateMemoryUsage(); // Small delay to let browser breathe await delay(100); } } if (isRunning) { updateStatus(`Fetched ${successCount} posts successfully (${errorCount} errors)`, 'success'); // Show clipboard button if (clipboardBtn) clipboardBtn.style.display = 'block'; // Try to create ZIP updateStatus('Creating ZIP archive...', 'normal'); try { await createZipArchive(); } catch (zipError) { updateStatus(`❌ ZIP creation failed: ${zipError.message}`, 'error'); updateStatus('💡 Use "Copy to Clipboard" button to export your data', 'warning'); debugLog('ZIP creation failed', zipError); } } } catch (error) { updateStatus(`Archive failed: ${error.message}`, 'error'); debugLog('Archive process failed', error); } finally { if (startBtn) startBtn.style.display = 'block'; if (stopBtn) stopBtn.style.display = 'none'; isRunning = false; } } function stopArchiving() { isRunning = false; updateStatus('Archive process stopped by user', 'warning'); debugLog('Archive process stopped'); const startBtn = document.getElementById('start-archive'); const stopBtn = document.getElementById('stop-archive'); const clipboardBtn = document.getElementById('copy-clipboard'); if (startBtn) startBtn.style.display = 'block'; if (stopBtn) stopBtn.style.display = 'none'; if (posts.length > 0 && clipboardBtn) { clipboardBtn.style.display = 'block'; } } async function createZipArchive() { debugLog('Starting ZIP archive creation'); const zip = new JSZip(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const safeAuthorName = sanitizeFilename(authorName); // Process in batches to avoid memory issues const batchSize = CONFIG.MAX_POSTS_PER_BATCH; const batches = Math.ceil(posts.length / batchSize); debugLog(`Processing ${posts.length} posts in ${batches} batches`); // Create folders by forum const forumFolders = {}; for (let i = 0; i < batches; i++) { const start = i * batchSize; const end = Math.min(start + batchSize, posts.length); const batchPosts = posts.slice(start, end); updateStatus(`Processing batch ${i + 1}/${batches} (posts ${start + 1}-${end})`, 'normal'); batchPosts.forEach(post => { const forumName = sanitizeFilename(post.forum); if (!forumFolders[forumName]) { forumFolders[forumName] = zip.folder(forumName); } const file = createPostFile(post); forumFolders[forumName].file(file.name, file.content); }); // Small delay between batches await delay(100); } // Add summary file const successCount = posts.filter(p => !p.error).length; const errorCount = posts.filter(p => p.error).length; const summary = `Forum Post Archive ================== User: ${authorName} (ID: ${authorId}) Total Posts: ${posts.length} Successfully Archived: ${successCount} Errors: ${errorCount} Archive Date: ${new Date().toString()} Forum: ${window.location.hostname} Posts by Forum: ${Object.keys(forumFolders).map(f => { const forumPosts = posts.filter(p => sanitizeFilename(p.forum) === f); return `- ${f}: ${forumPosts.length} posts`; }).join('\n')} ${errorCount > 0 ? `\nPosts with Errors:\n${posts.filter(p => p.error).map(p => `- Post ${p.id}: ${p.error}`).join('\n')}` : ''} Notes: - Files are in BBCode format - Posts you don't have permission to edit show as [Unable to fetch content] - Archive created with Forum Post Archiver userscript v2.0 `; zip.file('README.txt', summary); // Generate and download ZIP with progress monitoring // Generate and download ZIP with progress monitoring try { updateStatus('Generating ZIP file (this may take a moment)...', 'normal'); debugLog('Starting ZIP blob generation'); debugLog(`Total posts in archive: ${posts.length}`); debugLog(`Total forums: ${Object.keys(forumFolders).length}`); // Log memory before ZIP generation if (performance.memory) { debugLog(`Memory before ZIP: ${(performance.memory.usedJSHeapSize / 1048576).toFixed(2)}MB`); } let lastProgress = 0; let progressCallCount = 0; debugLog('Calling zip.generateAsync...'); // Add timeout wrapper const zipPromise = new Promise(async (resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('ZIP generation timeout after 60 seconds')); }, 60000); // 60 second timeout try { debugLog('Inside ZIP promise, starting generation'); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: CONFIG.ZIP_COMPRESSION_LEVEL } }, function(metadata) { progressCallCount++; const progress = Math.round(metadata.percent); // Log every 5% instead of 10% for more feedback if (progress !== lastProgress && progress % 5 === 0) { updateStatus(`ZIP compression: ${progress}%`, 'normal'); debugLog(`ZIP generation progress: ${metadata.percent.toFixed(2)}% (callback #${progressCallCount})`); lastProgress = progress; } // Also log the first callback if (progressCallCount === 1) { debugLog(`First progress callback received: ${metadata.percent.toFixed(2)}%`); } }); clearTimeout(timeout); debugLog('zip.generateAsync completed successfully'); resolve(blob); } catch (innerError) { clearTimeout(timeout); debugLog(`zip.generateAsync threw error: ${innerError.message}`); console.error('Full ZIP generation error:', innerError); reject(innerError); } }); debugLog('Awaiting ZIP promise...'); const blob = await zipPromise; debugLog(`ZIP blob created successfully, size: ${blob.size} bytes (${(blob.size / 1048576).toFixed(2)}MB)`); debugLog(`Progress callbacks received: ${progressCallCount}`); // Log memory after ZIP generation if (performance.memory) { debugLog(`Memory after ZIP: ${(performance.memory.usedJSHeapSize / 1048576).toFixed(2)}MB`); } // Verify blob is valid if (!blob || blob.size === 0) { throw new Error('Generated blob is empty or invalid'); } const filename = `Forum_Archive_${safeAuthorName}_${timestamp}.zip`; debugLog(`Filename: ${filename}`); updateStatus(`ZIP ready, initiating download...`, 'normal'); debugLog('Calling saveAs...'); try { saveAs(blob, filename); debugLog('saveAs called successfully'); } catch (saveError) { debugLog(`saveAs error: ${saveError.message}`); console.error('Full saveAs error:', saveError); throw saveError; } const sizeKB = (blob.size / 1024).toFixed(2); const sizeMB = (blob.size / 1048576).toFixed(2); updateStatus(`✅ Archive completed! ${successCount} posts saved successfully.`, 'success'); updateStatus(`📦 File: ${filename} (${sizeMB > 1 ? sizeMB + ' MB' : sizeKB + ' KB'})`, 'success'); if (errorCount > 0) { updateStatus(`⚠️ ${errorCount} posts had errors (see README.txt)`, 'warning'); } debugLog('ZIP archive saved successfully'); debugLog('=== ZIP Generation Complete ==='); } catch (error) { debugLog('=== ZIP Generation Failed ==='); debugLog(`Error type: ${error.constructor.name}`); debugLog(`Error message: ${error.message}`); debugLog(`Error stack: ${error.stack}`); console.error('Full ZIP generation error object:', error); // Check for specific error types if (error.message.includes('timeout')) { updateStatus('ZIP generation timed out - archive may be too large', 'error'); updateStatus('Try using the clipboard export instead', 'warning'); } else if (error.message.includes('memory')) { updateStatus('Out of memory - archive too large for browser', 'error'); updateStatus('Try closing other tabs and retrying', 'warning'); } else { updateStatus(`ZIP error: ${error.message}`, 'error'); } throw error; } } async function copyToClipboard() { if (!posts || posts.length === 0) { updateStatus('No posts to copy', 'error'); return; } const clipboardBtn = document.getElementById('copy-clipboard'); clipboardBtn.disabled = true; try { updateStatus('Preparing clipboard export...', 'normal'); debugLog(`Copying ${posts.length} posts to clipboard`); // Create text format for all posts let clipboardText = `FORUM POST ARCHIVE ================== User: ${authorName} (ID: ${authorId}) Total Posts: ${posts.length} Archive Date: ${new Date().toString()} Forum: ${window.location.hostname} ========================================== `; // Group posts by forum const postsByForum = {}; posts.forEach(post => { const forum = post.forum || 'Unknown Forum'; if (!postsByForum[forum]) { postsByForum[forum] = []; } postsByForum[forum].push(post); }); // Add posts to clipboard text Object.keys(postsByForum).forEach(forum => { clipboardText += `\n════════════════════════════════════════ FORUM: ${forum} ════════════════════════════════════════\n\n`; postsByForum[forum].forEach(post => { clipboardText += `---------------------------------------- Post ID: ${post.id} Subject: ${post.subject || post.title} Date: ${post.date} Topic: ${post.topic} URL: ${post.viewUrl} ${post.error ? `Error: ${post.error}` : ''} ---------------------------------------- ${post.content || '[No content available]'} ======================================== `; }); }); // Try GM_setClipboard first (more reliable) if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(clipboardText); updateStatus('✅ All posts copied to clipboard!', 'success'); updateStatus(`📋 ${posts.length} posts exported as text`, 'success'); debugLog('Clipboard export successful using GM_setClipboard'); } else { // Fallback to navigator.clipboard await navigator.clipboard.writeText(clipboardText); updateStatus('✅ All posts copied to clipboard!', 'success'); updateStatus(`📋 ${posts.length} posts exported as text`, 'success'); debugLog('Clipboard export successful using navigator.clipboard'); } } catch (error) { updateStatus(`❌ Clipboard copy failed: ${error.message}`, 'error'); debugLog('Clipboard export failed', error); // Fallback: Create textarea for manual copy updateStatus('Creating manual copy option...', 'warning'); // Create container const container = document.createElement('div'); container.style.position = 'fixed'; container.style.top = '0'; container.style.left = '0'; container.style.right = '0'; container.style.bottom = '0'; container.style.backgroundColor = 'rgba(0,0,0,0.8)'; container.style.zIndex = '10001'; container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; // Create inner wrapper const wrapper = document.createElement('div'); wrapper.style.width = '80%'; wrapper.style.maxWidth = '800px'; wrapper.style.backgroundColor = '#fff'; wrapper.style.borderRadius = '10px'; wrapper.style.padding = '20px'; wrapper.style.boxShadow = '0 4px 6px rgba(0,0,0,0.3)'; // Create header const header = document.createElement('div'); header.innerHTML = '<h3 style="margin-top:0;color:#333;">Manual Copy Required</h3><p style="color:#666;">Select all text below and press Ctrl+C (or Cmd+C on Mac) to copy:</p>'; wrapper.appendChild(header); // Create textarea const textarea = document.createElement('textarea'); textarea.value = clipboardText; textarea.style.width = '100%'; textarea.style.height = '400px'; textarea.style.border = '2px solid #00ff00'; textarea.style.padding = '10px'; textarea.style.fontFamily = 'monospace'; textarea.style.fontSize = '12px'; textarea.style.resize = 'vertical'; wrapper.appendChild(textarea); // Create button container const buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '10px'; buttonContainer.style.textAlign = 'right'; // Create select all button const selectBtn = document.createElement('button'); selectBtn.textContent = 'Select All'; selectBtn.style.padding = '10px 20px'; selectBtn.style.marginRight = '10px'; selectBtn.style.backgroundColor = '#00aaff'; selectBtn.style.color = '#fff'; selectBtn.style.border = 'none'; selectBtn.style.borderRadius = '5px'; selectBtn.style.cursor = 'pointer'; selectBtn.onclick = () => { textarea.select(); textarea.setSelectionRange(0, 99999999); }; buttonContainer.appendChild(selectBtn); // Create close button const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; closeBtn.style.padding = '10px 20px'; closeBtn.style.backgroundColor = '#ff4444'; closeBtn.style.color = '#fff'; closeBtn.style.border = 'none'; closeBtn.style.borderRadius = '5px'; closeBtn.style.cursor = 'pointer'; closeBtn.onclick = () => { container.remove(); }; buttonContainer.appendChild(closeBtn); wrapper.appendChild(buttonContainer); container.appendChild(wrapper); document.body.appendChild(container); // Auto-select text textarea.select(); textarea.setSelectionRange(0, 99999999); updateStatus('📋 Text selected - press Ctrl+C to copy', 'warning'); } finally { clipboardBtn.disabled = false; } } // Initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } // Log version console.log('Forum Post Archiver v2.0 loaded - Enhanced with verbose logging and clipboard export'); } })();