Skip to content

Instantly share code, notes, and snippets.

@JohanKlos
Forked from lfhbento/userscript.js
Last active February 24, 2025 02:47
Show Gist options
  • Save JohanKlos/3a92f6a55b8f1a8e712ce3c4b1510dd3 to your computer and use it in GitHub Desktop.
Save JohanKlos/3a92f6a55b8f1a8e712ce3c4b1510dd3 to your computer and use it in GitHub Desktop.
Download all your Kindle books before Feb 26, 2025
// ==UserScript==
// @name Amazon Kindle Auto-Downloader (Start/Stop Buttons)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Adds Start/Stop buttons to automatically download Kindle books from each page
// @author
// @match https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc?pageNumber=*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(async function() {
'use strict';
//--- Create Start/Stop buttons on the page
addControlButtons();
//--- If user previously clicked "Start" on another page, continue automatically
const isRunning = GM_getValue('isRunning', false);
if (isRunning) {
console.log('[Tampermonkey] isRunning = true from previous page, auto-starting process...');
await processPage();
} else {
console.log('[Tampermonkey] isRunning = false; waiting for user to click "Start Downloads"');
}
})();
//----------------------------------------------------------------------------
// MAIN PAGE PROCESSING
//----------------------------------------------------------------------------
async function processPage() {
// Double-check if user clicked "Stop" before we begin
if (!GM_getValue('isRunning', false)) {
console.log('[Tampermonkey] processPage() aborted: isRunning = false');
return;
}
console.log('[Tampermonkey] processPage() starting...');
const dropdowns = await waitForDropdownsOrTimeout(15000);
if (dropdowns.length === 0) {
console.warn('[Tampermonkey] No dropdowns found on this page. Going to next page...');
// If still running, go next
if (GM_getValue('isRunning', false)) {
goToNextPage();
}
return;
}
console.log(`[Tampermonkey] Found ${dropdowns.length} dropdown(s). Starting download process...`);
await processDropdowns(dropdowns);
// Finished current page. If still running, go next
if (GM_getValue('isRunning', false)) {
await delay(1000);
goToNextPage();
} else {
console.log('[Tampermonkey] Stopped after processing current page.');
}
}
//----------------------------------------------------------------------------
// DROPDOWNS LOGIC
//----------------------------------------------------------------------------
async function processDropdowns(dropdowns) {
for (let i = 0; i < dropdowns.length; i++) {
// Check if user stopped mid-loop
if (!GM_getValue('isRunning', false)) {
console.log(`[Tampermonkey] processDropdowns() aborted on dropdown #${i+1}`);
return;
}
const dropdown = dropdowns[i];
console.log(`[Tampermonkey] Opening dropdown #${i + 1}`);
// 1. Click to open
dropdown.click();
await waitFor('[class^="Dropdown-module_dropdown_container__"]', 10000, dropdown);
// 2. Click "Download & transfer via USB"
const container = dropdown.querySelector('[class^="Dropdown-module_dropdown_container__"]');
if (!container) {
console.warn(`[Tampermonkey] Dropdown #${i+1}: container not found.`);
continue;
}
const usbOptionDiv = Array.from(container.querySelectorAll('div'))
.find(div => div.textContent.includes('Download & transfer via USB'));
if (usbOptionDiv && usbOptionDiv.querySelector('div')) {
usbOptionDiv.querySelector('div').click();
console.log(`[Tampermonkey] Clicked "Download & transfer via USB" in dropdown #${i+1}`);
} else {
console.warn(`[Tampermonkey] "Download & transfer via USB" not found in dropdown #${i+1}`);
continue;
}
await delay(1000);
// 3. Choose first device
const kindleOption = dropdown.querySelector('span[id^="download_and_transfer_list_"]');
if (kindleOption) {
kindleOption.click();
console.log(`[Tampermonkey] Selected Kindle device in dropdown #${i+1}`);
} else {
console.warn(`[Tampermonkey] Could not find device option in dropdown #${i+1}`);
continue;
}
await delay(1000);
// 4. Confirm Download
const confirmButtons = dropdown.querySelectorAll('[id$="_CONFIRM"]');
const downloadBtn = Array.from(confirmButtons).find(btn => btn.textContent.includes('Download'));
if (downloadBtn) {
downloadBtn.click();
console.log(`[Tampermonkey] Clicked "Download" confirm in dropdown #${i+1}`);
} else {
console.warn(`[Tampermonkey] "Download" confirm not found in dropdown #${i+1}`);
continue;
}
// 5. Wait for success notification, close it
try {
const notifClose = await waitFor('span[id="notification-close"]', 8000);
notifClose.click();
console.log(`[Tampermonkey] Closed success notification for dropdown #${i+1}`);
} catch (err) {
console.warn(`[Tampermonkey] No success notification found for dropdown #${i+1}`);
}
await delay(1000);
}
console.log('[Tampermonkey] All dropdowns processed on this page.');
}
//----------------------------------------------------------------------------
// NEXT PAGE
//----------------------------------------------------------------------------
function goToNextPage() {
const currentUrl = new URL(window.location.href);
const currentPage = parseInt(currentUrl.searchParams.get('pageNumber') || '1', 10);
currentUrl.searchParams.set('pageNumber', currentPage + 1);
console.log(`[Tampermonkey] Going to next page: ${currentUrl.toString()}`);
window.location.href = currentUrl.toString();
}
//----------------------------------------------------------------------------
// HELPER: WAIT FOR DROPDOWNS
//----------------------------------------------------------------------------
async function waitForDropdownsOrTimeout(timeout = 15000) {
const start = Date.now();
while (Date.now() - start < timeout) {
// If user clicked Stop while waiting, abort
if (!GM_getValue('isRunning', false)) {
console.log('[Tampermonkey] waitForDropdownsOrTimeout() aborted (stop clicked).');
return [];
}
const dropdowns = document.querySelectorAll('[class^="Dropdown-module_container__"]');
if (dropdowns.length > 0) {
return dropdowns;
}
await delay(500);
}
return [];
}
//----------------------------------------------------------------------------
// HELPER: WAIT FOR SELECTOR
//----------------------------------------------------------------------------
function waitFor(selector, timeout = 10000, container = document) {
return new Promise((resolve, reject) => {
const start = Date.now();
function check() {
// If user stops mid-wait
if (!GM_getValue('isRunning', false)) {
reject(new Error('Stopped by user'));
return;
}
const el = container.querySelector(selector);
if (el) {
resolve(el);
return;
}
if (Date.now() - start >= timeout) {
reject(new Error(`Timeout waiting for ${selector}`));
} else {
setTimeout(check, 250);
}
}
check();
});
}
//----------------------------------------------------------------------------
// HELPER: DELAY
//----------------------------------------------------------------------------
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//----------------------------------------------------------------------------
// ADD START/STOP BUTTONS
//----------------------------------------------------------------------------
function addControlButtons() {
// Container styling
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '8px';
document.body.appendChild(container);
// START button
const startBtn = document.createElement('button');
startBtn.textContent = 'Start Downloads';
startBtn.style.padding = '8px';
startBtn.style.fontSize = '14px';
startBtn.style.cursor = 'pointer';
startBtn.style.backgroundColor = '#4CAF50';
startBtn.style.color = 'white';
startBtn.style.border = 'none';
startBtn.style.borderRadius = '4px';
startBtn.addEventListener('click', async () => {
if (!GM_getValue('isRunning', false)) {
GM_setValue('isRunning', true);
console.log('[Tampermonkey] "Start" clicked; set isRunning = true');
// Immediately start processing the page
await processPage();
} else {
console.log('[Tampermonkey] Already running; "Start" click ignored.');
}
});
container.appendChild(startBtn);
// STOP button
const stopBtn = document.createElement('button');
stopBtn.textContent = 'Stop Downloads';
stopBtn.style.padding = '8px';
stopBtn.style.fontSize = '14px';
stopBtn.style.cursor = 'pointer';
stopBtn.style.backgroundColor = '#f44336';
stopBtn.style.color = 'white';
stopBtn.style.border = 'none';
stopBtn.style.borderRadius = '4px';
stopBtn.addEventListener('click', () => {
if (GM_getValue('isRunning', false)) {
GM_setValue('isRunning', false);
console.log('[Tampermonkey] "Stop" clicked; set isRunning = false');
} else {
console.log('[Tampermonkey] Already stopped; "Stop" click ignored.');
}
});
container.appendChild(stopBtn);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment