Created
October 18, 2025 07:04
-
-
Save boringparty/f7845cea715852adbbda2756f989ce4f to your computer and use it in GitHub Desktop.
GoodReads clear off the 'To Read' list...
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 Goodreads: Remove all To-Read | |
| // @namespace https://www.goodreads.com/review/list/123456789-name | |
| // @version 1.0 | |
| // @description Adds a button to remove every book on your Goodreads "to-read" shelf shown on the page (use per_page=infinite). Use with caution — deletes are permanent. | |
| // @match https://www.goodreads.com/*review/list* | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // only show on to-read shelf pages | |
| if (!location.search.includes('shelf=to-read')) return; | |
| const makeButton = () => { | |
| const btn = document.createElement('button'); | |
| btn.textContent = 'Remove all To-Read'; | |
| Object.assign(btn.style, { | |
| position: 'fixed', right: '16px', bottom: '16px', | |
| zIndex: 9999, padding: '10px 14px', background: '#c0392b', color: '#fff', | |
| border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '13px', | |
| boxShadow: '0 3px 8px rgba(0,0,0,0.2)' | |
| }); | |
| return btn; | |
| }; | |
| const overlayLog = (() => { | |
| const el = document.createElement('div'); | |
| Object.assign(el.style, { | |
| position: 'fixed', right: '16px', bottom: '64px', zIndex: 9999, | |
| maxWidth: '360px', maxHeight: '40vh', overflow: 'auto', | |
| background: 'rgba(0,0,0,0.8)', color: '#fff', padding: '8px 10px', | |
| borderRadius: '6px', fontSize: '12px', lineHeight: '1.3' | |
| }); | |
| document.body.appendChild(el); | |
| return { | |
| log: (s) => { el.insertAdjacentHTML('beforeend', `<div>${s}</div>`); el.scrollTop = el.scrollHeight; }, | |
| clear: () => { el.innerHTML = ''; } | |
| }; | |
| })(); | |
| const getCsrfToken = () => { | |
| const m = document.querySelector('meta[name="csrf-token"]'); | |
| return m ? m.content : null; | |
| }; | |
| const absUrl = (u) => { | |
| const a = document.createElement('a'); a.href = u; return a.href; | |
| }; | |
| const collectDeleteLinks = () => { | |
| // find links with class deleteLink inside the review list | |
| return Array.from(document.querySelectorAll('a.deleteLink')) | |
| .filter(a => a.href && a.getAttribute('data-method')); // ensure they are the destroy-style links | |
| }; | |
| const sleep = (ms) => new Promise(r => setTimeout(r, ms)); | |
| const runRemoval = async (links, opts = {}) => { | |
| const token = getCsrfToken(); | |
| if (!token) { | |
| alert('No CSRF token found on page — aborting.'); | |
| return; | |
| } | |
| overlayLog.clear(); | |
| overlayLog.log(`Found ${links.length} entries. Starting removals...`); | |
| let i = 0; | |
| for (const link of links) { | |
| i++; | |
| const url = absUrl(link.getAttribute('href')); | |
| overlayLog.log(`(${i}/${links.length}) Removing: ${url}`); | |
| try { | |
| // Use POST with CSRF header; body left empty | |
| const resp = await fetch(url, { | |
| method: 'POST', | |
| credentials: 'same-origin', | |
| headers: { | |
| 'X-CSRF-Token': token, | |
| 'Accept': 'text/javascript, text/html, application/json, */*', | |
| 'X-Requested-With': 'XMLHttpRequest' | |
| }, | |
| // include same-origin cookie, body not required for Rails destroy when using correct endpoint | |
| }); | |
| if (!resp.ok) { | |
| overlayLog.log(`→ HTTP ${resp.status} (failed)`); | |
| } else { | |
| overlayLog.log('→ removed'); | |
| // Optionally remove the row from DOM for visual feedback | |
| const row = link.closest('tr') || link.closest('.bookalike'); // try common containers | |
| if (row) row.remove(); | |
| } | |
| } catch (e) { | |
| overlayLog.log(`→ error: ${e.message}`); | |
| } | |
| // polite delay to avoid hammering server | |
| await sleep(opts.delayMs ?? 800); | |
| } | |
| overlayLog.log('Done.'); | |
| alert('Finished attempting removals. Check shelf to confirm.'); | |
| }; | |
| const btn = makeButton(); | |
| document.body.appendChild(btn); | |
| btn.addEventListener('click', async () => { | |
| const links = collectDeleteLinks(); | |
| if (links.length === 0) { | |
| alert('No removable items found on this page. Make sure you are on your to-read shelf and that per_page=infinite is set.'); | |
| return; | |
| } | |
| const confirmMsg = `This will permanently remove ${links.length} book(s) from your To-Read shelf shown on this page. Are you sure?`; | |
| if (!confirm(confirmMsg)) return; | |
| btn.disabled = true; | |
| btn.textContent = 'Removing…'; | |
| await runRemoval(links, { delayMs: 800 }); | |
| btn.disabled = false; | |
| btn.textContent = 'Remove all To-Read'; | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment