Skip to content

Instantly share code, notes, and snippets.

@yige233
Last active December 6, 2024 14:30
Show Gist options
  • Save yige233/274fbd29ba26a6f9d75d698c1790e0d5 to your computer and use it in GitHub Desktop.
Save yige233/274fbd29ba26a6f9d75d698c1790e0d5 to your computer and use it in GitHub Desktop.
Kindle 中国 批量下载自己的电子书和个人文档
/**
* 批量下载自己已购买的电子书和个人文档
* 要求:至少有一台Kindle设备。
* 打开 https://www.amazon.cn/hz/mycd/myx/ ,然后按F12键进入Console(控制台),把代码全部复制并粘贴到控制台中,回车。
* 然后输入 download("ebook") ,下载所有的电子书
* 想下载个人文档,则是输入 download("pdoc")
* 下载时如果某个文件下载失败,可以使用刚刚运行的函数(也就是 download() 或者 download("pdoc") )重新开始下载。在网页没被关闭的情况下,程序会忽略已经下载了的文件。
* 脚本运行期间请不要关闭网页,请允许网页自动下载多个文件
* 如果网页被关闭了,但恰巧你保存了上次下载任务返回的成功下载的文件列表,
* 可以选择复制该列表中的所有文字,并将其作为 download 的第二个参数传入(如 download("ebook",["something","something else"]) ),这样程序同样会忽略已经下载了的文件。
*
* 原来通过请求获取下载url的方法只适用于电子书,虽然有意识到下载链接似乎有一定规律,但也没多想,
* 后来看到 https://github.com/yihong0618/Kindle_download_helper 这个项目,发现他是用拼接url而非请求获取url,
* 这样可以获取个人文档的下载链接,于是研究了下拼接用的参数,现在这个脚本也能下载个人文档了
* 在 @Xpink1999 的帮助下,程序现在可以正常处理超过1000本书了。(成功下载了1763本书)
*
* 2024/6/9: 修改了下载模块,避免产生“fetch时设置了‘credentials: "include"’的情况下,响应cros头是‘*’,则请求失败”的情况。
* 这样的下载方式实际上和网页的下载方式相同,但也因此,程序无法检测书本是否下载成功。
*
* @param {String} type 默认为 "ebook" ,下载电子书,若为 "pdoc", 则是下载个人文档
* @param {Array} completedList 数组,如果一本电子书的asin在其中就不会下载
* @param {Number} timeout 每个下载操作间隔的时间,单位为秒,默认为 20s
*
*/
async function download(type = "ebook", completedList = [], timeout = 20) {
function HTMLdecode(str) {
const textarea = document.createElement("textarea");
textarea.innerHTML = str;
return textarea.value;
}
async function request(activity, input) {
const result = await fetch("https://www.amazon.cn/hz/mycd/digital-console/ajax", {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
method: "POST",
body: [["activity", activity].join("="), ["activityInput", JSON.stringify(input)].join("="), ["csrfToken", encodeURIComponent(window.csrfToken)].join("=")].join("&"),
credentials: "include",
});
const resType = result.headers.get("content-type");
if (result.status == 200 && resType.includes("application/json")) {
return await result.json();
}
console.warn({ 状态: "请求失败", 状态码: result.status, post: { activity, input }, response: await result.text() });
return {};
}
async function dlFile(url, fileName = "未命名文件") {
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
await new Promise((resolve) => setTimeout(resolve, timeout * 1000));
console.log("下载完成", fileName);
}
const batchSize = 1000,
newCompletedList = [...completedList],
docType = type == "pdoc" ? "PDOC" : "EBOK",
ownershipDataCommon = {
showSharedContent: true,
fetchCriteria: { sortOrder: "DESCENDING", sortIndex: "DATE", startIndex: 0, batchSize: batchSize, totalContentCount: -1 },
surfaceType: "LargeDesktop",
},
ownershipDataEbook = {
contentType: "Ebook",
contentCategoryReference: "booksAll",
itemStatusList: ["Active"],
excludeExpiredItemsFor: ["KOLL", "Purchase", "Pottermore", "FreeTrial", "DeviceRegistration", "KindleUnlimited", "Sample", "Prime", "ComicsUnlimited", "Comixology"],
originTypes: [
"Purchase",
"PublicLibraryLending",
"PersonalLending",
"Sample",
"ComicsUnlimited",
"KOLL",
"RFFLending",
"Pottermore",
"Prime",
"Rental",
"DeviceRegistration",
"FreeTrial",
"KindleUnlimited",
"Comixology",
],
},
ownershipDataPDoc = {
contentType: "KindlePDoc",
contentCategoryReference: "pdocs",
itemStatusList: ["Active"],
},
{
success = false,
GetDevicesOverview: {
deviceList: [{ deviceSerialNumber = null, deviceTypeID = null, customerID = null }],
},
} = await request("GetDevicesOverview", { surfaceType: "LargeDesktop" });
let bookCount = 0;
if (!deviceSerialNumber || !success) {
return console.warn("获取Kindle设备信息失败");
}
while (true) {
const data = type == "pdoc" ? ownershipDataPDoc : ownershipDataEbook;
const {
success = false,
GetContentOwnershipData: { items = [], numberOfItems = 0 },
} = await request("GetContentOwnershipData", Object.assign({}, ownershipDataCommon, data));
if (!success) {
return console.warn("获取书本信息失败");
}
for (const { asin, authors, author, title } of items) {
if (newCompletedList.includes(asin)) continue;
const url = `https://cde-ta-g7g.amazon.com/FionaCDEServiceEngine/FSDownloadContent?type=${docType}&key=${asin}&fsn=${deviceSerialNumber}&device_type=${deviceTypeID}&customerId=${customerID}&authPool=AmazonCN`,
fileName = `${HTMLdecode(authors || author)} - ${HTMLdecode(title)}.azw3`;
try {
console.log("开始下载书籍:", fileName, "ASIN:", asin);
await dlFile(url, fileName);
newCompletedList.push(asin);
} catch (err) {
console.warn(err);
}
}
ownershipDataCommon.fetchCriteria.startIndex += batchSize;
bookCount += items.length;
if (bookCount >= numberOfItems) break;
}
console.log("Kindle 设备序列号:", deviceSerialNumber, "可以用于为下载的电子书移除DRM。个人文档无需去除DRM");
console.log("下方的内容,是本次下载任务中,已经完成下载的电子书的数据。将其作为 download 函数的第二个参数传入,则该次下载任务会忽略这些已下载的电子书。");
console.log(newCompletedList);
console.log("任务结束……");
}
@yige233
Copy link
Author

yige233 commented Jun 17, 2024

请问,为什么打开网页 https://www.amazon.cn/hz/mycd/myx/ 我运行这个脚本之后,网页直接跳转到 amazon.cn/error

试试浏览器的无痕模式呢?

@TheoHLong
Copy link

同样网页直接跳转到 amazon.cn/error

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