Skip to content

Instantly share code, notes, and snippets.

@boringparty
Created October 18, 2025 07:04
Show Gist options
  • Select an option

  • Save boringparty/f7845cea715852adbbda2756f989ce4f to your computer and use it in GitHub Desktop.

Select an option

Save boringparty/f7845cea715852adbbda2756f989ce4f to your computer and use it in GitHub Desktop.
GoodReads clear off the 'To Read' list...
// ==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