Skip to content

Instantly share code, notes, and snippets.

@bmakuh
Created February 16, 2025 21:02
Show Gist options
  • Save bmakuh/4a63e1a84a63967d4de23017b7968a75 to your computer and use it in GitHub Desktop.
Save bmakuh/4a63e1a84a63967d4de23017b7968a75 to your computer and use it in GitHub Desktop.
Kindle book auto-downloader
/**
* TO USE:
* 1. Save this file
* 1. Add your email and password in the variables below
* 2. Run `npm install @playwright/test`
* 3. Run `npx playwright test --ui index.spec.ts`
*/
import { test, expect, chromium } from "@playwright/test";
const EMAIL = '[email protected]'
const PASSWORD = 'your amazon password'
const PATH = '/Users/YOUR_USERNAME_HERE/working/amz-kindle-downloader/downloads/'
test("Kindle book auto-downloader", async () => {
// Try to avoid presenting as an automated browser
const browser = await chromium.launch({
headless: false,
args: ["--disable-blink-features=AutomationControlled"],
});
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
await page.goto(
"https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc"
);
await page.getByRole("textbox", { name: "Email or mobile phone number" }).click({delay: 100});
await page
.getByRole("textbox", { name: "Email or mobile phone number" })
.fill(EMAIL);
await page.getByRole("textbox", { name: "Password" }).click();
await page
.getByRole("textbox", { name: "Password" })
.fill(PASSWORD);
await page.waitForTimeout(100)
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForLoadState("networkidle");
/**
* Added a pause here in case Amazon requires you to verify that you're not a robot.
* If you have to do so, type the prompt manually, then resume the runner.
*/
await page.pause()
// Update this to be the number of pages in your content library. Yeah it's janky, but this script is free. Deal with it
const pages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
for await (let i of pages) {
await page.goto(
`https://www.amazon.com/hz/mycd/digital-console/contentlist/booksPurchases/dateDsc?pageNumber=${i}`
);
await page.waitForTimeout(1500)
const moreActionsButtons = await page
.getByText("More actions")
.all();
expect(moreActionsButtons.length).toBe(25)
for await (const button of moreActionsButtons) {
await page.mouse.wheel(0, 192.5)
// Click more action button
await button.click();
const downloadButton = page
.locator("[style='visibility: visible;'] [id*=DOWNLOAD_AND_TRANSFER_ACTION]")
.getByText("Download & transfer via USB")
await downloadButton.scrollIntoViewIfNeeded()
expect(downloadButton).toBeVisible()
// Click the download & transfer button
await downloadButton.click();
const invalidText = await page
.locator(
"[class*=DeviceDialogBox-module_container].DeviceDialogBox-module_container__1WOqR .DeviceDialogBox-module_message_container__175KJ > div"
)
.textContent();
// If you can't download this one, just move on
if (invalidText?.includes("You do not")) {
await page
.locator(
"[class*=DeviceDialogBox-module_container].DeviceDialogBox-module_container__1WOqR [id*=DOWNLOAD_AND_TRANSFER_ACTION]"
)
.getByText("Cancel")
.click();
continue;
}
// Click through the modal
await page
.locator(
"[class*=DeviceDialogBox-module_container].DeviceDialogBox-module_container__1WOqR [id*=download_and_transfer_list] span[class*=RadioButton-module_radio]"
)
.click();
const downloadPromise = page.waitForEvent("download");
await page
.locator(
"[class*=DeviceDialogBox-module_container].DeviceDialogBox-module_container__1WOqR [id*=DOWNLOAD_AND_TRANSFER_ACTION_]:last-of-type"
)
.getByText("Download")
.click();
const download = await downloadPromise;
await download.saveAs(`${PATH}${download.suggestedFilename()}`);
await page.locator("#notification-close").click();
}
}
await browser.close()
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment