// ==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');
}
})();