// Wikipedia Category Items Copier (API Version)
// This script adds two buttons to Wikipedia category pages:
// 1. "Copy Items" - Copies all items in the current category via API
// 2. "Copy All Items" - Copies all items in the current category and its subcategories via API
// Only run on Wikipedia category pages
if (window.___location.href.includes('/wiki/Category:')) {
// Extract the category name from the URL
const categoryName = decodeURIComponent(window.___location.pathname.split('/Category:')[1]);
console.log("Category Name:", categoryName);
// Create a container for our buttons
const container = document.createElement('div');
container.style.padding = '10px';
container.style.margin = '10px 0';
container.style.backgroundColor = '#f8f9fa';
container.style.border = '1px solid #a2a9b1';
container.style.borderRadius = '3px';
// Create the "Copy Items" button
const copyItemsBtn = document.createElement('button');
copyItemsBtn.textContent = 'Copy Items from this Category (API)';
copyItemsBtn.style.marginRight = '10px';
copyItemsBtn.style.padding = '8px 12px';
copyItemsBtn.style.cursor = 'pointer';
// Create the "Copy All Items" button
const copyAllItemsBtn = document.createElement('button');
copyAllItemsBtn.textContent = 'Copy Items from All Subcategories (API)';
copyAllItemsBtn.style.padding = '8px 12px';
copyAllItemsBtn.style.cursor = 'pointer';
// Create status text
const statusText = document.createElement('div');
statusText.style.marginTop = '10px';
statusText.style.color = '#555';
// Add buttons to container
container.appendChild(copyItemsBtn);
container.appendChild(copyAllItemsBtn);
container.appendChild(statusText);
// Insert container after the page title
const pageTitleHeading = document.querySelector('.mw-first-heading');
if (pageTitleHeading) {
pageTitleHeading.parentNode.insertBefore(container, pageTitleHeading.nextSibling);
} else {
document.querySelector('#content').prepend(container);
}
// Function to copy text to clipboard (Firefox on Pop OS compatible)
function copyToClipboard(text) {
return new Promise((resolve, reject) => {
// First try the modern Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => resolve(true))
.catch(err => {
console.log("Clipboard API failed, trying execCommand...", err);
// Fall back to execCommand method
execCommandCopy();
});
} else {
// If Clipboard API is not available, use execCommand
execCommandCopy();
}
function execCommandCopy() {
// Create a completely invisible temporary textarea
const textarea = document.createElement('textarea');
textarea.value = text;
// Make the textarea invisible but still selectable
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
textarea.style.opacity = '0';
textarea.setAttribute('readonly', ''); // Prevent mobile keyboard from appearing
document.body.appendChild(textarea);
// Select and try to copy
if (navigator.userAgent.indexOf('Firefox') !== -1 ||
navigator.userAgent.indexOf('Linux') !== -1) {
// Special handling for Firefox on Linux/Pop OS
textarea.contentEditable = true;
textarea.readOnly = false;
}
// Handle iOS specifics
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
const range = document.createRange();
range.selectNodeContents(textarea);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textarea.setSelectionRange(0, 999999);
} else {
textarea.select();
}
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
resolve(true);
} else {
reject(new Error('Unable to copy to clipboard using execCommand'));
}
} catch (err) {
document.body.removeChild(textarea);
reject(err);
}
}
});
}
// Function to get all members of a category using Wikipedia API
async function getCategoryMembers(categoryTitle, continueToken = null) {
try {
// Base API URL
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching category members for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 300));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { members: [], continueToken: null };
}
// Extract members and continue token
const members = data.query.categorymembers.map(member => member.title);
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { members, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching category members: ${error.message}`;
return { members: [], continueToken: null };
}
}
// Function to get all subcategories of a category
async function getSubcategories(categoryTitle, continueToken = null) {
try {
// Base API URL for subcategories (only get items with namespace 14, which is Category)
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=14&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching subcategories for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 300));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { subcategories: [], continueToken: null };
}
// Extract subcategories and continue token
const subcategories = data.query.categorymembers.map(member => member.title.replace('Category:', ''));
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { subcategories, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching subcategories: ${error.message}`;
return { subcategories: [], continueToken: null };
}
}
// Function to get all non-category members of a category
async function getNonCategoryMembers(categoryTitle, continueToken = null) {
try {
// Base API URL for non-category members (exclude namespace 14, which is Category)
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=0|1|2|3|4|5|6|7|8|9|10|11|12|13|15&cmlimit=500&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching items for: ${categoryTitle}...`;
// Add a small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 300));
const response = await fetch(apiUrl);
const data = await response.json();
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { members: [], continueToken: null };
}
// Extract members and continue token
const members = data.query.categorymembers.map(member => member.title);
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { members, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.textContent = `Error fetching items: ${error.message}`;
return { members: [], continueToken: null };
}
}
// Function to get all members of a category, handling pagination
async function getAllCategoryMembers(categoryTitle) {
let allMembers = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const { members, continueToken: nextToken } = await getNonCategoryMembers(categoryTitle, continueToken);
allMembers = allMembers.concat(members);
continueToken = nextToken;
pagesProcessed++;
statusText.textContent = `Retrieved ${allMembers.length} items from "${categoryTitle}" (page ${pagesProcessed})...`;
// Safeguard against infinite loops
if (pagesProcessed > 20) {
statusText.textContent += " (Stopped after 20 pages to avoid overloading)";
break;
}
} while (continueToken);
return allMembers;
}
// Function to get all subcategories of a category, handling pagination
async function getAllSubcategories(categoryTitle) {
let allSubcategories = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const { subcategories, continueToken: nextToken } = await getSubcategories(categoryTitle, continueToken);
allSubcategories = allSubcategories.concat(subcategories);
continueToken = nextToken;
pagesProcessed++;
// Safeguard against infinite loops
if (pagesProcessed > 10) {
break;
}
} while (continueToken);
return allSubcategories;
}
// Handle "Copy Items" button click
copyItemsBtn.addEventListener('click', async () => {
statusText.textContent = 'Gathering items from this category via API...';
try {
const items = await getAllCategoryMembers(categoryName);
if (items.length === 0) {
statusText.textContent = 'No items found in this category.';
return;
}
await copyToClipboard(items.join('\n'));
statusText.textContent = `Successfully copied ${items.length} items to clipboard.`;
} catch (error) {
statusText.textContent = `Error: ${error.message}`;
console.error('Error:', error);
}
});
// Handle "Copy All Items" button click
copyAllItemsBtn.addEventListener('click', async () => {
statusText.textContent = 'Gathering items from all subcategories via API (this may take a while)...';
try {
// Get items from the current category
let allItems = await getAllCategoryMembers(categoryName);
statusText.textContent = `Found ${allItems.length} items in main category. Checking subcategories...`;
// Get all subcategories
const subcategories = await getAllSubcategories(categoryName);
statusText.textContent = `Found ${subcategories.length} subcategories. Processing...`;
// Process a limited number of subcategories to avoid overloading
const MAX_SUBCATEGORIES = 10;
const processedCategories = new Set(); // To avoid processing the same category twice
for (let i = 0; i < Math.min(subcategories.length, MAX_SUBCATEGORIES); i++) {
const subcategoryTitle = subcategories[i];
if (!processedCategories.has(subcategoryTitle)) {
processedCategories.add(subcategoryTitle);
const subcategoryItems = await getAllCategoryMembers(subcategoryTitle);
allItems = allItems.concat(subcategoryItems);
statusText.textContent = `Processed ${i + 1}/${Math.min(subcategories.length, MAX_SUBCATEGORIES)} subcategories. Found ${allItems.length} items so far...`;
}
}
if (subcategories.length > MAX_SUBCATEGORIES) {
statusText.textContent += ` (Limited to ${MAX_SUBCATEGORIES} subcategories to avoid overloading. Found ${allItems.length} items.)`;
}
// Deduplicate items
const uniqueItems = [...new Set(allItems)];
if (uniqueItems.length === 0) {
statusText.textContent = 'No items found in this category or its subcategories.';
return;
}
await copyToClipboard(uniqueItems.join('\n'));
statusText.textContent = `Successfully copied ${uniqueItems.length} unique items to clipboard.`;
} catch (error) {
statusText.textContent = `Error: ${error.message}`;
console.error('Error:', error);
}
});
console.log('Wikipedia Category Copier script (API version) has been loaded successfully!');
} else {
// Do nothing on non-category pages
console.log('Wikipedia Category Copier: Not a category page, script inactive.');
}