Skip to content

Instantly share code, notes, and snippets.

@lfhbento
Forked from spf13/script.js
Last active August 24, 2025 16:46
Show Gist options
  • Save lfhbento/3388607475edc23a571e8eaf568469e3 to your computer and use it in GitHub Desktop.
Save lfhbento/3388607475edc23a571e8eaf568469e3 to your computer and use it in GitHub Desktop.
Download all your Kindle books before Feb 26, 2025
// ==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);
})();
@btate8
Copy link

btate8 commented Feb 23, 2025

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

@lfhbento
Copy link
Author

@tagoofy @btate8 I've modified the script to allow you to select the device.

Change const DEVICE = 1; on line 25 to whatever device you want to select and then run the script

@hiroo916
Copy link

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

@Mirah444
Copy link

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!

@knittingmommy
Copy link

knittingmommy commented Feb 23, 2025

@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.

@SpikeTheLobster
Copy link

SpikeTheLobster commented Feb 23, 2025

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. :(

@coopy
Copy link

coopy commented Feb 23, 2025

@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?

@nacht
Copy link

nacht commented Feb 23, 2025

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>

@CharlieGB
Copy link

CharlieGB commented Feb 23, 2025

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.

@TysonCodes
Copy link

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
Copy link

@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.

@SpikeTheLobster
Copy link

SpikeTheLobster commented Feb 23, 2025

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/*

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)

@JohanKlos
Copy link

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

@tir38
Copy link

tir38 commented Feb 23, 2025

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.

@CJoshuaV
Copy link

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.

@crasher35
Copy link

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!

@Cassieh1111
Copy link

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

@hkeach
Copy link

hkeach commented Feb 24, 2025

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.

@wilssearch
Copy link

Thank you for doing this! I have 12520 books to download. I didn't think I could get very many downloaded without your help!

@bookdragon9
Copy link

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 :)

@wilssearch
Copy link

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
})();

@wilssearch
Copy link

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.

@asakins
Copy link

asakins commented Feb 24, 2025

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?

@hkeach
Copy link

hkeach commented Feb 24, 2025 via email

@403-html
Copy link

403-html commented Feb 24, 2025

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.

@cindy1874
Copy link

I keep getting an "invalid userscript" message when trying to paste any of these codes on Tampermonkey

@Ldcgreen
Copy link

The OP script worked like a charm! I had 5500 books on 221 pages.

Thanks for sharing this script! It is a life-saver!

@cindy1874
Copy link

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
})();

@asakins
Copy link

asakins commented Feb 24, 2025

This happens when you don't have a physical kindle device registered to your account. I registered an old one and it started working

Thanks for the insight. Argh... I was hoping that wasn't the answer. I had a kindle years ago, rarely used it, and got rid of it.

@Ldcgreen
Copy link

@lfhbento Thanks for outstanding code! It worked and saved me so much time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment