Skip to content

Instantly share code, notes, and snippets.

@ivanlonel
Last active October 10, 2025 20:48
Show Gist options
  • Select an option

  • Save ivanlonel/292e43f6dda6417f1bd242f31f21be9e to your computer and use it in GitHub Desktop.

Select an option

Save ivanlonel/292e43f6dda6417f1bd242f31f21be9e to your computer and use it in GitHub Desktop.
Pokemon Zone Card Collection CSV Downloader
// ==UserScript==
// @name Pokemon Zone Card Collection CSV Downloader
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Download Pokemon card collection data as CSV from Pokemon Zone
// @author Ivan Donisete Lonel
// @match https://www.pokemon-zone.com/players/*/cards/
// @grant none
// ==/UserScript==
(function () {
'use strict';
// Configuration constants
const CONFIG = {
WAIT_TIMEOUT: 10000,
ELEMENT_CHECK_INTERVAL: 100,
MAX_LOAD_MORE_CLICKS: 1000,
LOAD_MORE_DELAY: 800,
FINAL_LOAD_DELAY: 2000,
BUTTON_RESET_DELAY: 3000,
CONTENT_WAIT_TIMEOUT: 5000,
BATCH_SIZE: 500
};
// Selector constants
const SELECTORS = {
ROOT: 'div.data-infinite-scroller',
LOAD_MORE: 'div.data-infinite-scroller__more > button',
CARD_CONTAINER: 'div.collection-card-grid > div.player-expansion-collection-card',
CARD_LINK: 'div.player-expansion-collection-card__preview > div.player-expansion-collection-card__card > div.game-card-image > a',
CARD_NAME: 'div.player-expansion-collection-card__footer > div.player-expansion-collection-card__name > div.player-expansion-collection-card__name-text',
CARD_RARITY: 'div.player-expansion-collection-card__footer > div.player-expansion-collection-card__name > div.player-expansion-collection-card__name-rarity > span.rarity-icon',
CARD_AMOUNT: 'div.player-expansion-collection-card__preview > div.player-expansion-collection-card__count',
CARD_UNREGISTERED: 'div.player-expansion-collection-card__preview > div.player-expansion-collection-card__number'
};
// Rarity icon mapping
const RARITY_ICONS = {
'rarity-icon__icon--diamond': '♦',
'rarity-icon__icon--star': '★',
'rarity-icon__icon--shiny': '✷',
'rarity-icon__icon--crown': '♛'
};
// Global state
let isDownloading = false;
let downloadCancelled = false;
// Utility function to log with timestamp
function log(message, ...args) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
console.log(`[${timestamp}] ${message}`, ...args);
}
// Wait for element to appear
function waitForElement(selector, timeout = CONFIG.WAIT_TIMEOUT) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
const element = document.querySelector(selector);
if (element) {
resolve(element);
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
} else {
setTimeout(check, CONFIG.ELEMENT_CHECK_INTERVAL);
}
}
check();
});
}
// Wait for new content to load
async function waitForNewContent(previousCount, rootDiv) {
const maxWait = CONFIG.CONTENT_WAIT_TIMEOUT;
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
if (downloadCancelled) return false;
const currentCount = countCardElements(rootDiv);
if (currentCount > previousCount) {
log(`Content loaded: ${previousCount} -> ${currentCount} cards`);
return true;
}
await new Promise(r => setTimeout(r, CONFIG.ELEMENT_CHECK_INTERVAL));
}
log('No new content detected within timeout');
return false;
}
// Count card elements
function countCardElements(rootDiv) {
const cardElements = rootDiv.querySelectorAll(SELECTORS.CARD_CONTAINER);
return Array.from(cardElements).filter(el => {
const classes = Array.from(el.classList);
return classes.some(cls =>
cls === 'player-expansion-collection-card' ||
(cls.startsWith('player-expansion-collection-card') && !cls.includes('__'))
);
}).length;
}
// Sanitize CSV field to prevent injection
function sanitizeCSVField(field) {
const str = field.toString();
// Prevent CSV injection attacks
if (str.startsWith('=') || str.startsWith('+') ||
str.startsWith('-') || str.startsWith('@') ||
str.startsWith('\t') || str.startsWith('\r')) {
return `'${str.replace(/"/g, '""')}`;
}
return str.replace(/"/g, '""');
}
// Create and download CSV
function downloadCSV(data, filename) {
const csvContent = data.map(row =>
row.map(field => `"${sanitizeCSVField(field)}"`).join(',')
).join('\n');
const bom = '\uFEFF'; // UTF-8 BOM for better Excel compatibility
const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the object URL
setTimeout(() => URL.revokeObjectURL(url), 100);
}
// Compose rarity string from icons
function getRarityString(rarityElement) {
if (!rarityElement) return '';
const spans = rarityElement.querySelectorAll('span');
let rarityString = '';
spans.forEach(span => {
for (const [className, icon] of Object.entries(RARITY_ICONS)) {
if (span.classList.contains(className)) {
rarityString += icon;
break;
}
}
});
return rarityString;
}
// Extract card data with error handling
function extractCardData(cardElement) {
const data = {
set: '',
id: '',
name: 'Unknown Card',
rarity: '',
amount: '0',
registered: false
};
try {
if (!cardElement) return data;
// Extract Set and ID from link
const linkElement = cardElement.querySelector(SELECTORS.CARD_LINK);
if (linkElement && linkElement.href) {
const href = linkElement.getAttribute('href');
const pathParts = href.split('/').filter(part => part.length > 0);
if (pathParts.length >= 4 && pathParts[0] === 'cards') {
data.set = pathParts[1].charAt(0).toUpperCase() + pathParts[1].slice(1);
data.id = pathParts[2];
}
}
// Extract name
const nameElement = cardElement.querySelector(SELECTORS.CARD_NAME);
if (nameElement) {
data.name = nameElement.textContent.trim() || data.name;
}
// Extract rarity
const rarityElement = cardElement.querySelector(SELECTORS.CARD_RARITY);
if (rarityElement) {
data.rarity = getRarityString(rarityElement);
}
// Extract amount
const amountElement = cardElement.querySelector(SELECTORS.CARD_AMOUNT);
if (amountElement) {
data.amount = amountElement.textContent.trim() || '0';
}
// Check registration status
const unregisteredElement = cardElement.querySelector(SELECTORS.CARD_UNREGISTERED);
data.registered = !unregisteredElement;
return data;
} catch (error) {
log('Error extracting card data:', error, cardElement);
return data;
}
}
// Update UI elements
function updateUI(button, text) {
button.textContent = text;
}
// Calculate statistics
function calculateStats(cardData) {
const registered = cardData.filter(c => c.registered).length;
const unregistered = cardData.length - registered;
const totalAmount = cardData.reduce((sum, c) => sum + parseInt(c.amount || 0), 0);
const uniqueSets = new Set(cardData.map(c => c.set).filter(s => s)).size;
return { registered, unregistered, totalAmount, uniqueSets };
}
// Main download handler
async function handleDownload(button, cancelButton) {
if (isDownloading) {
log('Download already in progress');
return;
}
isDownloading = true;
downloadCancelled = false;
button.disabled = true;
cancelButton.style.display = 'inline-block';
try {
const rootDiv = document.querySelector(SELECTORS.ROOT);
if (!rootDiv) {
throw new Error('Root div not found');
}
updateUI(button, 'Loading cards...');
// Load all cards by clicking "Load More"
let loadMoreButton;
let clickCount = 0;
let previousCount = countCardElements(rootDiv);
let consecutiveNoChange = 0;
const maxNoChange = 3; // Stop if no change after 3 attempts
while (clickCount < CONFIG.MAX_LOAD_MORE_CLICKS) {
if (downloadCancelled) {
throw new Error('Download cancelled by user');
}
loadMoreButton = rootDiv.querySelector(SELECTORS.LOAD_MORE);
if (!loadMoreButton || loadMoreButton.style.display === 'none' || !loadMoreButton.offsetParent) {
log('Load more button no longer visible');
break;
}
log(`Clicking load more button (${clickCount + 1})`);
loadMoreButton.click();
clickCount++;
// Wait for new content with smart detection
const currentCount = countCardElements(rootDiv);
const contentLoaded = await waitForNewContent(currentCount, rootDiv);
const newCount = countCardElements(rootDiv);
// Check if content actually increased
if (newCount <= previousCount) {
consecutiveNoChange++;
if (consecutiveNoChange >= maxNoChange) {
log(`No new cards loaded after ${maxNoChange} attempts, stopping`);
break;
}
} else {
consecutiveNoChange = 0;
}
previousCount = newCount;
updateUI(button, `Loading... (${previousCount} cards found)`);
await new Promise(resolve => setTimeout(resolve, CONFIG.LOAD_MORE_DELAY));
}
if (clickCount >= CONFIG.MAX_LOAD_MORE_CLICKS) {
log('Reached maximum click limit');
}
updateUI(button, 'Waiting for final content...');
await new Promise(resolve => setTimeout(resolve, CONFIG.FINAL_LOAD_DELAY));
// Extract all card elements
updateUI(button, 'Extracting card data...');
const allDivs = rootDiv.querySelectorAll(SELECTORS.CARD_CONTAINER);
const cardElements = Array.from(allDivs).filter(el => {
const classes = Array.from(el.classList);
return classes.some(cls =>
cls === 'player-expansion-collection-card' ||
(cls.startsWith('player-expansion-collection-card') && !cls.includes('__'))
);
});
log(`Found ${cardElements.length} card elements`);
if (cardElements.length === 0) {
throw new Error('No card elements found');
}
// Extract card data in batches
const allCardData = [];
for (let i = 0; i < cardElements.length; i += CONFIG.BATCH_SIZE) {
if (downloadCancelled) {
throw new Error('Download cancelled by user');
}
const batch = cardElements.slice(i, Math.min(i + CONFIG.BATCH_SIZE, cardElements.length));
const batchData = batch.map(extractCardData);
allCardData.push(...batchData);
updateUI(button, `Processing... (${i + batch.length}/${cardElements.length})`);
}
// Calculate statistics
const stats = calculateStats(allCardData);
log('Statistics:', stats);
// Prepare CSV data
updateUI(button, 'Generating CSV...');
const csvData = [['Set', 'ID', 'Name', 'Rarity', 'Amount', 'Registered']];
allCardData.forEach(card => {
csvData.push([
card.set,
card.id,
card.name,
card.rarity,
card.amount,
card.registered ? '✓' : ''
]);
});
// Download CSV
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');
const filename = `pokemon-cards-${dateStr}_${timeStr}.csv`;
downloadCSV(csvData, filename);
updateUI(
button,
`✓ Downloaded ${allCardData.length} cards (${stats.registered} registered, ${stats.uniqueSets} sets)`
);
log(`CSV downloaded: ${allCardData.length} cards, ${stats.registered} registered, ${stats.uniqueSets} unique sets`);
} catch (error) {
log('Error during download:', error);
updateUI(button, `Error: ${error.message}`);
alert(`Download failed: ${error.message}\n\nCheck the console for details.`);
} finally {
isDownloading = false;
cancelButton.style.display = 'none';
setTimeout(() => {
button.disabled = false;
updateUI(button, 'Download CSV');
}, CONFIG.BUTTON_RESET_DELAY);
}
}
// Cancel download
function cancelDownload(button, cancelButton) {
if (!isDownloading) return;
downloadCancelled = true;
log('Download cancellation requested');
updateUI(button, 'Cancelling...');
}
// Create UI elements
function createUI() {
const container = document.createElement('div');
container.style.cssText = `
margin: 10px 0;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;
// Download button
const downloadButton = document.createElement('button');
downloadButton.id = 'pokemon-csv-download';
downloadButton.textContent = 'Download CSV';
downloadButton.style.cssText = `
background-color: #4CAF50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
transition: background-color 0.3s;
`;
// Cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
background-color: #f44336;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: none;
transition: background-color 0.3s;
`;
// Button hover effects
downloadButton.addEventListener('mouseenter', () => {
if (!downloadButton.disabled) {
downloadButton.style.backgroundColor = '#45a049';
}
});
downloadButton.addEventListener('mouseleave', () => {
downloadButton.style.backgroundColor = '#4CAF50';
});
cancelButton.addEventListener('mouseenter', () => {
cancelButton.style.backgroundColor = '#da190b';
});
cancelButton.addEventListener('mouseleave', () => {
cancelButton.style.backgroundColor = '#f44336';
});
// Event listeners
downloadButton.addEventListener('click', () =>
handleDownload(downloadButton, cancelButton)
);
cancelButton.addEventListener('click', () =>
cancelDownload(downloadButton, cancelButton)
);
// Assemble UI
container.appendChild(downloadButton);
container.appendChild(cancelButton);
return container;
}
// Initialize script
async function init() {
try {
log('Initializing Pokemon Zone CSV Downloader v2.0');
const rootDiv = await waitForElement(SELECTORS.ROOT);
const uiContainer = createUI();
if (rootDiv.firstChild) {
rootDiv.insertBefore(uiContainer, rootDiv.firstChild);
} else {
rootDiv.appendChild(uiContainer);
}
log('Pokemon Zone CSV Downloader initialized successfully');
} catch (error) {
log('Failed to initialize:', error);
}
}
// Start the script
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment