-
-
Save lfhbento/3388607475edc23a571e8eaf568469e3 to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Kindle Download | |
// @namespace http://tampermonkey.net/ | |
// @version 2025-02-20 | |
// @description Download all your kindle books | |
// @author You | |
// @match https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=amazon.com | |
// @grant none | |
// ==/UserScript== | |
// 1. Log in to your Amazon account | |
// 2. Go to your Content Library > Books - https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc/ | |
(async function () { | |
// Close the notification if it appears | |
function closeNotification() { | |
const notifClose = document.querySelector('span#notification-close'); | |
if (notifClose) { | |
notifClose.click(); | |
} | |
} | |
// Change to whatever device you want to select. 1 = first device, 2 = second device, etc | |
const DEVICE = 1; | |
// Pause for a given duration (in milliseconds) | |
function pause(duration = 1000) { | |
return new Promise((resolve) => setTimeout(resolve, duration)); | |
} | |
await pause(5000); | |
const allPages = Array.from(document.querySelectorAll('a.page-item')); | |
const lastPage = allPages[allPages.length - 1]; | |
const lastPageNumber = lastPage ? parseInt(lastPage.innerText, 10) : 1; | |
let currentPage = document.querySelector('a.page-item.active'); | |
let currentPageNumber = parseInt(currentPage.innerText, 10); | |
do { | |
await pause(5000); | |
currentPage = document.querySelector('a.page-item.active'); | |
currentPageNumber = parseInt(currentPage.innerText, 10); | |
console.log(`downloading page ${currentPageNumber} of ${lastPageNumber}`); | |
// They removed the id, so we have to use the class name now | |
// It's a bit more brittle but it should do. | |
const menus = Array.from(document.querySelectorAll('div[class*="Dropdown-module_dropdown_container"]')) | |
.map((container) => | |
Array.from(container.children).find( | |
(child) => child.innerHTML.indexOf('DOWNLOAD_AND_TRANSFER_DIALOG') !== -1, | |
), | |
) | |
.filter((item) => !!item); | |
for (let menu of menus) { | |
// Extract the ASIN from the menu's id. | |
// E.g. "DOWNLOAD_AND_TRANSFER_ACTION_B07HYK662L" -> "B07HYK662L" | |
const dialog = menu.querySelector(`div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_']`); | |
if (!dialog) { | |
console.warn(`No dialog found for menu`); | |
continue; | |
} | |
const parts = dialog.id.split('_'); | |
const asin = parts[parts.length - 1]; | |
console.log(`Processing book with ASIN: ${asin}`); | |
// Click the menu to open the dialog | |
menu.click(); | |
const menuItem = Array.from(menu.childNodes).find((node) => node.querySelector(`div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_']`)); | |
menuItem.click(); | |
await pause(500); | |
// Within the dialog, select the first radio button (device) to download. | |
// This selector targets the list for this ASIN. | |
const inputSelector = `ul#download_and_transfer_list_${asin} li[class^='ActionList-module_action_list_item__'] > div > label`; | |
const inputList = Array.from(menu.querySelectorAll(inputSelector)); | |
console.log(inputList.length); | |
if (!inputList) { | |
console.warn(`No download option found for ASIN ${asin}`); | |
continue; | |
} | |
const deviceToCheck = inputList.length >= DEVICE ? DEVICE - 1 : 0; | |
const input = inputList[deviceToCheck]; | |
if (!input) { | |
console.log(`No download option found for ASIN ${asin}`); | |
continue; | |
} | |
input.click(); | |
await pause(500); | |
// Find the confirm button within the dialog for this ASIN. | |
const buttonSelector = `div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_${asin}'] div[class^='DeviceDialogBox-module_button_container__'] > div[id$='_CONFIRM']`; | |
const button = document.querySelector(buttonSelector); | |
if (!button) { | |
console.warn(`No confirm button found for ASIN ${asin}`); | |
continue; | |
} | |
button.click(); | |
await pause(1000); | |
closeNotification(); | |
await pause(500); | |
} | |
if (currentPage) { | |
const nextPage = currentPage.nextElementSibling; | |
if (nextPage) { | |
nextPage.click(); | |
} | |
} | |
} while (currentPageNumber < lastPageNumber); | |
})(); |
FYI, worked for me on Chrome. There was a message early asking me to confirm it was ok to do multiple downloads from Amazon. Maybe I missed that message on Edge and that was why the download never appeared in my download folder.
Still would like to know how to have the script select a Kindle in the list that is not the first one (3rd one in my case).
Thanks,
Todd
I am also trying to figure out how to get a device besides the 1st device listed to work (2nd in my case).
I have another script I can use that does 1 page at a time for a 2nd device, but with hundreds of books, I would love to have this script work with the 2nd listed device!
If anyone figures it out, please share.
Thanks,
BT
in case this helps somebody else. The expired library books in my collection were interfering with the script so I switched the view to Purchases only. And to start on another page, I changed the activation URL at the beginning of the script to the second page of the Purchases URL. Then reloaded on that page and it started downloading.
example:
// @match https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc?pageNumber=2
Unfortunately we were never able to locate the <div id="pagination" class="pagination" under Inspect.
Another user suggested downloading books by Collections. Because all of the Collections are less than 10000 books the 400 page limit would not be a problem.
My issue now is that the script starts downloads immediately and there is not time to navigate to the Collections page to open a Collection. Despite clicking with reckless abandon, I've only been successful in downloading a single 200 book Collection.
Is there a way to slow down or control the initiation of the automatic downloads? If not can this script map to the Collections page instead of all books?
Thanks for all the help!
@lfhbento So, I'm now on page 400 and the script just stopped downloading. Tampermonkey shows a red number 1 and the download script is there, but there's no way to trigger it. I looked at the inspect element and as @therenavigatedlife stated, the pagination isn't there anymore. This is what is there for the previous and next buttons if this helps:
<div id="PREVIOUS_INFINTE_LOADER_ID" class="action_button" tabindex="0" style="display: inline-block; border-radius: 3px; padding: 0px 10px; margin-bottom: 10px; min-height: 1.8rem; border-width: 1px; border-style: solid; border-color: rgb(173, 177, 184) rgb(162, 166, 172) rgb(141, 144, 150); border-image: initial; cursor: pointer; background: linear-gradient(rgb(247, 248, 250), rgb(231, 233, 236)); word-break: break-word; outline: none;"><div style="vertical-align: middle; padding-top: 0.5em; padding-bottom: 0.5em; font-size: 13px;"><span>Previous Page</span></div></div>
<div id="NEXT_INFINTE_LOADER_ID" class="action_button" tabindex="0" style="display: inline-block; border-radius: 3px; padding: 0px 10px; margin-bottom: 10px; min-height: 1.8rem; border-width: 1px; border-style: solid; border-color: rgb(173, 177, 184) rgb(162, 166, 172) rgb(141, 144, 150); border-image: initial; cursor: pointer; background: linear-gradient(rgb(247, 248, 250), rgb(231, 233, 236)); word-break: break-word; outline: none;"><div style="vertical-align: middle; padding-top: 0.5em; padding-bottom: 0.5em; font-size: 13px;">
<span>Next Page</span></div></div>
<span>Next Page</span>
I hope that helps. For the moment I'm not touching that browser tab. However, I did open a new tab and was able to have enough time to quickly sort books by purchased and oldest to newest so that my oldest books were now on page 1 and the script started running and downloading my books going in the other direction now. I'll figure out what's missing in the middle when get there.
I have a monstrous number of titles on Kindle (just under 15,000), so I ran into the difficulty of the script stopping at page 400 (9.975th to 10,000th). I figured I'd jury-rig/workaround simply by switching from dateDsc to dateAsc (in the #match on line 7), as @knittingmommy did in the previous comment...
Unfortunately, I'm now getting "Invalid access token" when the script runs on the first item on page 1 (ascending). Tried removing cookies and relogging, in case that'd help, but no joy. Switched browser (from Chrome to Edge), same error. :(
@SpikeTheLobster Just checking, I’m guessing this was exactly what you did: after you switched the sort order in the #match URL, did you also change the actual URL you’re visiting to the same?
Anyone else getting an XML error message?
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<error>
<div id="in-page-channel-node-id" data-channel-name="in_page_channel_QSMWKf"/>
<message>Invalid access token</message>
</error>
I am getting the same error. I thought I had things working earlier today, but it was downloading for my Fire device which meant I couldn't get it to remove the drm. Hence I changed the device.
Just worked for me on amazon.ca but note that I had to remove the final / for the matching pattern.
https://www.amazon.ca/hz/mycd/digital-console/contentlist/booksPurchases*
not
https://www.amazon.ca/hz/mycd/digital-console/contentlist/booksPurchases/*
@SpikeTheLobster Just checking, I’m guessing this was exactly what you did: after you switched the sort order in the #match URL, did you also change the actual URL you’re visiting to the same?
Yes. Easy to make that mistake, but otherwise the script wouldn't run.
Just worked for me on amazon.ca but note that I had to remove the final / for the matching pattern.
https://www.amazon.ca/hz/mycd/digital-console/contentlist/booksPurchases*
nothttps://www.amazon.ca/hz/mycd/digital-console/contentlist/booksPurchases/*
Oh, you clever, clever person, you. Zero idea why taking the last / off works, but mine's running again (on .co.uk) with that change. THANKS!
(Using amazon.co.uk/hz/mycd/digital-console/contentlist/booksAll/dateAsc* on line 7)
Great script, thanks! I have more than 1000 books, so I needed some automation to go to the next page. Find my fork of your script here: https://gist.github.com/JohanKlos/3a92f6a55b8f1a8e712ce3c4b1510dd3
For some reason @403-html 's update https://gist.github.com/lfhbento/3388607475edc23a571e8eaf568469e3?permalink_comment_id=5449612#gistcomment-5449612 was not navigating to the next page and just re-downloading the same 25 books. Switching to OP's script fixed it.
Is there an easy way to tweak this to batch download Comixology content? I tried manually filtering for it, but the script no longer worked.
This worked really well. I did have to register my husband's old kindle to my account though, otherwise, Amazon wasn't letting me download it at all. Not even manually. But once I did that, this script really works well.
For anyone following that YouTube video, this script doesn't have a green download button anymore. It just starts downloading on it's own. It also skips to the next page on its own now too. Really nice!
can you make a script so I can use on other sites such as ibookpile.in and https://oceanofpdf.com/ under the authors so I can get all the authors books in 1 go thank you hope you can help me
I have 6 registered devices but when I try do download manually I get the "you don't have any compatible devices" message. The script seems to be running without error as it pages to the end of 300 books but they're not in my download directlory and it completed in only about 90 seconds.
Edit: I checked the console and this is what I see over and over:
Processing book with ASIN: B0042XA2Y0
userscript.html?name=Kindle-Download.user.js&id=1af2cd89-b9fd-4f18-91fc-b92fe022083a:83 0
userscript.html?name=Kindle-Download.user.js&id=1af2cd89-b9fd-4f18-91fc-b92fe022083a:94 No download option found for ASIN B0042XA2Y0
Edit#2: I don't have a real kindle registered to my account, only apps on ios devices. I'll try registering an old one.
Thank you for doing this! I have 12520 books to download. I didn't think I could get very many downloaded without your help!
I'm sure someone must have said this before but you can actually get at the last 400 pages by telling it to sort in ascending date order. So the black hole is only there if you have more than 20,000. I only have 17.5k but my daughter has 28k and having to do the middle manually using the page at a time downloader. She hasn't got this working yet.
I currently have 2 machines going - one forwards and the other backwards.
It is working well although showing errors that it just seems to get around on the win10 machine while the win 11 machine stops dead and needs the urls changing to continue.
As long as the url following match is exactly the same as the url in the browser window it works well.
Thank you so much for providing this script :)
if there's no more than 1 page, then you get
Uncaught (in promise) TypeError: lastPage is undefined
. Easy to fix I think, with checking if pagination is even there, and if not then page number is just 1.Edit: my take on this, works for me
(async function () { // Close the notification if it appears function closeNotification() { const notifClose = document.querySelector("span#notification-close"); if (notifClose) { notifClose.click(); } } // Pause for a given duration (in milliseconds) function pause(duration = 1000) { return new Promise((resolve) => setTimeout(resolve, duration)); } await pause(5000); // #### MAIN CHANGE: different way of tracking pages ##### let currentPageNumber = 1; let lastPageNumber = 1; // Get the last page number. If there's only one page, the pagination elements won't exist. const lastPageElement = document.querySelector("a.page-item:last-of-type"); if (lastPageElement) { lastPageNumber = parseInt(lastPageElement.innerText, 10); } do { await pause(5000); console.log(`Downloading page ${currentPageNumber} of ${lastPageNumber}`); const menus = Array.from( document.querySelectorAll( 'div[class*="Dropdown-module_dropdown_container"]' ) ) .map((container) => Array.from(container.children).find((child) => child.innerHTML && child.innerHTML.indexOf("DOWNLOAD_AND_TRANSFER_DIALOG") !== -1)) .filter((item) => !!item); for (let menu of menus) { const dialog = menu.querySelector(`div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_']`); if (!dialog) { console.warn(`No dialog found for menu element. Skipping.`); continue; } const parts = dialog.id.split("_"); const asin = parts[parts.length - 1]; console.log(`Processing book with ASIN: ${asin}`); menu.click(); await pause(500); const inputSelector = `ul#download_and_transfer_list_${asin} li[class^='ActionList-module_action_list_item__'] > div > label`; const input = document.querySelector(inputSelector); if (!input) { console.warn(`No download option found for ASIN ${asin}`); closeNotification(); // Close the notification even if download option isn't found continue; } input.click(); await pause(500); const buttonSelector = `div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_${asin}'] div[class^='DeviceDialogBox-module_button_container__'] > div[id$='_CONFIRM']`; const button = document.querySelector(buttonSelector); if (!button) { console.warn(`No confirm button found for ASIN ${asin}`); closeNotification(); // Close the notification even if confirm button isn't found continue; } button.click(); await pause(1000); closeNotification(); await pause(500); } if (currentPageNumber < lastPageNumber) { // CHANGE: Check if there are pages, before going further const nextPage = document.querySelector("a.page-item.next:not(.disabled)"); if (nextPage) { nextPage.click(); currentPageNumber++; } } } while (currentPageNumber < lastPageNumber); // CHANGE: Use < instead of <=, so it doesn't re-download stuff })();
I have no clue how to use this. Do I just add this to the original script and if so, where - at the bottom before the ending lines})();? Or do I need to add it in different locations? And if so what line numbers? Thank you for your help as I have over 12,000 books to download and doing this 1 page at a time is driving me nuts!
Thank you in advance.
Real excited when I came across this script but am having problems running it. For every book in my library (5984 items - a lot of Comixology items) I get this message:
[Log] No download option found for ASIN B0C2B2JQ9P
[Log] Processing book with ASIN: B08QW77G1C
[Log] 0
[Log] No download option found for ASIN B08QW77G1C
[Log] Processing book with ASIN: B00OF9SEYG
Has anyone else seen the "No download option found for" message for the books in their library?
I have no clue how to use this. Do I just add this to the original script and if so, where - at the bottom before the ending lines})();? Or do I need to add it in different locations? And if so what line numbers? Thank you for your help as I have over 12,000 books to download and doing this 1 page at a time is driving me nuts! Thank you in advance.
it was to OP so he could for example adjust for people with just one page. For multi-page (more than 1) Amazon's library his script works well. No need to update it.
I keep getting an "invalid userscript" message when trying to paste any of these codes on Tampermonkey
The OP script worked like a charm! I had 5500 books on 221 pages.
Thanks for sharing this script! It is a life-saver!
HELP!! I finally got the batch download code applied to Tampermonkey. Problem is, nothing happens!! It says "no scripts running" and I've refreshed the Amazon page and redownloaded Tampermonkey, all to no avail. I'm not particularly computer literate, so I'm kind of dependent on y'all. Not to mention time is running out for me to download my kindle books. HELP!!! This is the code I am using:
// ==UserScript==
// @name Amazon Kindle Book Downloader
// @namespace http://tampermonkey.net/
// @Version 0.2.1
// @description Adds a button to trigger downloads of all Kindle books on the page
// @author Chris Hollindale
// @match https://www.amazon.com/hz/mycd/digital-console/contentlist/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
(async function () {
// Close the notification if it appears
function closeNotification() {
const notifClose = document.querySelector("span#notification-close");
if (notifClose) {
notifClose.click();
}
}
// Pause for a given duration (in milliseconds)
function pause(duration = 1000) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
await pause(5000);
// #### MAIN CHANGE: different way of tracking pages #####
let currentPageNumber = 1;
let lastPageNumber = 1;
// Get the last page number. If there's only one page, the pagination elements won't exist.
const lastPageElement = document.querySelector("a.page-item:last-of-type");
if (lastPageElement) {
lastPageNumber = parseInt(lastPageElement.innerText, 10);
}
do {
await pause(500);
console.log(`Downloading page ${currentPageNumber} of ${lastPageNumber}`);
const menus = Array.from(
document.querySelectorAll(
'div[class*="Dropdown-module_dropdown_container"]'
)
)
.map((container) => Array.from(container.children).find((child) => child.innerHTML && child.innerHTML.indexOf("DOWNLOAD_AND_TRANSFER_DIALOG") !== -1))
.filter((item) => !!item);
for (let menu of menus) {
const dialog = menu.querySelector(`div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_']`);
if (!dialog) {
console.warn(`No dialog found for menu element. Skipping.`);
continue;
}
const parts = dialog.id.split("_");
const asin = parts[parts.length - 1];
console.log(`Processing book with ASIN: ${asin}`);
menu.click();
await pause(500);
const inputSelector = `ul#download_and_transfer_list_${asin} li[class^='ActionList-module_action_list_item__'] > div > label`;
const input = document.querySelector(inputSelector);
if (!input) {
console.warn(`No download option found for ASIN ${asin}`);
closeNotification(); // Close the notification even if download option isn't found
continue;
}
input.click();
await pause(500);
const buttonSelector = `div[id^='DOWNLOAD_AND_TRANSFER_DIALOG_${asin}'] div[class^='DeviceDialogBox-module_button_container__'] > div[id$='_CONFIRM']`;
const button = document.querySelector(buttonSelector);
if (!button) {
console.warn(`No confirm button found for ASIN ${asin}`);
closeNotification(); // Close the notification even if confirm button isn't found
continue;
}
button.click();
await pause(500);
closeNotification();
await pause(500);
}
if (currentPageNumber < lastPageNumber) { // CHANGE: Check if there are pages, before going further
const nextPage = document.querySelector("a.page-item.next:not(.disabled)");
if (nextPage) {
nextPage.click();
currentPageNumber++;
}
}
} while (currentPageNumber < lastPageNumber); // CHANGE: Use < instead of <=, so it doesn't re-download stuff
})();
The script is up to page 237 now, so I should be able to test what happens at 400 in a few hours. Given what others have said, I'm expecting it to just stop working when it gets to 400.
But until then, I had a thought. Assuming that @therenavigatedlife might be able to go at it from a different way. I was thinking they could start their content over again, only when he gets to the page with the books, click on the part of the page that says sort by and choose: Acquired: Oldest to Newest and see if they can get it started again. Only this time they'd be starting with their oldest books on page 1. I'm not sure what to do about books in the middle that might be missed if they have that many books.
I'm also not sure if that would work. It's just a thought. I might try that myself if I need to when I get to page 400.