InoReader restore lost images and videos

Loads new images and videos from VK and Telegram in InoReader articles

目前为 2024-04-22 提交的版本。查看 最新版本

// ==UserScript==
// @name         InoReader restore lost images and videos
// @namespace    http://tampermonkey.net/
// @version      0.0.5
// @description  Loads new images and videos 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*
// @match        https://*.inoreader.com/starred*
// @match        https://*.inoreader.com/library*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/web_pages*
// @match        https://*.inoreader.com/trending*
// @match        https://*.inoreader.com/commented*
// @match        https://*.inoreader.com/recent*
// @match        https://*.inoreader.com/search*
// @match        https://*.inoreader.com/channel*
// @match        https://*.inoreader.com/teams*
// @match        https://*.inoreader.com/dashboard*
// @match        https://*.inoreader.com/pocket*
// @match        https://*.inoreader.com/liked*
// @match        https://*.inoreader.com/tags*
// @icon         https://inoreader.com/favicon.ico?v=8
// @license      MIT
// ==/UserScript==
// @ts-check

(function () {
    "use strict";

    /**
     * @typedef {Object} appConfig
     * @property {Array<{
     *     prefixUrl: string,
     *     corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare",
     *     token?: string,
     *     hidden?: boolean
     * }>} corsProxies
     */
    const appConfig = {
        corsProxies: [
            {
                prefixUrl: "https://corsproxy.io/?",
                corsType: "direct",
            },
            {
                prefixUrl: "https://proxy.cors.sh/",
                corsType: "corsSh",
                token: undefined,
                hidden: true,
            },
            {
                prefixUrl: "https://cors-anywhere.herokuapp.com/",
                corsType: "corsAnywhere",
                hidden: true,
            },
            {
                prefixUrl: "https://cors-1.kenya-west.workers.dev/?upstream_url=",
                corsType: "corsFlare"
            }
        ],
    };

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

    //
    //
    // FIRST PART - RESTORE IMAGES IN ARTICLE LIST
    //
    //
    //

    /**
     *
     * @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
         * @returns {string}
         */
        function getTelegramPostUrl(node) {
            return getFromArticleView() ?? getFromNode(node) ?? "";

            /**
             *
             * @returns {string | undefined}
             */
            function getFromArticleView() {
                /**
                 * @type {HTMLAnchorElement | null}
                 */
                const element = document.querySelector(
                    ".article_title > a[href^='https://t.me/']"
                );
                return element?.href;
            }

            /**
             *
             * @param {Node} node
             * @returns {string}
             */
            function getFromNode(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}`
                );
            }
        }
    }

    //
    //
    // SECOND PART - RESTORE IMAGES IN ARTICLE VIEW
    //
    //
    //

    /**
     *
     * @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);
            getVideoLink(articleRoot);
            return;
        }

        /**
         *
         * @param {HTMLDivElement} articleRoot
         */
        function getImageLink(articleRoot) {
            /**
             * @type {NodeListOf<HTMLAnchorElement> | null}
             */
            const ahrefElementArr =
                articleRoot.querySelectorAll("a[href*='t.me']:has(img[data-original-src*='cdn-telegram.org'])");
            const telegramPostUrl = commonGetTelegramPostUrl(node);

            ahrefElementArr.forEach((ahrefElement, index) => {
                /**
                 * @type {HTMLImageElement | null}
                 */
                const img = ahrefElement.querySelector(
                    "img[data-original-src*='cdn-telegram.org']"
                );
                if (img && telegramPostUrl) {
                    img.onerror = function () {
                        replaceImageSrc(img, telegramPostUrl, index);
                    };
                }
            });
        }

        /**
         *
         * @param {HTMLDivElement} articleRoot
         */
        function getVideoLink(articleRoot) {
            /**
             * @type {NodeListOf<HTMLVideoElement> | null}
             */
            const videos = articleRoot.querySelectorAll(
                "video[poster*='cdn-telegram.org']"
            );
            videos?.forEach((video) => {
                /**
                 * @type {HTMLSourceElement | null}
                 */
                const videoSource = video.querySelector("source");
                const telegramPostUrl = commonGetTelegramPostUrl(node);
                if (videoSource && telegramPostUrl) {
                    videoSource.onerror = function () {
                        replaceVideoSrc(videoSource, telegramPostUrl).then(
                            () => {
                                video.load();
                            }
                        );
                    };
                }
            });
        }

        /**
         *
         * @param {HTMLImageElement} img
         * @param {string} telegramPostUrl
         */
        async function replaceImageSrc(img, telegramPostUrl, index = 0) {
            const doc = await commonFetchTgPostEmbed(telegramPostUrl);
            const imgLink = commonGetImgUrlFromTgPost(doc, index);
            if (!imgLink) {
                return;
            }
            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 {HTMLSourceElement} source
         * @param {string} telegramPostUrl
         * @returns {Promise<void>}
         */
        async function replaceVideoSrc(source, telegramPostUrl) {
            const doc = await commonFetchTgPostEmbed(telegramPostUrl);
            const videoLink = commonGetVideoUrlFromTgPost(doc);
            try {
                source.src = videoLink ?? "";
                return Promise.resolve();
            } catch (error) {
                console.error(
                    `Error parsing the HTML from the telegram post. Error: ${error}`
                );
                return Promise.reject(error);
            }
        }
    }

    /**
     *
     * @param {string} telegramPostUrl
     * @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.corsProxies[3].prefixUrl
            ? appConfig.corsProxies[3].prefixUrl +
              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, index = 0) {
        /**
         * @type {NodeListOf<HTMLAnchorElement> | null}
         */
        const images = doc.querySelectorAll(
            "a[href^='https://t.me/'].tgme_widget_message_photo_wrap"
        );

        /**
         * @type {HTMLAnchorElement | null}
         */
        const img = images[index];
        // 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;
    }

    /**
     *
     * @param {Document} doc
     * @returns {string | undefined} imageUrl
     */
    function commonGetVideoUrlFromTgPost(doc) {
        /**
         * @type {HTMLVideoElement | null}
         */
        const video = doc.querySelector("video[src*='cdn-telegram.org']");
        const videoUrl = video?.src;
        return videoUrl;
    }

    /**
     *
     * @param {Node | undefined} node
     * @returns {string}
     */
    function commonGetTelegramPostUrl(node = undefined) {
        return getFromArticleView() ?? getFromNode(node) ?? "";

        /**
         *
         * @returns {string | undefined}
         */
        function getFromArticleView() {
            /**
             * @type {HTMLAnchorElement | null}
             */
            const element = document.querySelector(
                ".article_title > a[href^='https://t.me/']"
            );
            return element?.href;
        }

        /**
         *
         * @param {Node | undefined} node
         * @returns {string}
         */
        function getFromNode(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];
            }
        }
    }

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

QingJ © 2025

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