Skip to content

Instantly share code, notes, and snippets.

@TMcManus
Created March 10, 2025 17:24
Show Gist options
  • Save TMcManus/88899ff72a3a08e9ae075bf728c2eb04 to your computer and use it in GitHub Desktop.
Save TMcManus/88899ff72a3a08e9ae075bf728c2eb04 to your computer and use it in GitHub Desktop.
Pixieset does not provide a way to export clients. This javascript runs in Tampermonkey and provides a button that exports clients records to CSV.
// ==UserScript==
// @name Pixieset Client Exporter
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Export all Pixieset clients to CSV
// @author You
// @match https://studio.pixieset.com/contacts*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Add a button to the page
function addExportButton() {
// Check if button already exists
if (!document.getElementById('export-all-clients-btn')) {
console.log('Adding export button to page');
const button = document.createElement('button');
button.id = 'export-all-clients-btn';
button.textContent = 'Export All Clients to CSV';
button.style.padding = '8px 16px';
button.style.margin = '10px';
button.style.backgroundColor = '#0073e6';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
button.style.fontWeight = 'bold';
button.addEventListener('click', confirmExport);
// Try different selectors that might exist on the page
const possibleSelectors = [
'.contact-list-header',
'.client-list-header',
'.client-list',
'.list-header',
'header',
'.header-container'
];
let targetElement = null;
for (const selector of possibleSelectors) {
targetElement = document.querySelector(selector);
if (targetElement) break;
}
// Fall back to body if no suitable container found
if (!targetElement) {
targetElement = document.body;
button.style.position = 'fixed';
button.style.top = '10px';
button.style.right = '10px';
button.style.zIndex = '9999';
}
targetElement.prepend(button);
console.log('Export button added');
}
}
// Confirm export with user before proceeding
function confirmExport() {
if (confirm('This will export all clients to CSV. The process may take a while if you have many clients. Continue?')) {
fetchAllClientsToCSV();
}
}
// Function to fetch all clients and convert to CSV
async function fetchAllClientsToCSV() {
let allClients = [];
let currentPage = 1;
let hasMorePages = true;
let totalPages = 0;
// Disable button during export
const exportButton = document.getElementById('export-all-clients-btn');
if (exportButton) {
exportButton.disabled = true;
exportButton.style.backgroundColor = '#999';
exportButton.style.cursor = 'not-allowed';
}
console.log("Starting to fetch clients...");
const statusSpan = createStatusElement();
updateStatus(statusSpan, "Fetching clients...");
try {
while (hasMorePages) {
try {
// Add delay for rate limiting (skip delay for first page)
if (currentPage > 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
// Construct URL with page parameter
const url = `https://studio.pixieset.com/api/v1/clients/?&page=${currentPage}&types=client,lead`;
// Calculate and show progress if we know total pages
if (totalPages > 0) {
const progressPct = Math.round((currentPage - 1) / totalPages * 100);
updateStatus(statusSpan, `Fetching page ${currentPage} of ${totalPages} (${progressPct}%)...`);
} else {
updateStatus(statusSpan, `Fetching page ${currentPage}...`);
}
// Fetch the data
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include' // Include cookies for authenticated requests
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const responseData = await response.json();
// Validate response structure
if (!responseData || typeof responseData !== 'object') {
throw new Error('Invalid API response format');
}
// Add the clients from this page to our collection
if (responseData.data && Array.isArray(responseData.data)) {
allClients = allClients.concat(responseData.data);
updateStatus(statusSpan, `Added ${responseData.data.length} clients from page ${currentPage}`);
// Check if there are more pages
if (responseData.meta && responseData.meta.pagination) {
const pagination = responseData.meta.pagination;
totalPages = pagination.total_pages || 0;
hasMorePages = currentPage < totalPages;
if (!hasMorePages) {
updateStatus(statusSpan, `Reached last page (${currentPage} of ${totalPages})`);
}
} else {
// If there's no pagination info or we got fewer results, assume this is the last page
if (responseData.data.length === 0) {
updateStatus(statusSpan, "Received empty data array. Stopping.");
hasMorePages = false;
}
}
} else {
updateStatus(statusSpan, "No data array found in response. Stopping.");
hasMorePages = false;
}
currentPage++;
} catch (error) {
console.error("Error fetching clients:", error);
updateStatus(statusSpan, `Error: ${error.message}`);
hasMorePages = false;
}
}
updateStatus(statusSpan, `Completed fetching all clients. Total: ${allClients.length}`);
if (allClients.length === 0) {
updateStatus(statusSpan, "No clients found to export.");
return;
}
// Convert to CSV
updateStatus(statusSpan, "Converting to CSV...");
const csv = convertToCSV(allClients);
// Create download link
updateStatus(statusSpan, "Downloading CSV...");
downloadCSV(csv, 'pixieset_clients.csv');
updateStatus(statusSpan, "Export complete! CSV file downloaded.");
} finally {
// Re-enable button regardless of success or failure
if (exportButton) {
exportButton.disabled = false;
exportButton.style.backgroundColor = '#0073e6';
exportButton.style.cursor = 'pointer';
}
// Remove status element after 5 seconds
setTimeout(() => {
if (statusSpan && statusSpan.parentNode) {
statusSpan.parentNode.removeChild(statusSpan);
}
}, 5000);
}
}
// Create a status element to show progress
function createStatusElement() {
const statusSpan = document.createElement('div');
statusSpan.id = 'export-status';
statusSpan.style.padding = '10px';
statusSpan.style.margin = '10px';
statusSpan.style.backgroundColor = '#f8f9fa';
statusSpan.style.border = '1px solid #ddd';
statusSpan.style.borderRadius = '4px';
statusSpan.style.fontFamily = 'sans-serif';
statusSpan.style.fontSize = '14px';
const exportButton = document.getElementById('export-all-clients-btn');
if (exportButton && exportButton.parentNode) {
exportButton.parentNode.insertBefore(statusSpan, exportButton.nextSibling);
} else {
document.body.appendChild(statusSpan);
}
return statusSpan;
}
// Update status element with new message
function updateStatus(element, message) {
if (element) {
element.textContent = message;
console.log(message);
}
}
// Sanitize CSV value to prevent formula injection
function sanitizeCSVValue(value) {
if (value === null || value === undefined) return '';
const strValue = String(value);
const escaped = strValue.replace(/"/g, '""');
// Prepend tab to values starting with CSV injection characters
return /^[=+\-@]/.test(escaped) ? `\t${escaped}` : escaped;
}
// Convert JSON data to CSV
function convertToCSV(data) {
if (data.length === 0) {
return '';
}
// Define the headers
const headers = [
'id', 'email', 'status_name', 'first_name', 'last_name', 'phone',
'company', 'job_title', 'address_line_1', 'address_line_2', 'city',
'postal_code', 'state', 'country', 'notes', 'avatar_color',
'created_at', 'sample', 'type', 'type_formatted'
];
// Create the header row
let csvContent = headers.join(',') + '\n';
// Add each row of data
data.forEach(client => {
const address = client.address_info || {};
const rowValues = [
sanitizeCSVValue(client.id),
sanitizeCSVValue(client.email),
sanitizeCSVValue(client.status_name),
sanitizeCSVValue(client.first_name),
sanitizeCSVValue(client.last_name),
sanitizeCSVValue(client.phone),
sanitizeCSVValue(client.company),
sanitizeCSVValue(client.job_title),
sanitizeCSVValue(address.address_line_1),
sanitizeCSVValue(address.address_line_2),
sanitizeCSVValue(address.city),
sanitizeCSVValue(address.postal_code),
sanitizeCSVValue(address.state),
sanitizeCSVValue(address.country),
sanitizeCSVValue(client.notes ? client.notes.replace(/\n/g, ' ') : ''),
sanitizeCSVValue(client.avatar_color),
sanitizeCSVValue(client.created_at),
client.sample ? 'true' : 'false',
sanitizeCSVValue(client.type),
sanitizeCSVValue(client.type_formatted)
];
// Wrap all values in quotes
const quotedValues = rowValues.map(val => `"${val}"`);
csvContent += quotedValues.join(',') + '\n';
});
return csvContent;
}
// Download CSV file
function downloadCSV(csv, filename) {
// Create a blob with the CSV data
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
// Create a download link
const link = document.createElement('a');
// If it's supported, use the Blob URL
if (navigator.msSaveBlob) { // IE10+
navigator.msSaveBlob(blob, filename);
} else {
// Create a blob URL
const url = URL.createObjectURL(blob);
// Set up the download link
link.href = url;
link.download = filename;
// Append to the body, click it, and remove it
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the URL
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
}
console.log(`CSV file "${filename}" has been created and downloaded.`);
}
// Wait for page to load and add button
setTimeout(addExportButton, 1500);
// Setup MutationObserver to detect page changes (for single-page applications)
const observer = new MutationObserver(() => {
setTimeout(addExportButton, 1000);
});
// Start observing
observer.observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment