Skip to content

Instantly share code, notes, and snippets.

@smartchaos
Forked from yige233/downloadMyEBooks.js
Last active May 7, 2023 07:02
Show Gist options
  • Save smartchaos/fcf95f4ebe21059d5eb5241bef87f5c1 to your computer and use it in GitHub Desktop.
Save smartchaos/fcf95f4ebe21059d5eb5241bef87f5c1 to your computer and use it in GitHub Desktop.
Kindle 中国 批量下载自己的电子书和个人文档
/**
* 批量下载自己已购买的电子书和个人文档
* 要求:至少有一台Kindle设备。
*
* 打开 https://z.cn/myk ,然后按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,
* 这样可以获取个人文档的下载链接,于是研究了下拼接用的参数,现在这个脚本也能下载个人文档了
* 超过1000本书的情况,也是借鉴了该项目的处理方法,因为本人没那么多书,所以==v==
*/
function decodeEntity(inputStr) {
var textarea = document.createElement("textarea");
textarea.innerHTML = inputStr;
return textarea.value;
}
async function ajax(data) {
function obj2Urlencoded(obj) {
const arr = [];
for (const key in obj) arr.push(`${key}=${obj[key]}`);
return arr.join("&");
};
const res = await fetch("https://www.amazon.cn/hz/mycd/digital-console/ajax", {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: obj2Urlencoded({
data: JSON.stringify(data),
csrfToken: encodeURIComponent(window.csrfToken)
}),
method: "POST",
credentials: "include"
});
return await res.json() || {};
};
/**
* @param type 默认为 "ebook" ,下载电子书,若为 "pdoc", 则是下载个人文档
* @param completedList 数组,如果一本电子书的asin在其中就不会下载
*/
async function download(type = "ebook", completedList = window.completedList || []) {
function getCompletedList(list = []) {
window.open(URL.createObjectURL(new Blob([JSON.stringify(list)], {
type: "application/json"
})));
};
async function dl(url, fileName) {
try {
console.log("开始下载", fileName);
const startAt = new Date();
const res = await fetch(url, {
credentials: "include"
});
const contentLength = res.headers.get('content-length') / 1024 / 1024;
const reader = res.body.getReader();
let receivedLength = 0;
let chunks = [];
while (true) {
const {
done,
value
} = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
};
const blob = new Blob(chunks, {
type: res.headers.get('content-type')
});
let a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName || "未命名文件";
document.body.append(a);
a.click();
console.log(`下载 ${fileName} 完成!文件大小共${contentLength.toFixed(2)}MB,下载耗时 ${(new Date()-startAt)/1e3} s`);
a.remove();
} catch (err) {
console.error("Download Error:", err);
throw {
url: url,
fileName: fileName
}
};
};
const devices = await ajax({
param: {
GetDevices: {}
}
}).catch(err => {
console.error("获取设备列表失败:", err);
throw err;
});
const newCompletedList = [...completedList];
const params = {
OwnershipData: {
sortOrder: "DESCENDING",
sortIndex: "DATE",
batchSize: 1000,
totalContentCount: 0,
itemStatus: ["Active"]
}
};
var startIndex = 0;
if (!devices.GetDevices.devices.length) {
return console.warn("没有可供下载的设备,必须要有至少一台Kindle设备才可以下载……");
};
if (type == "pdoc") {
params.OwnershipData = Object.assign(params.OwnershipData, {
contentType: "KindlePDoc",
itemStatus: ["Active"]
});
} else {
params.OwnershipData = Object.assign(params.OwnershipData, {
contentType: "Ebook",
itemStatus: ["Active", "Expired"],
excludeExpiredItemsFor: ["KOLL", "Purchase", "Pottermore", "FreeTrial", "DeviceRegistration", "ku", "Sample", "Prime", "ComicsUnlimited"],
originType: ["Purchase", "PublicLibraryLending", "PersonalLending", "ComicsUnlimited", "KOLL", "RFFLending", "Pottermore", "Rental", "DeviceRegistration", "FreeTrial", "ku", "Sample", "Prime"],
showSharedContent: true
});
};
while (true) {
params.OwnershipData = Object.assign(params.OwnershipData, {
startIndex: startIndex,
});
const res = await ajax({
param: params
}).catch(err => {
console.error("获取电子书列表失败:", err);
throw err;
});
for (let eBook of res.OwnershipData.items) {
const docType = (type == "pdoc") ? "PDOC" : "EBOK",
key = eBook.asin,
fsn = devices.GetDevices.devices[0].deviceSerialNumber,
deviceType = devices.GetDevices.devices[0].deviceType,
customerId = devices.GetDevices.devices[0].customerId;
try {
if (newCompletedList.includes(key)) continue;
await dl(`https://cde-ta-g7g.amazon.com/FionaCDEServiceEngine/FSDownloadContent?type=${docType}&key=${key}&fsn=${fsn}&device_type=${deviceType}&customerId=${customerId}&authPool=AmazonCN`,
`${decodeEntity(eBook.authors ||eBook.author)} - ${decodeEntity(eBook.title)}.azw3`);
newCompletedList.push(key);
} catch (err) {
console.warn("下载电子书失败:", err);
};
};
if (!res.hasMoreItems) break;
startIndex += 1000;
};
console.log("Kindle 设备序列号:",devices.GetDevices.devices[0].deviceSerialNumber,"可以用于为下载的电子书移除DRM。个人文档无需去除DRM");
window.completedList = newCompletedList;
getCompletedList(newCompletedList);
console.log("刚刚打开的网页,是本次下载任务中,已经完成下载的电子书的数据。将其作为 download 函数的第二个参数传入,则该次下载任务会忽略这些已下载的电子书。");
console.log("任务结束……");
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment