InoReader restore lost images

Loads new images from VK and Telegram in InoReader articles

目前為 2024-03-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         InoReader restore lost images
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  Loads new images from VK and Telegram in InoReader articles
// @author       Kenya-West
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @match        https://*.inoreader.com/feed*
// @match        https://*.inoreader.com/article*
// @match        https://*.inoreader.com/folder*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    const appConfig = {
        corsProxy: "https://corsproxy.io/?",
    };

    const appState = {
        readerPaneExists: false,
        restoreImagesInListView: false,
        restoreImagesInArticleView: false,
    };

    // Select the node that will be observed for mutations
    const targetNode = document.body;

    // Options for the observer (which mutations to observe)
    const mutationObserverGlobalConfig = {
        attributes: false,
        childList: true,
        subtree: true,
    };

    const querySelectorPathArticleRoot =
        ".article_full_contents .article_content";

    /**
     * Callback function to execute when mutations are observed
     * @param {MutationRecord[]} mutationsList - List of mutations observed
     * @param {MutationObserver} observer - The MutationObserver instance
     */
    const callback = function (mutationsList, observer) {
        for (let mutation of mutationsList) {
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach(function (node) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                            restoreImagesInArticleList(node);
                            runRestoreImagesInArticleView(node);
                    }
                });
            }
        }
    };

    function registerCommands() {
        let enableImageRestoreInListViewCommand;
        let disableImageRestoreInListViewCommand;
        let enableImageRestoreInArticleViewCommand;
        let disableImageRestoreInArticleViewCommand;

        const restoreImageListView = localStorage.getItem("restoreImageListView") ?? "false";
        const restoreImageArticleView = localStorage.getItem(
            "restoreImageArticleView"
        ) ?? "true";

        if (restoreImageListView === "false") {
            appState.restoreImagesInListView = false;
            // @ts-ignore
            enableImageRestoreInListViewCommand = GM_registerMenuCommand(
                "Enable image restore in article list",
                () => {
                    localStorage.setItem("restoreImageListView", "true");
                    appState.restoreImagesInListView = true;
                    if (enableImageRestoreInListViewCommand) {
                        unregisterAllCommands();
                        registerCommands();
                    }
                }
            );
        } else {
            appState.restoreImagesInListView = true;
            // @ts-ignore
            disableImageRestoreInListViewCommand = GM_registerMenuCommand(
                "Disable image restore in article list",
                () => {
                    localStorage.setItem("restoreImageListView", "false");
                    appState.restoreImagesInListView = false;
                    if (disableImageRestoreInListViewCommand) {
                        unregisterAllCommands();
                        registerCommands();
                    }
                }
            );
        }

        if (restoreImageArticleView === "false") {
            appState.restoreImagesInArticleView = false;
            // @ts-ignore
            enableImageRestoreInArticleViewCommand = GM_registerMenuCommand(
                "Enable image restore in article view",
                () => {
                    localStorage.setItem("restoreImageArticleView", "true");
                    appState.restoreImagesInArticleView = true;
                    if (enableImageRestoreInArticleViewCommand) {
                        unregisterAllCommands();
                        registerCommands();
                    }
                }
            );
        } else {
            appState.restoreImagesInArticleView = true;
            // @ts-ignore
            disableImageRestoreInArticleViewCommand = GM_registerMenuCommand(
                "Disable image restore in article view",
                () => {
                    localStorage.setItem("restoreImageArticleView", "false");
                    appState.restoreImagesInArticleView = false;
                    if (disableImageRestoreInArticleViewCommand) {
                        unregisterAllCommands();
                        registerCommands();
                    }
                }
            );
        }

        function unregisterCommand(command) {
            // @ts-ignore
            GM_unregisterMenuCommand(command);
        };

        function unregisterAllCommands() {
            // @ts-ignore
            GM_unregisterMenuCommand(enableImageRestoreInListViewCommand);
            // @ts-ignore
            GM_unregisterMenuCommand(disableImageRestoreInListViewCommand);
            // @ts-ignore
            GM_unregisterMenuCommand(enableImageRestoreInArticleViewCommand);
            // @ts-ignore
            GM_unregisterMenuCommand(disableImageRestoreInArticleViewCommand);
        }
    }

    /**
     *
     * @param {Node} node
     * @returns {void}
     */
    function restoreImagesInArticleList(node) {
        const readerPane = document.body.querySelector("#reader_pane");
        if (readerPane) {
            if (!appState.readerPaneExists) {
                appState.readerPaneExists = true;

                /**
                 * Callback function to execute when mutations are observed
                 * @param {MutationRecord[]} mutationsList - List of mutations observed
                 * @param {MutationObserver} observer - The MutationObserver instance
                 */
                const callback = function (mutationsList, observer) {
                    for (let mutation of mutationsList) {
                        if (mutation.type === "childList") {
                            mutation.addedNodes.forEach(function (node) {
                                if (node.nodeType === Node.ELEMENT_NODE) {
                                    if (appState.restoreImagesInListView) {
                                        setTimeout(() => {
                                            start(node);
                                        }, 500);
                                    }
                                }
                            });
                        }
                    }
                };

                // Options for the observer (which mutations to observe)
                const mutationObserverLocalConfig = {
                    attributes: false,
                    childList: true,
                    subtree: false,
                };

                // Create an observer instance linked to the callback function
                const tmObserverImageRestoreReaderPane = new MutationObserver(
                    callback
                );

                // Start observing the target node for configured mutations
                tmObserverImageRestoreReaderPane.observe(
                    readerPane,
                    mutationObserverLocalConfig
                );
            }
        } else {
            appState.readerPaneExists = false;
        }

        /**
         *
         * @param {Node} node
         */
        function start(node) {
            const imageElement = getImageElement(node);
            if (imageElement) {
                const telegramPostUrl = getTelegramPostUrl(node);
                const imageUrl = getImageLink(imageElement);
                if (imageUrl) {
                    console.log(
                        `Found an image in the article list. Image URL: ${imageUrl}, Telegram post URL: ${telegramPostUrl}`
                    );
                    testImageLink(imageUrl).then(() => {
                        console.log(`Image loaded. Image URL: ${imageUrl}`);
                        replaceImageSrc(imageElement, telegramPostUrl);
                        console.log(`Replaced the image!`);
                    });
                }
            }
        }

        /**
         *
         * @param {Node} node
         * @returns {HTMLDivElement | null}
         */
        function getImageElement(node) {
            /**
             * @type {HTMLDivElement}
             */
            // @ts-ignore
            const nodeElement = node;
            /**
             * @type {HTMLDivElement | null}
             */
            const divImageElement = nodeElement.querySelector(
                "a[href*='t.me'] > div[style*='background-image']"
            );
            return divImageElement ?? null;
        }

        /**
         *
         * @param {Node} node
         */
        function getTelegramPostUrl(node) {
            /**
             * @type {HTMLDivElement}
             */
            // @ts-ignore
            const nodeElement = node;
            /**
             * @type {HTMLAnchorElement | null}
             */
            const ahrefElement = nodeElement.querySelector("a[href*='t.me']");
            const telegramPostUrl = ahrefElement?.href ?? "";
            // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
            try {
                return (
                    new URL(telegramPostUrl).origin +
                    new URL(telegramPostUrl).pathname
                );
            } catch (error) {
                return telegramPostUrl?.split("?")[0];
            }
        }

        /**
         *
         * @param {HTMLDivElement} div
         */
        function getImageLink(div) {
            const backgroundImageUrl = div?.style.backgroundImage;
            /**
             * @type {string | undefined}
             */
            let imageUrl;
            try {
                imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
            } catch (error) {
                imageUrl = backgroundImageUrl?.slice(5, -2);
            }

            if (!imageUrl?.startsWith("http")) {
                console.error(
                    `The image could not be parsed. Image URL: ${imageUrl}`
                );
                return null;
            }
            return imageUrl;
        }

        /**
         *
         * @param {string} imageUrl
         * @returns {Promise<void>}
         */
        function testImageLink(imageUrl) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.src = imageUrl;
                img.onload = function () {
                    reject();
                };
                img.onerror = function () {
                    resolve();
                };
            });
        }

        /**
         *
         * @param {HTMLDivElement} div
         * @param {string} telegramPostUrl
         */
        async function replaceImageSrc(div, telegramPostUrl) {
            const doc = await commonFetchTgPostEmbed(telegramPostUrl);
            const imgLink = commonGetImgUrlFromTgPost(doc);
            try {
                div.style.backgroundImage = `url(${imgLink})`;
            } catch (error) {
                console.error(
                    `Error parsing the HTML from the telegram post. Error: ${error}`
                );
            }
        }
    }

    /**
     *
     * @param {Node} node
     * @returns {void}
     */
    function runRestoreImagesInArticleView(node) {
        if (!appState.restoreImagesInArticleView) {
            return;
        }
        /**
         * @type {HTMLDivElement}
         */
        // @ts-ignore
        const nodeElement = node;

        /**
         * @type {HTMLDivElement | null}
         */
        const articleRoot = nodeElement?.querySelector(
            querySelectorPathArticleRoot
        );
        if (articleRoot) {
            getImageLink(articleRoot);
            return;
        }

        /**
         *
         * @param {HTMLDivElement} articleRoot
         */
        function getImageLink(articleRoot) {
            /**
             * @type {HTMLAnchorElement[]}
             */
            const ahrefElementArr = Array.from(
                articleRoot.querySelectorAll("a[href*='t.me']")
            );
            /**
             * @type {HTMLAnchorElement | null}
             */
            const ahrefElement =
                ahrefElementArr[1] ??
                ahrefElementArr[2] ??
                ahrefElementArr[0] ??
                null;
            /**
             * @type {string | undefined} telegramPostUrl
             */
            let telegramPostUrl = ahrefElement?.href ?? "";
            // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
            try {
                telegramPostUrl =
                    new URL(telegramPostUrl).origin +
                    new URL(telegramPostUrl).pathname;
            } catch (error) {
                telegramPostUrl = telegramPostUrl?.split("?")[0];
            }

            articleRoot.querySelectorAll("img")?.forEach(
                /**
                 *
                 * @param {HTMLImageElement} img
                 */
                function (img) {
                    const attributes = img.attributes;
                    const originalSrcLink =
                        attributes?.getNamedItem("data-original-src")?.value;
                    if (originalSrcLink?.includes("cdn-telegram.org")) {
                        img.onerror = function () {
                            if (telegramPostUrl) {
                                replaceImageSrc(img, telegramPostUrl);
                            } else {
                                console.error(
                                    `The image could not be loaded and no Telegram post found. Telegram post URL: ${telegramPostUrl}. Img original src: ${originalSrcLink}`
                                );
                            }
                        };
                    }
                }
            );
        }

        /**
         *
         * @param {HTMLImageElement} img
         * @param {string} telegramPostUrl
         */
        async function replaceImageSrc(img, telegramPostUrl) {
            const doc = await commonFetchTgPostEmbed(telegramPostUrl);
            const imgLink = commonGetImgUrlFromTgPost(doc);
            try {
                img.src = imgLink ?? "";
                img.setAttribute("data-original-src", imgLink ?? "");
            } catch (error) {
                console.error(
                    `Error parsing the HTML from the telegram post. Error: ${error}`
                );
            }
        }
    }

    /**
     *
     * @param telegramPostUrl string
     * @returns {Promise<Document>}
     */
    async function commonFetchTgPostEmbed(telegramPostUrl) {
        // add ?embed=1 to the end of the telegramPostUrl by constructing URL object
        const telegramPostUrlObject = new URL(telegramPostUrl);
        telegramPostUrlObject.searchParams.append("embed", "1");

        const requestUrl = appConfig.corsProxy
            ? appConfig.corsProxy +
              encodeURIComponent(telegramPostUrlObject.toString())
            : telegramPostUrlObject;
        const response = await fetch(requestUrl);
        try {
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, "text/html");
            return Promise.resolve(doc);
        } catch (error) {
            console.error(
                `Error parsing the HTML from the telegram post. Error: ${error}`
            );
            return Promise.reject(error);
        }
    }

    /**
     *
     * @param {Document} doc
     * @returns {string | undefined} imageUrl
     */
    function commonGetImgUrlFromTgPost(doc) {
        /**
         * @type {HTMLAnchorElement | null}
         */
        const img = doc.querySelector(
            "a[href^='https://t.me/'].tgme_widget_message_photo_wrap"
        );
        // get background-image url from the style attribute
        const backgroundImageUrl = img?.style.backgroundImage;
        /**
         * @type {string | undefined}
         */
        let imageUrl;
        try {
            imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
        } catch (error) {
            imageUrl = backgroundImageUrl?.slice(5, -2);
        }
        // any better way?
        if (!imageUrl?.startsWith("http")) {
            console.error(
                `The image could not be parsed. Image URL: ${imageUrl}`
            );
            return;
        }
        return imageUrl;
    }

    // Create an observer instance linked to the callback function
    const tmObserverImageRestore = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    tmObserverImageRestore.observe(targetNode, mutationObserverGlobalConfig);

    registerCommands();
})();