I want a quick backup of my own Teachers Pay Teachers store: the descriptions, listing details and thumbnails of every listing (not the product files or previews). I am logged in to my TPT seller account in this browser. Please follow these instructions exactly. ## Safety rules (most important) - This is a read-only job. Do not change anything in my store. - Never click Save, Update, Submit, Publish, Delete, Remove, Deactivate, Revise, or anything similar. - Never tick or untick a checkbox, never type in a field, never press Enter on a form. - Only visit teacherspayteachers.com: My Products (https://www.teacherspayteachers.com/My-Products), plus further pages of My Products if there are any. - If a popup, dialog, or sign-in screen appears, stop and tell me. Do not click it away. - If the same step fails twice, stop and tell me what happened. ## Step 1: Install the backup kit Go to https://www.teacherspayteachers.com/My-Products. Then run the code block at the end of these instructions, exactly as written, on that page using your JavaScript tool. It should answer "Backup kit installed". The kit only reads pages and starts a download. It is saved in this browser for teacherspayteachers.com only. ## Step 2: Make the list of listings On My Products, run: ```js eval(localStorage.tptKit); await tpt.list() ``` It tells you how many listings it found, and `morePages` says whether My Products seems to have more pages. If it does, open each further page (following page links is allowed), run the same line there, and then go back to the first page of My Products. Then tell me the total. ## Step 3: Save everything Stay on My Products and run this line: ```js eval(localStorage.tptKit); await tpt.quick() ``` Each run takes up to about half a minute and saves the next group of listings. Repeat the same line until the result says `finished: true`. If your tool reports that the call timed out, that is fine: the kit keeps working in the background. Just run the same line again; it waits for the earlier run and carries on. Do not open any other pages in between: the kit reads everything in the background. After each run, give me a one-line update ("28 of 46 done"). When it is finished, the kit downloads one zip file (for very large stores, a few parts). Chrome may ask to allow the download: if so, tell me so I can click Allow. ## Step 4: Finish Tell me: - the name of the zip file (it is in my Downloads folder) - how many listings it saved (`done` of `total`) - every warning, or "no warnings" Then remind me to unzip the file (on Windows: right-click it and choose Extract All; on a Mac: double-click it). ## The backup kit (run in Step 1) ```js // TPT store backup kit. Read-only: it reads pages and starts downloads, it never clicks or types in TPT's forms. // Install once (run this whole block on any teacherspayteachers.com page). It is saved in this browser for TPT only. // Then: on My Products: eval(localStorage.tptKit); await tpt.list() // full backup, on an Edit page: eval(localStorage.tptKit); await tpt.backup() // quick backup, on My Products: eval(localStorage.tptKit); await tpt.quick() (repeat until finished) const kit = () => { const Q = 'tptQueue', D = 'tptDone'; const load = k => JSON.parse(localStorage.getItem(k) || (k === Q ? '[]' : '{}')); const store = (k, v) => localStorage.setItem(k, JSON.stringify(v)); const wait = ms => new Promise(r => setTimeout(r, ms)); const enc = new TextEncoder(), txt = s => enc.encode('' + s); const save = (href, name) => { const a = document.createElement('a'); a.href = href; a.download = name || ''; document.body.appendChild(a); a.click(); a.remove(); }; const crcT = new Uint32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; }); const crc32 = u => { let c = ~0; for (let i = 0; i < u.length; i++) c = crcT[(c ^ u[i]) & 255] ^ (c >>> 8); return ~c >>> 0; }; const makeZip = files => { // store-only zip, enough for text files and images const parts = [], cen = []; let off = 0; for (const f of files) { const name = enc.encode(f.name), d = f.data, c = crc32(d); const h = new DataView(new ArrayBuffer(30)); h.setUint32(0, 0x04034b50, true); h.setUint16(4, 20, true); h.setUint16(6, 0x0800, true); h.setUint32(14, c, true); h.setUint32(18, d.length, true); h.setUint32(22, d.length, true); h.setUint16(26, name.length, true); parts.push(h, name, d); const e = new DataView(new ArrayBuffer(46)); e.setUint32(0, 0x02014b50, true); e.setUint16(4, 20, true); e.setUint16(6, 20, true); e.setUint16(8, 0x0800, true); e.setUint32(16, c, true); e.setUint32(20, d.length, true); e.setUint32(24, d.length, true); e.setUint16(28, name.length, true); e.setUint32(42, off, true); cen.push(e, name); off += 30 + name.length + d.length; } const cs = cen.reduce((s, x) => s + x.byteLength, 0), end = new DataView(new ArrayBuffer(22)); end.setUint32(0, 0x06054b50, true); end.setUint16(8, files.length, true); end.setUint16(10, files.length, true); end.setUint32(12, cs, true); end.setUint32(16, off, true); return new Blob([...parts, ...cen, end], { type: 'application/zip' }); }; const htmlToText = h => h.replace(//gi, '\n').replace(/<\/(p|li|h\d|div)>/gi, '\n').replace(/]*>/gi, '- ') .replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n{3,}/g, '\n\n').trim(); const nextUrl = () => { const done = load(D), n = load(Q).find(x => !done[x.id]); return n ? n.url : null; }; const counts = () => { const q = load(Q), done = load(D); return { done: q.filter(x => done[x.id]).length, total: q.length }; }; const kindOf = url => /itemsBundle/.test(url) ? 'bundle' : 'product'; // A request that gets no answer is cut off after 15 seconds instead of waiting forever const fetchT = (url, ms = 15000) => { const c = new AbortController(), t = setTimeout(() => c.abort(), ms); return fetch(url, { signal: c.signal }).finally(() => clearTimeout(t)); }; const timeLimit = (promise, ms) => Promise.race([promise, wait(ms).then(() => null)]); // An Edit page has finished drawing once the editor, a file link (not for bundles) and a thumbnail are there const editReady = (doc, kind) => doc.querySelector('.LexicalEditor__content') && (kind === 'bundle' || doc.querySelector('a[href*="/Download/"]')) && doc.querySelector('img[src*="/thumbitem/"]'); // Everything we need from an Edit page (the current one, or one loaded in the background) const readEdit = (doc, kind) => { const pageText = doc.body.innerText; const fileLinks = [...doc.querySelectorAll('a[href*="/Download/"],a[href*="/Preview/"]')] .map(a => ({ type: a.getAttribute('href').startsWith('/Download/') ? 'product-file' : 'preview', name: a.innerText.trim(), href: a.getAttribute('href') })); let bundleItems = []; if (kind === 'bundle') { const seg = pageText.slice(pageText.indexOf('Choose the resources'), pageText.indexOf('Upload thumbnails')); bundleItems = [...seg.matchAll(/\n(\d+)\n(.+)\n(\$[\d.,]+|FREE)\nRemove/g)].map(x => `${x[1]}. ${x[2]} (${x[3]})`); } const val = sel => doc.querySelector(sel)?.value || ''; // TPT doesn't link the grade boxes to their labels, so read the text around each ticked box const ticked = [...doc.querySelectorAll('input[type=checkbox]:checked')].map(c => ((c.labels && c.labels[0]) || c.closest('label') || c.parentElement)?.innerText.trim() || ''); const chips = sel => { const l = doc.querySelector(sel)?.labels?.[0]; return l ? l.innerText.split('\n').slice(1).map(s => s.trim()).filter(Boolean).join(', ') : ''; }; return { fileLinks, bundleItems, title: val('#ItemName') || (pageText.match(/Edit (?:Product|Bundle): (.+)/) || [])[1] || '', descHtml: doc.querySelector('.LexicalEditor__content')?.innerHTML || '', price: ticked.some(t => /Free Resource/.test(t)) ? 'Free' : val('#item-price'), licensePrice: val('#item-license-price'), grades: ticked.filter(t => /Grade|Kindergarten|Preschool|Education|Not Grade Specific/.test(t)).join(', '), subjects: chips('#subject-areas'), tags: chips('#tags'), format: chips('#formats'), pages: val('#numberOfPagesOrSlides'), images: [...new Set([...doc.querySelectorAll('img')].map(x => x.src.split('?')[0]).filter(s => s.includes('/thumbitem/')))], }; }; // Load an Edit page out of sight and read it, without leaving the current page const readEditInBackground = async (url, kind) => { const f = document.createElement('iframe'); f.style.cssText = 'position:fixed;left:-9999px;top:0;width:1200px;height:900px;'; f.src = url; document.body.appendChild(f); try { for (let i = 0; i < 30 && !(f.contentDocument && editReady(f.contentDocument, kind)); i++) await wait(500); await wait(1500); // TPT ticks the grade boxes a moment after the page appears const d = f.contentDocument; return d && d.querySelector('.LexicalEditor__content') ? readEdit(d, kind) : null; } finally { f.remove(); } }; // Everything we need from the public product page (inactive listings have none: TPT redirects to My Products) const readPublic = async id => { const res = await fetchT('/Product/' + id); if (!/\/Product\//.test(res.url)) return null; const doc = new DOMParser().parseFromString(await res.text(), 'text/html'); const ld = JSON.parse([...doc.querySelectorAll('script[type="application/ld+json"]')].map(s => s.textContent).find(s => s.includes('"Product"')) || '{}'); const hl = [...doc.querySelectorAll('h2,h3')].find(h => h.textContent.trim() === 'Highlights'); const highlights = hl ? [...hl.parentElement.querySelectorAll('span + div')] .filter(d => d.previousElementSibling.tagName === 'SPAN' && !d.previousElementSibling.querySelector('*')) .map(d => [d.previousElementSibling.textContent.trim(), d.textContent.replace(/\s+/g, ' ').trim()]) : []; return { title: doc.querySelector('h1')?.textContent.trim() || '', descHtml: doc.querySelector('.DescriptionLayout__htmlDisplay')?.innerHTML || '', price: ld.offers ? `${ld.offers.price} ${ld.offers.priceCurrency}` : '', released: ld.releaseDate || '', images: ld.image || [], highlights, }; }; // Combine what we read into the listing's files: description, details, thumbnails const buildListing = async (id, kind, edit, pub) => { edit = edit || {}; const title = pub?.title || edit.title || id; const descHtml = pub?.descHtml || edit.descHtml || ''; const highlights = pub ? pub.highlights : [['Grades', edit.grades], ['Subjects', edit.subjects], ['Tags', edit.tags], ['Format', edit.format], ['Pages', edit.pages]].filter(x => x[1]); const images = pub ? pub.images : (edit.images || []); const price = pub?.price || edit.price || ''; const short = title.split(/[|·]/)[0].trim().replace(/[\\/:*?"<>|]/g, '').slice(0, 60).trim() || id; const folder = `${id} - ${short}`; const fileLinks = edit.fileLinks || [], bundleItems = edit.bundleItems || []; const details = [ `Title: ${title}`, `Type: ${kind}${pub ? '' : ' (inactive, no public page)'}`, `Product ID: ${id}`, `URL: https://www.teacherspayteachers.com/Product/${id}`, `Price: ${price}`, ...(edit.licensePrice ? [`Multiple-license price: ${edit.licensePrice}`] : []), `Released: ${pub?.released || ''}`, `Saved: ${new Date().toISOString()}`, '', ...highlights.map(h => `${h[0]}: ${h[1]}`), ...(bundleItems.length ? ['', `Products in this bundle (${bundleItems.length}):`, ...bundleItems] : []), ]; const files = [ { name: 'description.html', data: enc.encode(`${title}

${title}

\n${descHtml}`) }, { name: 'description.txt', data: txt(title + '\n\n' + htmlToText(descHtml)) }, ]; let thumbs = 0; for (const [i, u] of images.entries()) { try { files.push({ name: `thumbnails/thumbnail-${i + 1}.jpg`, data: new Uint8Array(await (await fetchT(u)).arrayBuffer()) }); thumbs++; } catch (e) { } } const warnings = []; if (!descHtml) warnings.push('No description found'); return { id, kind, folder, title, descHtml, price, highlights, isPublic: !!pub, fileLinks, bundleItems, details, files, thumbs, warnings }; }; window.tpt = { // On My Products: add every listing on this page to the to-do list. Run again on each further page, if there are any. async list() { if (!location.pathname.startsWith('/My-Products')) return 'Open My Products first: https://www.teacherspayteachers.com/My-Products'; for (let i = 0; i < 20 && !document.querySelector('a[href*="editNext/"]'); i++) await wait(500); const q = load(Q), known = new Set(q.map(x => x.id)); let added = 0; for (const a of document.querySelectorAll('a[href*="editNext/"]')) { const m = a.getAttribute('href').match(/items(Digital|Bundle)\/editNext\/(\d+)/); if (m && !known.has(m[2])) { q.push({ id: m[2], url: 'https://www.teacherspayteachers.com/' + m[0] }); known.add(m[2]); added++; } } store(Q, q); const pager = [...document.querySelectorAll('a,button')].some(e => /^(next|›|»|\d+)$/i.test((e.innerText || '').trim()) || /next|page/i.test(e.getAttribute('aria-label') || '')); const morePages = pager ? 'maybe: page links found. Open each further page of My Products and run tpt.list() there too' : 'no'; return { addedFromThisPage: added, morePages, ...counts(), nextUrl: nextUrl() }; }, // Full backup, on an Edit page: save this listing and its files, then report where to go next. async backup() { const m = location.pathname.match(/items(Digital|Bundle)\/editNext\/(\d+)/); if (!m) return 'Not on an Edit page. Go to nextUrl from the last result.'; const kind = m[1] === 'Bundle' ? 'bundle' : 'product', id = m[2]; for (let i = 0; i < 30 && !editReady(document, kind); i++) await wait(500); await wait(1500); // TPT ticks the grade boxes a moment after the page appears const edit = readEdit(document, kind); const l = await buildListing(id, kind, edit, await readPublic(id)); if (l.fileLinks.length) l.details.push('', 'Files downloaded separately:', ...l.fileLinks.map(f => `${f.type}: ${f.name}`)); save(URL.createObjectURL(makeZip([...l.files, { name: 'details.txt', data: txt(l.details.join('\n') + '\n') }])), `${l.folder} - listing.zip`); for (const f of l.fileLinks) { await wait(1500); save(f.href); } await wait(1500); const done = load(D); done[id] = new Date().toISOString(); store(D, done); if (kind === 'product' && !l.fileLinks.some(f => f.type === 'product-file')) l.warnings.push('No product file found on the Edit page'); return { saved: l.folder, thumbnails: l.thumbs, files: l.fileLinks.map(f => f.type), bundleItems: l.bundleItems.length || undefined, warnings: l.warnings, ...counts(), nextUrl: nextUrl() }; }, // Quick backup, on My Products: descriptions, details and thumbnails of the next listings, without opening any page. // Run it again until finished is true; then everything downloads as one zip with a spreadsheet of all listings. // Each run stops starting new listings after about 12 seconds, so it answers well within the browser tool's time limit. async quick() { // An earlier run that timed out may still be finishing: wait for it, but never more than a minute in total while (window.__tptQuickBusy && Date.now() - window.__tptQuickBusy < 60000) await wait(500); window.__tptQuickBusy = Date.now(); try { return await this.quickRun(); } finally { window.__tptQuickBusy = 0; } }, async quickRun() { const q = load(Q), started = Date.now(); if (!q.length) return 'The to-do list is empty. Run tpt.list() on My Products first.'; const s = window.__tptQuick = window.__tptQuick || { listings: [], warnings: [], tries: {} }; const has = id => s.listings.some(l => l.id === id); for (const item of q.filter(x => !has(x.id))) { if (Date.now() - started > 12000) break; const kind = kindOf(item.url); try { const pub = await readPublic(item.id); const edit = (!pub || kind === 'bundle') ? await timeLimit(readEditInBackground(item.url, kind), 20000) : null; const l = await buildListing(item.id, kind, edit, pub); if (has(item.id)) continue; // a stuck earlier run got there first l.warnings.forEach(w => s.warnings.push(`${l.folder}: ${w}`)); s.listings.push(l); } catch (e) { // Try a listing twice; after that, note it and move on so the backup always finishes s.tries[item.id] = (s.tries[item.id] || 0) + 1; if (s.tries[item.id] >= 2) { s.warnings.push(`${item.id}: could not be read (${e.message}). Run the quick backup again later for this listing.`); s.listings.push({ id: item.id, kind, folder: `${item.id} - could not be read`, title: '', descHtml: '', price: '', highlights: [], isPublic: false, bundleItems: [], details: [`Product ID: ${item.id}`, 'This listing could not be read during the backup.'], files: [] }); } } await wait(300); } const finished = s.listings.length >= q.length; if (!finished) return { done: s.listings.length, total: q.length, finished, warnings: s.warnings }; // Spreadsheet of all listings, then one zip (split into parts for very large stores) const cell = v => '"' + String(v ?? '').replace(/"/g, '""') + '"'; const hl = (l, k) => (l.highlights.find(h => h[0] === k) || [])[1] || ''; const rows = [['Product ID', 'Title', 'Type', 'Active', 'Price', 'Grades', 'Subjects', 'Tags', 'Pages', 'Products in bundle', 'Link', 'Description']] .concat(s.listings.map(l => [l.id, l.title, l.kind, l.isPublic ? 'yes' : 'no', l.price, hl(l, 'Grades'), hl(l, 'Subjects'), hl(l, 'Tags'), hl(l, 'Pages'), l.bundleItems.join('; '), `https://www.teacherspayteachers.com/Product/${l.id}`, htmlToText(l.descHtml)])); const csv = { name: 'All listings.csv', data: txt(rows.map(r => r.map(cell).join(',')).join('\r\n')) }; const day = new Date().toLocaleDateString('sv'), size = 250, parts = Math.ceil(s.listings.length / size), saved = []; for (let p = 0; p < parts; p++) { const files = p === 0 ? [csv] : []; for (const l of s.listings.slice(p * size, (p + 1) * size)) { files.push(...l.files.map(f => ({ name: `${l.folder}/${f.name}`, data: f.data })), { name: `${l.folder}/details.txt`, data: txt(l.details.join('\n') + '\n') }); } const name = `TPT Store Backup (quick) ${day}${parts > 1 ? ` part ${p + 1} of ${parts}` : ''}.zip`; save(URL.createObjectURL(makeZip(files)), name); saved.push(name); await wait(1500); } delete window.__tptQuick; return { done: s.listings.length, total: q.length, finished, saved, warnings: s.warnings }; }, status() { return { ...counts(), nextUrl: nextUrl() }; }, // Start over: forget the to-do list and what is done reset() { localStorage.removeItem(Q); localStorage.removeItem(D); delete window.__tptQuick; return 'Cleared'; }, // Remove the kit and its lists from this browser uninstall() { ['tptKit', Q, D].forEach(k => localStorage.removeItem(k)); delete window.tpt; delete window.__tptQuick; return 'Removed'; }, }; }; localStorage.tptKit = '(' + kit.toString() + ')()'; eval(localStorage.tptKit); 'Backup kit installed'; ```