// ==UserScript==
// @name 豆瓣书籍一键跳转
// @description 改编自https://github.com/OldPanda/douban-book-plus-homepage
// @namespace namespace
// @match *://book.douban.com/subject/*
// @version 1.5.1
// @author -
// @license WTFPL
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.xmlhttpRequest
// @grant GM.getResourceURL
// @resource logo_douban https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/douban-logo.svg
// @resource logo_duokan https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/duokan-logo.png
// @resource logo_snail https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/snailreader-logo.png
// @resource logo_dedao https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/dedao-logo.png
// @resource logo_zlibrary https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/zlibrary-logo.png
// @resource logo_anna https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/anna-logo.svg
// @resource logo_weread https://github.com/OldPanda/douban-book-plus-homepage/raw/refs/heads/master/docs/public/weread-logo.png
// ==/UserScript==
const vendors = [
"weread",
// "kindle",
"duokan",
"snail",
"douban",
"dedao",
"zlibrary",
"anna"
];
const preferencesKey = "douban-book-plus-preferences";
const apiEndpoint = "https://api.old-panda.com/GetEBookUrls";
const dedaoPrefix1 = "https://www.dedao.cn/reader";
const dedaoPrefix2 = "https://www.dedao.cn/ebook/reader";
const doubanPrefix = "https://read.douban.com/reader/ebook";
function simple_fetch(url, method = "GET") {
return new Promise((resolve, reject) => {
GM.xmlhttpRequest({
url: url,
method: method,
responseType: "json",
onload(res) {
try {
if (res.status == 200) {
resolve(res.response);
} else {
reject(new DOMException("Failed to fetch: response code is " + res.status));
}
} catch (e) {
reject(e);
}
},
onabort() {
reject(new DOMException("Aborted", "AbortError"));
},
ontimeout() {
reject(new TypeError("Network request failed, timeout"));
},
onerror(err) {
reject(new TypeError("Failed to fetch: " + err.finalUrl));
},
});
})
}
async function background(message) {
let ebookLinks = message.ebooks;
let dedao = null;
let douban = null;
for (let link of ebookLinks) {
if (link.startsWith(dedaoPrefix1) || link.startsWith(dedaoPrefix2)) {
dedao = link;
} else if (link.startsWith(doubanPrefix)) {
douban = link;
}
}
let params = new URLSearchParams({
"isbn": message.isbn,
"title": message.title,
"authors": message.authors,
"publisher": message.publisher,
"douban_url": message.doubanURL,
});
if (message.subtitle !== null) {
params.append("subtitle", message.subtitle);
}
if (message.translators !== null) {
params.append("translators", message.translators);
}
if (dedao !== null) {
params.append("dedao", dedao);
}
if (douban !== null) {
params.append("douban", douban);
}
let url = apiEndpoint + "?" + params.toString();
// fetch preferences first, then remove the vendors
// which are turned off by users
let resp = await simple_fetch(url);
let settings = await GM.getValue("vendors", { 'weread': false });
for (let [vendor, checked] of Object.entries(settings)) {
if (!checked) {
delete resp[vendor];
}
}
return resp;
}
const nonAsciiChecker = str => [...str].some(char => char.charCodeAt(0) > 127);
const imgSizes = {
"weread": [117, 32],
"duokan": [109, 32],
"snail": [129, 32],
"douban": [99, 32],
"dedao": [80, 32],
"zlibrary": [126, 32],
"anna": [200, 32],
"nobook": [150, 32],
};
let getValue = (item) => {
return item.split(":")[1].trim();
};
let parseNames = (names) => {
return names.split("/")
.map((item) => item.trim())
.filter((item) => item.length > 0)
.map((item) => {
let newItem = item;
if (item.startsWith("[") || item.startsWith("【")) {
let idx = 0;
while (idx < item.length) {
if (item[idx] == "]" || item[idx] == "】") {
break;
}
idx++;
}
newItem = item.substring(idx + 1, item.length);
}
return newItem.trim().replace(/ /g, "");
})
.join(",");
}
async function main() {
if (window.location.href.indexOf("book.douban.com/subject/") != -1 && window.location.href.indexOf("/comments/") === -1) {
// parse basic info of the book
let title = document
.querySelectorAll("[property='v:itemreviewed']")[0]
.textContent.trim();
if (nonAsciiChecker(title)) {
title = title.replaceAll(" ", "").trim();
}
let bookInfo = document.getElementById("info").innerText.split("\n");
let isbn, publisher, authors, subtitle, translators;
for (let i = 0; i < bookInfo.length; i++) {
if (bookInfo[i].trim().startsWith("ISBN:")) {
isbn = getValue(bookInfo[i]);
} else if (bookInfo[i].trim().startsWith("出版社:")) {
publisher = getValue(bookInfo[i]);
} else if (bookInfo[i].trim().startsWith("作者:")) {
authors = getValue(bookInfo[i]);
} else if (bookInfo[i].trim().startsWith("副标题:")) {
subtitle = getValue(bookInfo[i]);
} else if (bookInfo[i].trim().startsWith("译者:")) {
translators = getValue(bookInfo[i]);
}
}
authors = parseNames(authors);
if (translators !== null && translators !== undefined) {
translators = parseNames(translators);
}
// parse link of Douban and Dedao, if available
let ebooksOnPage = document.getElementsByClassName("online-read-or-audio");
let ebookLinks = [];
for (let ebook of ebooksOnPage) {
let link = ebook.getElementsByTagName("a").item(0).getAttribute("href");
if (link !== null) {
ebookLinks.push(link);
}
}
if (isbn !== null && title !== null && publisher !== null && authors !== null) {
let message = await background(
{
isbn: isbn,
title: title,
subtitle: subtitle,
publisher: publisher,
authors: authors,
translators: translators,
doubanURL: window.location.href,
ebooks: ebookLinks
}
);
let found = false;
if (message.hasOwnProperty("weread")) {
showLink("weread", message.weread, "img/weread-logo.png");
found = true;
}
// if (message.hasOwnProperty("kindle")) {
// showLink(message.kindle, "img/kindle-logo.png");
// }
if (message.hasOwnProperty("duokan")) {
showLink("duokan", message.duokan, "img/duokan-logo.png");
found = true;
}
if (message.hasOwnProperty("snail")) {
showLink("snail", message.snail, "img/snail-logo.png");
found = true;
}
if (message.hasOwnProperty("douban")) {
showLink("douban", message.douban, "img/douban-logo.svg");
found = true;
}
if (message.hasOwnProperty("dedao")) {
showLink("dedao", message.dedao, "img/dedao-logo.png");
found = true;
}
if (message.hasOwnProperty("zlibrary")) {
showLink("zlibrary", message.zlibrary, "img/zlibrary-logo.png");
found = true;
}
if (message.hasOwnProperty("anna")) {
showLink("anna", message.anna, "img/anna-logo.svg");
found = true;
}
if (!found) {
showLink("nobook", "", "img/no-book.png");
}
}
}
}
function initDivElement() {
let ul = document.getElementById("douban-book-plus-list");
if (ul === null) {
let div = document.createElement("div");
div.id = "douban-book-plus";
div.style.padding = "18px 16px";
div.style.backgroundColor = "#F6F6F2";
div.style.margin = "20px auto";
let componentTitle = document.createElement("h2");
componentTitle.innerHTML = `
<span>在线阅读</span>
· · · · · ·
`;
componentTitle.style.fontSize = "15px";
div.append(componentTitle);
ul = document.createElement("ul");
ul.id = "douban-book-plus-list";
div.append(ul);
let footer = document.createElement("p");
footer.style = "text-align: center; color: grey;";
footer.innerHTML = `Powered by <a href="https://doubanbook.plus/" target="_blank">Douban Book+</a>`;
div.append(footer);
let element = document.getElementsByClassName("aside");
element.item(0).insertBefore(div, element.item(0).firstChild);
}
return ul;
}
// add ebook logo
function showLink(name, url, imgUrl) {
if (url) {
let ul = initDivElement();
let li = document.createElement("li");
li.style.borderBottom = "1px solid rgba(0,0,0,0.08)";
li.style.margin = "10px auto";
let a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.style.backgroundColor = "transparent";
let img = new Image();
if (imgUrl.startsWith("http")) {
img.src = imgUrl;
} else {
img.src = GM.getResourceURL(`logo_${name}`);
}
[img.width, img.height] = imgSizes[name];
a.append(img);
li.append(a);
ul.append(li);
} else if (name == "nobook") {
let ul = initDivElement();
let li = document.createElement("li");
li.style.borderBottom = "1px solid rgba(0,0,0,0.08)";
li.style.margin = "10px auto";
let img = new Image();
if (imgUrl.startsWith("http")) {
img.src = imgUrl;
} else {
img.src = chrome.runtime.getURL(imgUrl);
}
img.style = "display: block; margin-left: auto; margin-right: auto;";
[img.width, img.height] = imgSizes[name];
li.append(img);
ul.append(li);
}
}
main().then();