您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Checks if updates were available for the e-books you own.
当前为
// ==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.4.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/1297431/queue.js // @supportURL https://gf.qytechs.cn/scripts/482410/feedback // ==/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", FINISHED_CHECKING_ALL_BOOKS: "Finished checking all books for this page.\n\nLatest: {0}\nOutdated: {1}\nPreview: {2}\nSkipped{3}\nFailed: {4}", COPY_OUTDATED_BOOKS: "Copy Outdated Books", NO_BOOKS_WERE_OUTDATED: "No books were outdated.", COPIED_N_BOOKS_INTO_CLIPBOARD: "Copied {0} book(s) into the clipboard.", CHECK_UPDATE: "Check Update", PENDING: "Pending...", CHECKING: "Checking...", LATEST: "Latest", OUTDATED: "Outdated", PREVIEW: "Preview", SKIPPED: "Skipped", FAILED: "Failed", BOOK_WAS_UNLISTED: "This book was unlisted, there’s no way to check update for this type of books at the moment.", FETCH_PRODUCT_ID_FAILED: "Failed to fetch the latest product ID, please contact the developer for further investigations.", UNKNOWN_ERROR: "Unknown error, please contact the developer for further investigations.", }, zh: { CHECK_UPDATE_FOR_PAGE: "為本頁檢查更新", FINISHED_CHECKING_ALL_BOOKS: "完成檢查本頁的書籍。\n\n最新:{0}\n過時:{1}\n預覽:{2}\n已略過:{3}\n檢查失敗:{4}", COPY_OUTDATED_BOOKS: "複製過時書籍", NO_BOOKS_WERE_OUTDATED: "沒有過時的書籍。", COPIED_N_BOOKS_INTO_CLIPBOARD: "已複製 {0} 本書到剪貼簿。", CHECK_UPDATE: "檢查更新", PENDING: "等待中…", CHECKING: "檢查中…", LATEST: "最新", OUTDATED: "過時", PREVIEW: "預覽", SKIPPED: "已略過", FAILED: "檢查失敗", BOOK_WAS_UNLISTED: "該書已下架,目前尚未有方法為這類書籍檢查更新。", FETCH_PRODUCT_ID_FAILED: "無法取得最新的產品編號,請聯絡開發者以進一步調查。", UNKNOWN_ERROR: "未知錯誤,請聯絡開發者以進一步調查。", }, }; const { language } = location.pathname.match(PATHNAME_PREFIX_PATTERN).groups; MESSAGES = Object.hasOwn(I18N, language) ? I18N[language] : I18N.en; } GM.addStyle(` .library-container .update-container { text-align: right; } .library-container .update-controls { position: relative; display: inline-block; margin-top: 18px; min-width: 13rem; width: auto; } .library-container .update-button { border-radius: 20px; min-width: 0; max-width: 100%; width: auto; 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 .update-button:not(:first-child) { margin-left: 5px; } .library-container .update-button:not(:last-child) { margin-right: 5px; } .library-container .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 .update-button:hover { background-color: rgba(0, 0, 0, .04); } .library-container .update-button:focus::before { opacity: 1; transform: scale(1); } .library-container .update-button:active { background-color: #000; color: #fff; } @media (max-width: 568px) { .library-container .secondary-controls { margin-right: 18px; } .library-container .update-container { text-align: left; } .library-container .update-controls { margin-right: 0; width: 100%; white-space: break-spaces; } .library-container .update-button { margin-left: 0 !important; margin-right: 0 !important; width: 100%; text-align: center; } .library-container .update-button:not(:first-child) { margin-top: 18px; } .library-container .library-content.grid .more-actions:not(.open) { width: fit-content; transform: translateY(35px); } } @media (max-width: 378px) { } .item-wrapper.book[data-check-status=outdated] .product-field.item-status { background: #FE8484; } .item-wrapper.book:is([data-check-status=skipped], [data-check-status=failed]) .product-field.item-status a { text-decoration-line: underline; cursor: pointer; } `); const STATUS_PENDING = "pending"; const STATUS_CHECKING = "checking"; const STATUS_LATEST = "latest"; const STATUS_OUTDATED = "outdated"; const STATUS_PREVIEW = "preview"; const STATUS_SKIPPED = "skipped"; const STATUS_FAILED = "failed"; 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 queue = new Queue({ autostart: true, concurrency: 6 }); const books = document.querySelectorAll(".item-wrapper.book"); for (const book of books) { const actions = book.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(book)); actionContainer.appendChild(action); actions.appendChild(actionContainer); } { const controls = document.querySelector(".secondary-controls"); const container = document.createElement("div"); container.classList.add("update-container"); const wrapper = document.createElement("div"); wrapper.classList.add("update-controls"); const checkButton = document.createElement("button"); checkButton.classList.add("update-button"); checkButton.textContent = MESSAGES.CHECK_UPDATE_FOR_PAGE; checkButton.addEventListener("click", () => checkUpdateForBooks(books)); const copyButton = document.createElement("button"); copyButton.classList.add("update-button"); copyButton.textContent = MESSAGES.COPY_OUTDATED_BOOKS; copyButton.addEventListener("click", () => { const books = document.querySelectorAll(".item-wrapper.book[data-check-status=outdated]"); navigator.clipboard.writeText(Array.prototype.map.call(books, getBookTitle).join("\n")); alert((books.length === 0) ? MESSAGES.NO_BOOKS_WERE_OUTDATED : buildMessage(MESSAGES.COPIED_N_BOOKS_INTO_CLIPBOARD, [books.length])); }); wrapper.append(checkButton, copyButton); container.appendChild(wrapper); controls.insertBefore(container, controls.firstChild); } function isAudiobook(book) { return STORE_AUDIOBOOK_URL_PATTERN.test(book.querySelector(".product-field.title a").href); } function getBookTitle(book) { return book.querySelector(".product-field.title").innerText; } function getCurrentProductId(book) { const config = JSON.parse(book.querySelector(".library-action.mark-as-finished").dataset.koboGizmoConfig); return config.productId; } async function getLatestProductId(book) { const response = await fetch(book.querySelector(".product-field.title a").href); 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; } const config = page.querySelector(".item-detail"); if (config) { return JSON.parse(config.dataset.koboGizmoConfig).productId; } throw new Error(MESSAGES.UNKNOWN_ERROR); } function buildMessage(message, replacements) { return message.replaceAll(/\{(\d+)\}/g, (_, index) => replacements[index]); } function checkUpdate(book) { const message = book.querySelector(".product-field.item-status"); book.dataset.checkStatus = STATUS_PENDING; message.replaceChildren(MESSAGES.PENDING); queue.push(async () => { book.dataset.checkStatus = STATUS_CHECKING; message.textContent = MESSAGES.CHECKING; if (book.dataset.koboGizmo === "PreviewLibraryItem") { book.dataset.checkStatus = STATUS_SKIPPED; message.classList.remove("buy-now"); message.replaceChildren(MESSAGES.PREVIEW); return; } try { const currentId = getCurrentProductId(book); const latestId = await getLatestProductId(book); console.debug(`${getBookTitle(book)}\n Current: ${currentId}\n Latest : ${latestId}`); if (currentId === latestId) { book.dataset.checkStatus = STATUS_LATEST; message.replaceChildren(MESSAGES.LATEST); } else { book.dataset.checkStatus = STATUS_OUTDATED; message.replaceChildren(MESSAGES.OUTDATED); } } catch (e) { book.dataset.checkStatus = STATUS_FAILED; const link = document.createElement("a"); link.textContent = MESSAGES.FAILED; link.addEventListener("click", (event) => alert(e.message)); message.replaceChildren(link); } }); } function checkUpdateForBooks(books) { queue.addEventListener("end", () => { let latest = 0; let outdated = 0; let preview = 0; let skipped = 0; let failed = 0; for (const book of books) { switch (book.dataset.checkStatus) { case STATUS_LATEST: latest++; break; case STATUS_OUTDATED: outdated++; break; case STATUS_PREVIEW: preview++; break; case STATUS_SKIPPED: skipped++ break; case STATUS_FAILED: failed++; break; } } alert(buildMessage(MESSAGES.FINISHED_CHECKING_ALL_BOOKS, [latest, outdated, preview, skipped, failed])); }, { once: true }); books.forEach(checkUpdate); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址