Kobo e-Books Update Checker

Checks if updates were available for the e-books you own.

当前为 2023-12-17 提交的版本,查看 最新版本

// ==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.2.1
// @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",
            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: "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 an 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: "為本頁檢查更新",
            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: "該書已下架,目前尚未有方法為這類書籍檢查更新。",
            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 .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 .update-container
        {
            text-align: left;
        }

        .library-container .update-controls
        {
            margin-right: 18px;
        }

        .library-container .library-content.grid .more-actions:not(.open)
        {
            width: fit-content;
            transform: translateY(35px);
        }
    }

    .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 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", () => books.forEach(checkUpdate));

    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 : MESSAGES.COPIED_N_BOOKS_INTO_CLIPBOARD.replace("{0}", 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 matches = book.querySelector(".library-action.readnow").href.match(READING_URL_PATTERN);
    return matches?.groups.id;
}

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; }

    throw new Error(MESSAGES.UNKNOWN_ERROR);
}

const queue = new Queue({ autostart: true, concurrency: 6 });

function checkUpdate(book)
{
    const message = book.querySelector(".product-field.item-status");

    book.dataset.checkStatus = STATUS_PENDING;
    message.replaceChildren(MESSAGES.PENDING);

    if (book.dataset.koboGizmo === "PreviewLibraryItem")
    {
        book.dataset.checkStatus = STATUS_SKIPPED;

        message.classList.remove("buy-now");
        message.replaceChildren(MESSAGES.PREVIEW);
        return;
    }

    if (isAudiobook(book))
    {
        book.dataset.checkStatus = STATUS_SKIPPED;

        const link = document.createElement("a");
        link.textContent = MESSAGES.SKIPPED;
        link.addEventListener("click", (event) => alert(MESSAGES.BOOK_IS_AUDIOBOOK));

        message.replaceChildren(link);
        return;
    }

    queue.push(async () =>
    {
        book.dataset.checkStatus = STATUS_CHECKING;
        message.textContent = MESSAGES.CHECKING;

        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);
        }
    });
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址