Created
February 16, 2025 21:02
-
-
Save bmakuh/4a63e1a84a63967d4de23017b7968a75 to your computer and use it in GitHub Desktop.
Kindle book auto-downloader
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
/** | |
* 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