// ==UserScript==
// @name Kobo e-Books Update Checker
// @name:zh-TW Kobo 電子書更新檢查器
// @description Checks if updates were available for the e-books you own.
// @description:zh-TW 檢查你購買的電子書是否有更新檔提供。
// @icon https://icons.duckduckgo.com/ip3/www.kobo.com.ico
// @author Jason Kwok
// @namespace https://jasonhk.dev/
// @version 1.0.0
// @license MIT
// @match https://www.kobo.com/*/library/books
// @run-at document-end
// @grant GM.addStyle
// @require https://update.gf.qytechs.cn/scripts/482311/1296526/queue.js
// ==/UserScript==
let MESSAGES;
{
const PATHNAME_PREFIX_PATTERN = /\/(?<region>[a-z]{2})\/(?<language>[a-z]{2})\//;
const I18N = {
en: {
CHECK_UPDATE_FOR_PAGE: "Check Update for Page",
CHECK_UPDATE: "Check Update",
QUEUED: "Queued",
CHECKING: "Checking...",
LATEST: "Latest",
OUTDATED: "Outdated",
PREVIEW: "Preview",
FAILED: "Failed",
NOT_SUPPORTED: "Not Supported",
BOOK_WAS_UNLISTED: "The book was unlisted, there’s no way to check update for this type of books at the moment.",
BOOK_IS_AUDIOBOOK: "The book is audiobook, there’s no way to check update for this type of books at the moment.",
UNKNOWN_ERROR: "Unknown error, please contact the developer for further investigations.",
},
zh: {
CHECK_UPDATE_FOR_PAGE: "為本頁檢查更新",
CHECK_UPDATE: "檢查更新",
QUEUED: "等待中",
CHECKING: "檢查中…",
LATEST: "最新",
OUTDATED: "過時",
PREVIEW: "預覽",
FAILED: "檢查失敗",
NOT_SUPPORTED: "不支援",
BOOK_WAS_UNLISTED: "該書籍已下架,目前尚未有方法為這類書籍檢查更新。",
BOOK_IS_AUDIOBOOK: "該書籍為有聲書,目前尚未有方法為這類書籍檢查更新。",
UNKNOWN_ERROR: "未知錯誤,請聯絡開發者以進一步調查。",
},
};
const { language } = location.pathname.match(PATHNAME_PREFIX_PATTERN).groups;
MESSAGES = Object.hasOwn(I18N, language) ? I18N[language] : I18N.en;
}
GM.addStyle(`
.library-container .check-update-container
{
display: inline;
}
.library-container .check-update-controls.filter-chip, .library-container .check-update-controls .check-update-button
{
width: auto;
}
.library-container .check-update-button
{
display: flex;
border-radius: 20px;
min-width: 0;
max-width: 100%;
width: 100%;
justify-content: space-between;
align-items: center;
overflow: hidden;
background-color: #eee;
color: #000;
font-size: 1.6rem;
font-family: "Rakuten Sans UI", "Trebuchet MS", Trebuchet, Arial, Helvetica, sans-serif;
font-weight: 400;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
white-space: nowrap;
transition: background-color .3s ease-in-out, color .15s ease-in-out 0s;
}
.library-container .check-update-button::before
{
position: absolute;
top: calc(50% - 30px);
border-radius: 80px;
width: calc(100% - 30px);
height: 60px;
background-color: rgba(0, 0, 0, .1);
content: "";
opacity: 0;
transform: scale(0);
}
.library-container .check-update-button:hover
{
background-color: rgba(0, 0, 0, .04);
}
.library-container .check-update-button:active
{
background-color: #000;
color: #fff;
}
.library-container .check-update-button:focus::before
{
opacity: 1;
transform: scale(1);
}
@media (max-width: 745px)
{
.library-container .check-update-container
{
display: block;
text-align: left;
}
.library-container .check-update-controls
{
margin-right: 18px;
}
}
.product-field.item-status.outdated
{
background: #FE8484;
}
.product-field.item-status:is(.failed, .audiobook) a
{
cursor: pointer;
}
`);
const READING_URL_PATTERN = /\/ReadNow\/(?<id>[0-9a-f-]{36})/;
const STORE_AUDIOBOOK_URL_PATTERN = /\/(?<region>[a-z]{2})\/(?<language>[a-z]{2})\/audiobook\//;;
const items = document.querySelectorAll(".library-items > li");
for (const item of items)
{
const actions = item.querySelector(".item-info + .item-bar .library-actions-list");
const actionContainer = document.createElement("li");
actionContainer.classList.add("library-actions-list-item");
const action = document.createElement("button");
action.classList.add("library-action");
action.textContent = MESSAGES.CHECK_UPDATE;
action.addEventListener("click", () => checkUpdate(item));
actionContainer.appendChild(action);
actions.appendChild(actionContainer);
}
{
const controls = document.querySelector(".secondary-controls");
const container = document.createElement("div");
container.classList.add("check-update-container");
const wrapper = document.createElement("div");
wrapper.classList.add("check-update-controls", "filter-chip");
const button = document.createElement("button");
button.classList.add("check-update-button");
button.textContent = MESSAGES.CHECK_UPDATE_FOR_PAGE;
button.addEventListener("click", () => items.forEach(checkUpdate));
wrapper.appendChild(button);
container.appendChild(wrapper);
controls.insertBefore(container, controls.firstChild);
}
const queue = new Queue({ autostart: true, concurrency: 6 });
function checkUpdate(item)
{
const message = item.querySelector(".product-field.item-status");
message.replaceChildren(MESSAGES.QUEUED);
if (item.dataset.koboGizmo === "PreviewLibraryItem")
{
message.classList.remove("buy-now");
message.replaceChildren(MESSAGES.PREVIEW);
return;
}
const storeLink = item.querySelector(".product-field.title a");
if (STORE_AUDIOBOOK_URL_PATTERN.test(storeLink.href))
{
message.classList.add("audiobook");
const link = document.createElement("a");
link.textContent = MESSAGES.NOT_SUPPORTED;
link.addEventListener("click", (event) => alert(MESSAGES.BOOK_IS_AUDIOBOOK));
message.replaceChildren(link);
return;
}
queue.push(async () =>
{
message.textContent = MESSAGES.CHECKING;
try
{
const currentId = getCurrentProductId();
const latestId = await getLatestProductId(storeLink.href);
console.debug(`${storeLink.innerText}\n Current: ${currentId}\n Latest : ${latestId}`);
if (currentId === latestId)
{
message.classList.remove("outdated", "failed");
message.replaceChildren(MESSAGES.LATEST);
}
else
{
message.classList.add("failed");
message.replaceChildren(MESSAGES.OUTDATED);
message.classList.add("outdated");
}
}
catch (e)
{
message.classList.remove("outdated");
message.classList.add("failed");
const link = document.createElement("a");
link.textContent = MESSAGES.FAILED;
link.addEventListener("click", (event) => alert(e.message));
message.replaceChildren(link);
}
});
function getCurrentProductId()
{
const matches = item.querySelector(".library-action.readnow").href.match(READING_URL_PATTERN);
return matches?.groups.id;
}
async function getLatestProductId(url)
{
const response = await fetch(url);
if (!response.ok)
{
if (response.status === 404)
{
throw new Error(MESSAGES.BOOK_WAS_UNLISTED);
}
throw new Error(MESSAGES.UNKNOWN_ERROR);
}
const html = await response.text();
const parser = new DOMParser();
const page = parser.parseFromString(html, "text/html");
const itemId = page.querySelector("#ratItemId");
if (itemId) { return itemId.value; }
throw new Error(MESSAGES.UNKNOWN_ERROR);
}
}