Created
March 10, 2025 17:24
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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