-
-
Save JohanKlos/3a92f6a55b8f1a8e712ce3c4b1510dd3 to your computer and use it in GitHub Desktop.
Download all your Kindle books before Feb 26, 2025
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 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